Eternet.TestPlanner
1.1.125
Prefix Reserved
dotnet tool install --global Eternet.TestPlanner --version 1.1.125
dotnet new tool-manifest
dotnet tool install --local Eternet.TestPlanner --version 1.1.125
#tool dotnet:?package=Eternet.TestPlanner&version=1.1.125
nuke :add-package Eternet.TestPlanner --version 1.1.125
Eternet Test Planner
Eternet.TestPlanner is a .NET global tool that plans the best tests to run
for a git diff. The installed command is etp.
For a Spanish (Argentina) explanation of the current ETP/EAC integration and the repository CI, see ETP, EAC y la CI de Eternet.AspNetCore.
For staged CI adoption of the native producer-build decision, see pre-build decision migration and compiler state-machine impact ownership.
See generated test dispatch contexts for receipt-bound specialization of direct Mediator test-base calls.
For recorded precision, fallback and execution scope explanations, see selection precision report. For the explicit data exception policy, see data input dependencies. For the CI measurement and staged rollout of the efficiency switches, see CI efficiency.
Local loop
For an opt-in local affected-test run with incremental build outputs, shared analysis cache, bounded run retention, fragment retries, and agent output, see the local loop guide. Local and imported-baseline evidence is never certifiable; keep isolated CI runs for publication.
The goal is fast, reviewable affected-test planning for Eternet repositories:
run the smallest useful set of tests when there is enough evidence, and widen
explicitly when the tool cannot prove a narrower selection. etp is not a
general-purpose test oracle and it is not a replacement for nightly, release, or
full-suite safety nets.
CI savings self-report
workflow affected-tests finalize writes an additive savings object with schema
etp.savings.v1 into affected-tests-receipt.json, a CI savings vs full suite
table in summary.md, and one etp-savings: line on stderr; stdout carries only
the receipt JSON. The receipt's etp.affected-tests-workflow.v2 schema remains
unchanged.
The test projection sums whole-project runner times from the duration baseline
for inventory test projects. Actual test machine time sums fragment process wall
time, including class and project fragments. The percentage compares only
projects with baseline runner observations. The build projection counts the
prepared graph closure of every inventory test project; actual build projects
come from stage build graph roots and their closures, counted within that same
closure, so a forced-full run saves 0%. Projects built outside it (bootstrap
tooling, snapshot completion) are reported separately as
outsideTestClosureProjectsBuilt. Missing evidence yields
partial with reason codes while failed runs or missing prepared graphs yield
unavailable. Savings reporting does not change the finalize exit code.
When any compiled-ownership step ran, the savings object also carries an
ownership block, a Compiled ownership: cost vs avoided work table in
summary.md, and an ownership=<cost>/proven=<n>of<m>/avoided=<p>p token on the
etp-savings: line. It weighs the cost of each step (HEAD inventory capture,
receipt import enrichment, native and staged proof, the post-build witness, and
a candidate replan that HEAD ownership forced) against the projects a global
fallback would have affected without the proven sources and the source scopes
dropped after the build. rejectedSources counts unproven ownership diagnostics
by reason (a source can fail native proof and then staged evaluation) and
rejectedShards counts carriers unusable for ownership by reason, so a run shows
which limit below it hit.
For CI, pass --test-duration-baseline to workflow affected-tests plan using
the latest successful forced-full run's durations/etp-test-durations.mpack.
etp ci evaluate reads the receipt block for X6, counts successful measured
and partial records, and flags duration baselines older than seven days.
CI efficiency commands
Evaluate a versioned replay corpus against either the frozen CI CSV or a prior
evaluation report. Run roots under --records must be named for their corpus
case IDs. Pass one or more audit JSON files and any certified differential
references when evaluating selection safety:
etp ci evaluate --corpus ci-replay-corpus.json --records replay-records \
--baseline-csv etp-ci-runs.csv --audit audit-1.json audit-2.json \
--reference-selection case-id=reference-selection.json --output evaluation
--reference-selection is the differential oracle: every test selected in the
reference (for example a frozen baseline CI run's effective selection) must be
covered by the executed tests of the case's run root. Method targets compare
without a trailing empty (); class and project selections cover the methods
they own. A missing test is a possible miss; see docs/ci-efficiency.md for
how missing baseline tests are classified.
The output directory contains ci-evaluation-report.json and
ci-evaluation-report.md. A threshold with fewer than five applicable records
or missing CI timing fields is insufficient-data. etp audit --manifest <effective-selection> --broad-results <full-suite TRX files> checks whether a
failed test in the broad run was omitted; a broad run with no failed tests
cannot by itself certify the absence of under-selection.
The selected build records etp.build-set.v1 project accounting. Its producer
split runs only when it avoids at least 12 projects and 30% of the candidate
closure by default. workflow affected-tests build accepts
--prebuild-split-min-avoidable-projects and
--prebuild-split-min-avoidable-fraction to tune that decision; 0 for the
project minimum restores the earlier split eligibility.
The additional selection and execution options are opt-in:
--precompile-narrowing onuses certified predecessor compiler evidence to narrow before the build. The default isoff; missing or stale evidence widens selection.--shard-long-polesuses the partition settings' process-isolated safe projects and class inventory to divide long whole-project work. The default is off; pass--partition-settings <json>for the host's isolation policy.--build-output-reuse verifychecks a local output store while compiling normally; useononly after verify has no mismatches. A mismatch fails the build stage (reuse-invalidated-output-mismatch) and quarantines the entry. On the preview SDK, verify can report a mismatch for an identical tree (seedocs/ci-efficiency.md, "Local replays and the differential oracle"). Supply--build-output-store <dir>and a stable logical build-root alias per runner.build-output-store publish|inspect|prunemanages certified entries;publishverifies the run root'spublication-intent.json(output manifest and package graph bound at finalization) exactly like deferred publication and refuses a run without one (build-output-manifest-missing).--allow-unbound-publicationis a developer-only escape that skips those comparisons (the summary reportsunbound-publication); CI never uses it. The default local size cap is 2 GiB. A deterministic build with-p:IncludeSourceRevisionInInformationalVersion=falseis required for cross-revision reuse. A miss or uncertified output compiles normally.workflow affected-tests finalize --defer-publication(orrunwith the same flag) writes apublication-intent.json. After reporting certified status, callworkflow affected-tests publish --run-root <dir>with the required store, checkpoint, and archive inputs. Publication is resumable; its receipt records each item. Keep legacy evidence staging for runs that never reached deferred finalize.durations export --calibrate --results <TRX directory> --output <file> --format messagepackwrites a v2 duration baseline when native execution receipts and attributed TRX time allow calibration. Existing v1 baselines remain readable. An explicit process startup value in CLI or partition settings takes priority over calibrated startup.
Plan widening changes executable obligations, not their authority: the original input-policy hash, source diff and graph evidence remain attached to the plan, including its canonical MessagePack representation. Plans produced by an older tool that lost the policy hash must be regenerated; the build-stage policy check is not bypassed. No payload schema migration is required.
Native lane execution accepts portable manifest separators with Windows extended
workspace paths (\\?\...). Filesystem resolution converts separators before
checking containment; the same outside-workspace rejection applies.
Portable snapshot composition preserves a validated project identity when its dependency closure is stale. The stale shard contributes no semantic or test inventory authority: a newer build replaces it, or snapshot completion rebuilds the project. Corrupt receipts and unvalidated foreign identities still fail composition instead of reviving older evidence.
Prepare's per-project CacheKey, the DependencyClosureFingerprint derived from it, and
PortableGraphFingerprint key the output location MSBuildProjectExtensionsPath by its
workspace-relative path (a relative value is resolved from the project directory), or as
<outside-workspace> when it lies outside the workspace. Files that evaluation imports from
such an external location (for example $(MSBuildProjectFile).*.props beside the restore
outputs) stay keyed by content, named relative to the common directory of every graph
variant's location plus the target frameworks that import them, so one variant's import never
stands in for another's. NuGet's generated
*.nuget.g.props/targets are left out because prepare excludes the package imports they
declare, the package set they record is keyed through its inputs, and their own contents
carry run-specific paths. A per-run build root outside the workspace (such as EAC's
EAC_AFFECTED_TEST_BUILD_ROOT) therefore no longer makes every predecessor receipt stale on
the next run. Evaluated properties such as DefineConstants or AssemblyName, package
references, and workspace imports and inputs stay keyed. Receipts that earlier versions wrote
under an out-of-tree root mismatch once and are rebuilt; in-tree layouts keep their keys.
GraphFingerprint remains the exact, workspace-bound graph identity. Like the rest of the
prepare fast cache, a fast hit does not revalidate files outside Git's view (ignored restore
outputs in obj/ or an external output location); a fresh --cache-directory forces a new
evaluation.
Build-impact carriers now use etp.build-impact.v7. Full and incremental test
entries retain CLR metadata class names (for example Outer+Inner, including
generic arity), rather than deriving runner filters from documentation IDs.
Existing v5/v6 snapshots must be fully rebuilt before they can seed new deltas;
unchanged nested tests in an older base still contain invalid filters. The delta
contribution fingerprint algorithm remains unchanged across its producers and
verifiers; the full carrier schema gates compatibility.
What etp Is
etp is an affected-test planner for modern .NET repositories that follow the
Eternet stack and conventions. It combines cheap repository signals, MSBuild
ownership, Roslyn analysis, and Eternet-specific structural patterns to produce
versioned manifests that can be executed by Microsoft Testing Platform.
It is designed for repositories that use:
- SDK-style .NET projects.
- Central Package Management (CPM).
- Microsoft Testing Platform v2.
- xUnit v3 or modern xUnit-style test metadata.
- Eternet packages and conventions, especially
Eternet.Mediator.*, source generators, analyzers, generated pipeline patterns, and Eternet CLI or service contract layouts.
The first supported target is .NET. Other languages and test runners are out of scope for now.
What etp Is Not
etp does not guarantee that a selected subset is complete in every possible
runtime scenario. It does not understand arbitrary reflection, dynamic loading,
external services, mutable databases, or hidden runtime wiring unless there is a
deterministic pattern, semantic edge, history, or explicit fallback rule for it.
etp also does not decide workflow policy. CI still owns retries, timeouts,
parallelism, required checks, broad fallback suites, and release gates.
When confidence is low, etp should widen, not guess.
etp is also not trying to be a universal test-selection platform. Building a
general-purpose planner that works across languages, build systems, test
runners, generated code styles, service frameworks, and organization-specific
runtime conventions is a much harder problem. Large engineering organizations
can invest in broad dependency indexing, coverage infrastructure, remote build
graphs, ownership metadata, and long-running historical feedback loops. That is
not the scope here.
The pragmatic bet for Eternet is narrower and more useful: make a fast, deterministic planner that understands our .NET repositories, our package ecosystem, our generators, our analyzers, and the patterns our teams actually use.
Experimental session server
ETP can keep one CLI host warm for the lifetime of a CI flow or local plan. The
feature is opt-in: normal etp commands continue to execute in their own process
unless ETP_EXECUTION_MODE=server is set.
$session = "local_plan_a"
etp server start --workspace . --session $session
$env:ETP_EXECUTION_MODE = "server"
$env:ETP_SERVER_SESSION = $session
etp workflow affected-tests plan <arguments>
etp workflow affected-tests build <arguments>
etp workflow affected-tests test <arguments>
etp workflow affected-tests finalize <arguments>
etp server stop --workspace . --session $session
Each instance belongs to one workspace, session, tool version and private token. Requests for that flow are serialized; parallel work inside an individual ETP command is unchanged. Separate sessions allow multiple flows on the same machine or even the same workspace without sharing mutable CLI state.
The local transport is a current-user named pipe with length-framed MessagePack.
Set ETP_SERVER_PROTOCOL=json before server start only when a readable wire
format is useful for diagnosis. The small state descriptor remains JSON and
contains process, session, workspace, protocol, version, token and one environment
fingerprint; command payloads use the selected pipe protocol. A client with a
different environment sends the complete current environment for that request so
commands preserve normal CLI semantics across CI steps.
.github/workflows/diff-affected-tests.yml exposes experimental_etp_server for
manual runs. Its default is false, and push, pull-request and scheduled runs keep
the existing CLI behavior. The reusable replay workflow contains one copy of the
plan/build/test/finalize sequence; the boolean only starts the session and selects
its transport.
Server lifetime does not make every cache valid for the whole flow. DI singletons
reuse the command host, while compiler evidence remains bounded to a quiescent
build phase and is invalidated by later builds. Cross-command workflow artifacts
use their own exact identities: in particular, build reuses the persisted plan
prepare manifest only when workspace, base, head, scope, schema and portable graph
fingerprint all match. The cache and transport measurements behind these choices
are recorded in the CI ownership audit.
Native SDK-aware graph fingerprint
etp ci graph-fingerprint --workspace . --output artifacts/graph-fingerprint.json
prints only the fingerprint to stdout and optionally writes a compact receipt.
CI owns exporting that value to GITHUB_ENV; the command works identically locally.
The etp.graph-fingerprint.v1 contract hashes repository-relative paths and raw file
content for global.json, NuGet.Config, .csproj, .props, .targets, .slnx
and eng/lanes/**/*.json. Paths are ordinal-sorted, hash fields are length-framed,
and file content is streamed. Generated/cache directories (bin, obj, artifacts,
.etp, .git, .eac, node_modules) are pruned before traversal. Symbolic links
in the graph input tree fail explicitly rather than introducing outside-workspace inputs.
Every global.json must declare sdk.version. ETP runs dotnet --version once per
SDK context, maps each project to its nearest context, and records requested and
effective versions. Projects without a repository global.json share a default
SDK probe. Missing SDKs, malformed input, or a probe exceeding 30 seconds fail;
no guessed version or fallback fingerprint is emitted. --dotnet-command chooses
an explicit dotnet executable when needed.
This replaces EAC's PowerShell graph fingerprint with a new deterministic contract. The hash intentionally changes once during adoption (including ordinal ordering, raw-byte hashing and cache-tree pruning); regenerate dependent evidence. Do not probe the previous fingerprint or retain a second implementation for compatibility.
Native green-tree self-sealing
The native green-tree protocol freezes the reusable obligation before build, then binds that exact obligation to terminal build/test receipts in the same DAG. A post-build refinement cannot silently replace the selection or partition manifest that will later authorize reuse.
CI adapters should use the high-level workflow commands. prepare freezes the
pre-build manifests and returns a typed execute or reuse; finalize consumes
that preparation plus the current run identity and exact terminal lane outcomes.
It returns reused without creating a seal, or writes a transport-independent
seal preparation for publication:
etp green-tree workflow prepare --repository <owner/repo> --tested-head <commit> --tested-tree <tree> \
--selection-manifest <selection.json> --partition-manifest <partitions.json> \
--continuous-coverage-base <commit> --continuous-coverage-mode diff-affected-tests \
--analysis-depth semantic --workflow "Diff Affected Tests" --finalizer green-tree-validation \
--protected-branch main --artifact-root <root> --trusted-store-root <trusted-root> --output <preparation.json>
etp green-tree workflow finalize --preparation <preparation.json> --artifact-root <root> \
--event pull_request --run-id <current-run> --run-attempt <current-attempt> --pull-request-number <number> \
--base-commit <commit> --workflow "Diff Affected Tests" --ref <ref> --protected-branch main \
--head-repository <owner/repo> --finalizer green-tree-validation --verifier <identity> \
--protected-definitions-unchanged true --build-outcome success --tests-outcome success \
--output <finalization.json>
These commands own canonical contracts, fingerprints, timestamps, obligation identity, terminal receipts, same-DAG authority, and seal preparation. The CI adapter owns only event/branch policy and trusted-store transport, locking, CAS, and atomic publication.
For the native affected-tests lifecycle, replace the two manifest arguments with
--affected-tests-run-root <run-root> immediately after workflow affected-tests plan
and before build. ETP reads the authoritative ci-plan.mpack and prepare identity
from that run, validates the requested head, tree, coverage base, analysis depth,
selection count and partition count, and freezes selection/partitions as .mpack
under the green-tree artifact root. It does not emit large JSON payload copies.
Compiler-index planning may preserve an effective fast-project depth when a
requested semantic plan takes its recorded conservative fallback or control-plane
bypass. Green-tree accepts this only with an owned baseline and matching fallback
evidence in the plan and workflow state. The frozen selection retains its actual
depth and decisions; the obligation retains the requested analysis policy.
The small preparation, terminal and publication receipts remain JSON.
This entry rejects dirty workspaces recorded at planning, failed or post-build states, unsupported state schemas, escaping artifact paths and missing/inconsistent plan evidence. It never rebuilds a missing partition or silently falls back to the standalone manifest inputs. The two input forms are mutually exclusive. Existing standalone callers may supply either canonical MessagePack or JSON manifests; their file extensions are preserved when freezing them. Native plans require current v3 state: generate a fresh plan instead of migrating timestamps or relabeling an old post-build state as a pre-build obligation. The workflow configuration comes from the native state rather than a CI-side constant.
When this native input form is used, also pass --affected-tests-run-root <run-root>
to green-tree workflow finalize, after native workflow affected-tests finalize
succeeds. ETP validates the successful terminal receipt, matching head/tree/base,
completed partitions, current artifact set and file hashes, and its binding to the
original pre-build plan. Successful GitHub lane outcomes remain independently required;
they cannot substitute for missing native evidence. The plan receipt hash is run-local
preparation metadata, not part of the reusable obligation identity (plan telemetry and
run paths must not invalidate otherwise identical reuse). Native terminal hashes are
included in the sealed lane receipts. Exact trusted reuse needs no new test receipt.
Frozen native obligation payloads omit elapsed stage timings and cache observations;
semantic budgets, selected scope, fallback decisions and partition costs remain bound.
The original native plan retains its full telemetry and is bound by its run-local hash.
The stable affectedTestsEvidence=native-terminal-v1 validation input requires this
gate even if run-local binding metadata is absent. Existing native obligations must
be prepared again when upgrading; standalone manifest callers retain their lane gate.
Keep the preparation immutable between its creation and finalization, as with the
existing obligation. This validates native terminal evidence; it does not re-run tests.
Native seals also carry the effective post-build selection and partition manifests,
plus portable partition execution receipts. Native finalization with
--reuse-preparation <preparation.json> requires those effective manifests to match
the current build, in addition to the pre-build plan binding. Elapsed timings,
cache observations and run-local plan identity are excluded from the comparison;
selection decisions, budgets and widening reasons remain included. Missing or
changed effective evidence fails closed. Checkpoint publication validates the
reused partition/fragment coverage and copies the portable evidence into its
candidate without inventing local test execution.
Freezing reuse evidence stages an owned snapshot and publishes it atomically.
Retries accept an already complete, identical, validated snapshot; conflicting
files are preserved and rejected. Tree equivalence is authorized by the canonical
policy fingerprint, so both the producing and consuming preparation must opt in
with --allow-tree-equivalence. Editing a decision reason cannot grant that
permission. Records created under the previous policy must be regenerated.
etp green-tree obligation create --input obligation-draft.json --artifact-root artifacts --output obligation.json
etp green-tree propose --input proposal-draft.json --artifact-root artifacts --output proposal.json
etp green-tree verify --proposal proposal.json --authority authority.json --artifact-root artifacts \
--required-workflow "Diff Affected Tests" --required-branch main \
--required-finalizer green-tree-finalizer --current-run-id <run-id> --current-run-attempt <attempt> \
--verifier-identity eac.private.same-dag --output verification.json
etp green-tree seal --proposal proposal.json --verification verification.json \
--artifact-root artifacts --record-output trusted-record.json --output prepared-record.json
etp green-tree decide --record trusted-record.json --artifact-root artifacts \
--expected-repository Eternet/Eternet.Agents.Control \
--expected-obligation sha256:<hex> --expected-tested-head <commit> --expected-tested-tree <tree> \
--expected-validation-fingerprint sha256:<hex> --expected-scope-fingerprint sha256:<hex> \
--expected-policy-fingerprint sha256:<hex>
obligation create requires the validation scope to bind the exact pre-build
selection and partition hashes, and requires those two immutable files as its
artifacts. propose adds successful terminal receipts whose tested head/tree
and lane set must exactly match the obligation. verify treats source authority
as a strict same-DAG finalizer contract: authorityKind must be
same-dag-finalizer, the current run must still be in_progress, its run id
and attempt must equal the caller-provided current run, and every required
upstream lane must already be terminal success. A completed run, a
workflow_run-style sealer, an external run, or an incomplete/failed needs
set is an authority failure (exit 3); there is no asynchronous fallback.
Portable proposal rejection remains a typed exit 2. seal produces a
publication key, trusted record, hashes, and copy candidates only. It performs
no storage lookup, lock, CAS, rename, or copy; the transport adapter owns those
operations and can pass --existing-record for deterministic
idempotency/collision classification. decide returns
reuse only for an exact obligation/head/tree/contract/scope/policy match.
Missing or mismatched candidates return typed execute/exit 2; tampered
authority certification fails with exit 3.
Green-tree validation receipts
ETP can create and verify a portable, deterministic receipt for a completed validation wave. It owns receipt normalization, scope hashing, expiration, and artifact-table verification; the caller owns CI provenance and transport.
etp validation-receipt create --input receipt-draft.json --artifact-root artifacts/receipts --output receipt.json
etp validation-receipt verify --receipt receipt.json --artifact-root artifacts/receipts
etp validation-receipt decide --receipt receipt.json --artifact-root artifacts/receipts \
--expected-repository Eternet/Eternet.Agents.Control \
--expected-tree <tree-sha> \
--expected-validation-fingerprint sha256:<hex> \
--expected-scope-fingerprint sha256:<hex>
etp validation-receipt inspect --receipt receipt.json
etp validation-contract fingerprint --input validation-contract.json
etp validation-scope fingerprint --input validation-scope.json
Receipts are intentionally bounded to compact evidence (128 files, 64 MiB per
file, 128 MiB total). A verification failure is a conservative miss, not a
reason to skip the normal build or tests. verify checks portable receipt
integrity; decide additionally compares the receipt with the current
repository/tree/contract/scope obligation and exits with 2 for a conservative
execute decision.
The fingerprint commands accept only their versioned schemas and reject missing
or unknown properties. A validation contract also requires a non-empty
externalInputs string map for mutable inputs outside the Git tree (for example
test-platform/action/package-source identities, test arguments, or relevant
MSBuild properties). Scope inputs are therefore never silently reduced before
hashing. JSON numbers are normalized canonically (so equivalent spellings do not
create avoidable misses), and verify applies the same strict raw scope check
as create and decide. A missing, unreadable, or malformed receipt from
decide produces a structured execute decision with exit code 2. Decision
identity is published beneath expected and actual objects, with camel-case
field names, so adapters can consume one stable contract.
Build certification follows a stricter two-phase contract. verify and
decide are repeatable, read-only inspections; they never consume proposal
state. Only build-certification seal consumes the proposal nonce, after the
artifact table and verifier identity have passed validation. A second seal of
the same proposal is rejected with nonce-reused. This lets trusted consumers
inspect or reuse an accepted certification without turning ordinary reads into
order-dependent behavior.
How Selection Works
Planning is intentionally layered from cheap to expensive:
- Git diff discovers changed files between
--base-refand--head-ref. - Global fallback rules classify changes to solution, build, package, props, targets, workflow, and other broad-impact inputs.
- MSBuild ownership maps changed inputs to affected projects and reverse project-reference test candidates.
- Roslyn semantic analysis narrows candidates to changed declarations, referencing tests, callers, relationships, fixtures, attributes, generics, interface/override links, and Eternet.Mediator pipeline edges.
- Structural contract analysis supplements Roslyn where the tested contract is not a direct symbol reference.
- The manifest records selections, confidence, reasons, graph paths, fallback reasons, telemetry, and cache diagnostics.
A multi-targeted project has several MSBuild graph nodes. Its inputs, imports,
references and packages are the union over all of them, while scalar evaluation
properties (TargetFramework, DefineConstants, effective package versions)
come from one representative evaluation: the node with the most items, then the
first framework declared in TargetFrameworks (the project's default target),
then an ordinal comparison. MSBuild enumerates graph nodes in an order that
varies between processes, so the choice never depends on node order; repeated
plans produce the same GraphFingerprint and project cache keys.
The planner prefers method selections, then class selections, then project selections. Project selections are conservative fallback evidence, not a failure.
Eternet Patterns
etp is intentionally opinionated about Eternet repositories. That is the
point: it should learn the frameworks and conventions that matter here instead
of pretending to be a generic black box.
Current deterministic signals include:
- MSBuild project ownership and reverse test-project closure.
- Roslyn symbol references from tests to changed production symbols.
- Changed test method and changed test class source detection.
- Related symbol traversal for callers, fixtures/helpers, attributes, constructed generics, interfaces, overrides, and selected deep relationships.
- Eternet.Mediator pipeline relationships such as generated execute pipeline steps, request/response links, validators, retry providers, execution policies, branch paths, generated pipeline test-base anchors, and generated pipeline ordering.
- Eternet.Mediator helper and dependency closure: changed pure static helpers select consuming generated steps and owning pipeline tests; source-owned workflow dependencies select bounded consuming pipelines or widen with an explicit Mediator budget fallback.
- CLI structural contracts: production declarations such as
new Command("contracts")plus tests that invoke the same command path, for examplecontracts scaffold.
The structural-contract layer exists because many real tests exercise public
contracts rather than calling implementation symbols directly. A CLI test may
call RunAsync("contracts", "scaffold") without referencing
ContractsCommandModule; an HTTP test may exercise a route without referencing
the endpoint builder; a generated Mediator pipeline test may assert generated
behavior through contracts. These are not semantic symbol edges, but they are
real evidence when the pattern is deterministic.
Future pattern families should follow the same rule: add reproducible, auditable evidence first, and only widen when evidence is missing.
Mediator Impact Graph
The Mediator layer models generated pipelines as first-class impact facts. It tracks handlers, requests, responses, generated steps, step inputs and outputs, execution policies, retry providers, validators, explicit branch paths, generated pipeline test-base anchors, synchronous pure helpers, narrow infrastructure adapters, and source-owned workflow dependencies.
When those facts are complete, etp can select direct step tests, generated
pipeline test-base classes, branch-specific tests, retry or validator behavior
tests, and pipeline tests reached through helper or dependency closure. The
manifest records reason codes such as
ChangedMediatorStepSelectedPipelineTests,
ChangedMediatorPureHelperSelectedPipelineTests,
ChangedMediatorValidatorSelectedPipelineTests, and
GeneratePipelineTestBaseAnchoredPipelineSelection so reviewers can audit why a
test was selected.
The graph stays conservative. etp widens to candidate test projects when
Mediator ownership is ambiguous, helper purity is not proven, branch evidence is
missing, dependency fan-out crosses unrelated feature areas, or global build
inputs changed. These fallbacks are explicit safety behavior, not silent misses.
Peer compiler generators are inventoried through
etp.peer-generated-sources.v1. The manifest carries only generator-relative
paths, SHA-256 hashes, byte counts, project build identity, and SDK identity;
it never transports generated source files or absolute obj/RAM paths. ETP
accepts that evidence only when the receipt hash and the adjacent IL/PDB graph
are complete. Otherwise it widens with one explicit reason such as
peer-generated-sources-assembly-impact-incomplete; malformed and legacy
manifests remain separately observable and fail closed.
Promotion gates can require the exact compiler projects whose evidence must be complete:
etp build-index --root artifacts/impact-index --allow-partial `
--require-project tests/Eternet.Agents.Control.Cli.Tests/Eternet.Agents.Control.Cli.Tests.csproj `
--require-project tests/Eternet.Agents.Control.Web.Tests/Eternet.Agents.Control.Web.Tests.csproj
--allow-partial may tolerate unrelated incomplete shards, but it never hides a
missing or unusable required project. The JSON receipt records a
required-project-carrier-missing:<path> or
required-project-carrier-unusable:<path> reason and the command exits with
code 2.
Repository snapshot promotion should use the selection-workspace gate instead of maintaining a hard-coded list of important tests:
etp build-index --root artifacts/impact-index --allow-partial `
--workspace $PWD `
--prepare-manifest artifacts/etp-prepare.json `
--require-complete-workspace
For inspection immediately after a build with redirected intermediates, also pass
the current build's evaluation properties using repeated --msbuild-property NAME=VALUE arguments. For example, this repository uses
--msbuild-property "EacAffectedTestBuildRoot=$buildRoot". This allows MSBuild to
import the actual restored NuGet targets, including Compile Remove rules.
These are caller-supplied current paths, never recovered from a producer receipt.
Restored NuGet compile items can move between package caches only when package
identity, version, internal source path and compiler-recorded content hash agree.
Unrecorded source files and changed package content still invalidate the shard.
ci refine-post-build accepts the same repeated evaluation-property arguments.
workflow affected-tests build automatically forwards the context from its own
restore receipt to both pre-build and post-build refinement, including ETP-owned
artifacts output paths.
Here “complete workspace” means every standalone test execution project plus
its transitive ProjectReference closure. Standalone test projects require a
complete test inventory; referenced C# projects require usable semantic impact
evidence. Unrelated applications, benchmarks, plugin fixtures, and unsupported
non-C# project types are not promotion requirements unless they occur in that
closure. This is independent of the current diff's CandidateTestProjects, so
the promoted snapshot remains safe for a later change anywhere in the test
execution graph.
Compatibility note: ETP 1.1.30 briefly interpreted
--require-complete-workspace as every project in etp.prepare.v2. Consumers
that intentionally need evidence for a non-selection project should continue
to add --require-project <path> explicitly; normal snapshot promotion should
adopt the selection-closure behavior above without repository-specific lists.
Prepare a full-v3 promotion candidate locally before the host takes its store lock:
etp build-impact-snapshot publication prepare `
--workspace . `
--source-root artifacts/impact-index `
--prepare-manifest artifacts/etp-prepare.json `
--repository Eternet/Eternet.AspNetCore `
--etp-version 1.2.3 `
--scope repository `
--input-fingerprint <hex> `
--commit <40-hex-sha> `
--require-complete-workspace `
--candidate-root artifacts/snapshot-candidate `
--pointer-output artifacts/latest.txt.candidate `
--existing-object-root <optional-commit-object-root> `
--existing-pointer <optional-latest.txt> `
--output artifacts/snapshot-publication.json
ETP discovers and validates the current etp-layout-v3 carriers, applies the
same test-execution-graph inspection as build-index, and emits create,
no-op, or reject. A prepare manifest transported from another runner is
accepted only when its HeadRef matches the exact publication commit and its
project/change paths remain repository-relative. ETP resolves those paths in
the destination workspace and validates the carriers against that checkout;
the original manifest is not rewritten. A workspace-path difference alone
does not require rebuilding tests. Changed destination inputs still invalidate
carrier evidence, and partial snapshots remain ineligible for promotion.
Both publication prepare and publication publish accept repeated
--predecessor-root <full-v3-root> arguments, ordered oldest first (for
example, owned baseline followed by current pre-build evidence). --source-root
is newest. ETP uses the same whole-project replacement as post-build refinement:
new evidence replaces every prior TFM of that project, even when the new carrier
is invalid. An old valid carrier never repairs an invalid current one. The ZIP
contains exactly the chosen project carriers and their companion files; only
the current root contributes non-layout metadata. The composed coverage is
validated against the destination checkout before pointer eligibility is granted.
Missing, overlapping or duplicate roots and colliding archive paths fail rather
than dropping inputs or selecting an arbitrary winner. Single-root callers and
the v3 snapshot restore format are unchanged; no consumer-side tree merge or
schema migration is needed.
For an initial complete seed, opt into workflow affected-tests build --complete-snapshot
(also available on run). After the selected build and before post-build refinement, ETP compares the validated
current and predecessor carriers with the entire test-execution project closure.
Only missing or unusable projects are submitted to an additional bounded build;
their project dependencies remain MSBuild-owned. This does not add test scopes,
fragments or partitions. A complete existing carrier set performs no extra build.
The extra build reuses the native output root and has its own receipt, timing and
snapshot-completion build-root count in the final summary. The coverage decision
is recorded in build/snapshot-completion.json; publication inputs include the new
carrier root and preserved predecessors. A failed compilation or remaining coverage
gap fails the stage and cannot produce publication authority. No-op plans cannot
use this option to invent executed test evidence. The option is not implicit on
ordinary incremental runs. Incremental parent-checkpoint publication instead consumes
the verified fragments already carried by the owned v2 ephemeral overlay; it does not
rescan an external fragment directory after tests finish.
When an unusable required predecessor forces a consumed checkpoint to be reseeded,
the native build automatically repairs the carrier closure required by the original
plan before running tests. Candidates removed by refinement remain part of that
publication contract, but their tests are not added back. This bounded reseed repair
uses the same completion receipts and validation; a complete workspace still requires
the explicit --complete-snapshot option.
Source-owned v8 deltas cannot advance a changed project from an expanded v7
carrier. Native builds detect that transition before compilation and record
checkpoint-carrier-schema-full-reseed:<project>. The current build stage has one
global carrier mode and a checkpoint has one schema fingerprint, so this one-time
migration rebuilds the already selected execution graph with full carriers and
publishes a new mixed-schema seed. It preserves the selected tests and every
validated predecessor root; unrelated v7 projects remain reusable. It does not
force a workspace build or add tests. Legacy projects without changed C#/Razor
inputs do not request this migration.
A future per-project migration can introduce an effective carrier mode per project in the MSBuild graph and a certified full-carrier replacement operation in the ephemeral overlay. That operation must bind the replacement to the tested head, preserve unchanged project contributions, and migrate the checkpoint schema without interpreting a v7 expanded contribution as a source-owned v8 delta.
After workflow affected-tests finalize succeeds, use
--affected-tests-run-root <run-root> instead of --source-root,
--predecessor-root, --prepare-manifest, and --msbuild-property on either
publication command. ETP verifies the native terminal and exact clean tested
checkout before any publication write, then loads the build-recorded roots and
restore context from build/snapshot-publication.mpack, whose hash is bound by
the terminal receipt. Manual input overrides cannot be mixed with this mode.
Owned run/workspace paths in evaluation properties are rebased when transported;
external SDK, package and feed paths retain their identity and must validate at
the destination. Baseline file inventory and hashes are revalidated too.
Compatibility: older runs without this descriptor must execute a fresh native build/test/finalize before using native publication. There is no inference from directory names and no conversion of old receipts into publication authority. Failed rebuilds clear the descriptor binding; a previous green attempt cannot authorize publication. A green terminal authorizes inspection, not completeness: the normal full-workspace coverage gate still controls pointer eligibility.
Consumers restore only exact complete snapshots through ETP as well. The fingerprint command owns the compatibility key, and restore validates the pointer, immutable metadata, publication proof, archive SHA-256, carrier count, and ZIP containment before atomically creating a fresh local root:
$fingerprint = etp build-impact-snapshot fingerprint --workspace .
etp build-impact-snapshot restore `
--workspace . `
--store-root <durable-store> `
--repository Eternet/Eternet.AspNetCore `
--etp-version 1.2.3 `
--scope repository `
--input-fingerprint $fingerprint `
--expected-commit <40-hex-base-sha> `
--output-root artifacts/restored-impact-index `
--output artifacts/snapshot-restore.json
A missing or non-exact pointer produces a typed unavailable receipt and no local
payload; it does not authorize a full-suite fallback. Corrupt storage fails the
command, and an existing destination is never overwritten.
The exact base of a pull request often has no certified snapshot yet: the protected
branch's run for it may still be running, may have been cancelled by a newer push, or
may have been skipped by path filters. --max-ancestor-distance <n> (0-256, default 0)
opts into a bounded fallback: when the exact object is unavailable, ETP reads the
first-parent lineage of --expected-commit from --workspace and restores the nearest
ancestor whose complete snapshot exists under the same input fingerprint and scope.
ETP searches its current package directory and at most seven recently modified sibling
package directories, checking candidate commits in first-parent order. Cross-version
reuse requires an explicit matching EvidenceCompatibilityVersion in certified snapshot
metadata. The ETPVersion in that metadata remains the actual producer and must match
the storage directory. Metadata from before this field was introduced remains usable
only with the same ETP package version. A newer reader must bump
CurrentEvidenceCompatibilityVersion whenever it changes the meaning or required shape
of compiler carriers, graph inputs, task/ISG output, snapshot context, or their consumers;
the snapshot metadata SHA in the publication proof binds the recorded epoch. A package
release without such a change can reuse the previous package's certified evidence.
The generator stamps the carrier epoch; the build task copies it into its canonical
receipt and stamps any assembly-impact graph it computes. Publication checks every
selected receipt, including carriers drawn from predecessor roots, and checks the
assembly epoch when a graph exists. Archived receipts retain their actual build-task
producer versions and bind the carrier and assembly graph hashes.
Archive and green-pointer paths remain under their original etp-<version> directories.
After a complete publication, ETP atomically registers its producer namespace in the
repository's windows/evidence-versions.v1.json catalog. This bounded catalog is a
discovery hint, never a substitute for snapshot metadata and proof validation. It keeps
the eight most recently registered package versions under a shared file lock; retention
may remove an older namespace, which restore skips. A missing or malformed catalog uses
a bootstrap scan of at most 64 version directory entries plus one truncation sentinel;
the next complete publication uses that same bounded scan to rebuild the catalog before
registering its own version. A completed, pointer-eligible publication writes a small
epoch marker under its version namespace before catalog registration. Bootstrap checks
at most 64 such markers before choosing seven siblings, so rejected or incomplete
namespace directories cannot crowd out a completed producer. Existing namespaces
without this marker are not migrated into cross-version discovery; their same-version
objects retain the existing metadata and proof checks. The marker is only a discovery
hint: every candidate still needs its own compatible metadata and publication proof.
The catalog records when its bounded selection omitted namespaces, and restore can
refill slots vacated by retention from a bounded scan.
Directory enumeration order is not guaranteed, so that fallback can miss a compatible
version once the input bound is reached; the failure receipt reports
snapshot-version-scan-truncated. An exact object in the running package is restored
before any catalog read or sibling discovery.
ETP lists each selected fingerprint's commits directory rather than probing every
ancestor. A metadata-only epoch gate checks at most 24 candidate objects per search;
legacy or incompatible objects do not consume the three full proof/archive validations.
The receipt reports compatibilityMetadataChecks and incompatibleEvidenceObjects.
If the metadata cap is reached it reports snapshot-compatibility-metadata-limit;
if candidate objects have no compatible epoch it reports
snapshot-evidence-compatibility-unproven. If older version directories were omitted
and no object restores, the receipt includes snapshot-version-scan-truncated. The
restored receipt reports snapshotEtpVersion for publisher package provenance and
snapshot-compatible-version-restored when it differs from the running package. The
receipt then carries the restored commit, requestedCommit, ancestorDistance,
ancestorSearchMilliseconds, and the reason codes of the exact miss followed by
snapshot-ancestor-restored. When nothing matches it reports
snapshot-ancestor-not-found, adds snapshot-graph-fingerprint-changed when the same
lineage exists only under another fingerprint (the change edited build-graph inputs, so
no compatible compiler baseline can exist), or snapshot-ancestor-lineage-unavailable
when the workspace cannot prove the lineage. With distance zero, ETP may still restore
the requested commit from a compatible package version; it does not search ancestors.
The restored commit is the planning base. Pass it as --base-ref and pass the receipt
to workflow affected-tests plan --build-impact-restore-receipt: ETP fails closed when a
restored receipt's commit is not the resolved plan base or its outputRoot is not
--build-impact-root, because planning from the requested base with the ancestor's
compiler facts would drop every change between the two from the diff. Planning from the
ancestor widens the diff instead: those changes count as changed, so the selection is a
superset of the exact-base selection. The plan adds build-impact-baseline-ancestor to
its reason codes and a Compiler baseline section to plan/summary.md.
When a plan has no --build-impact-root, it records build-impact-baseline-unavailable
in its reason codes, writes etp affected-tests-plan event="baseline-unavailable" with the
restore reason codes (not-supplied without a receipt), selected and inventory test
projects, selected tests and the historical duration of the selection, and renders the
same estimate in the Compiler baseline section of plan/summary.md. Neither pre-build
nor post-build compiler refinement can narrow such a plan, so the cost is shown rather
than widened silently.
Partial reuse across a build-graph change
A change that edits a .slnx, .csproj, .props or .targets file changes the graph
fingerprint, so no snapshot matches it. --allow-partial-graph-reuse (with an optional
--dotnet-command) lets build-impact-snapshot restore continue after the same-graph
exact and ancestor searches fail. ETP searches the requested commit and up to
--max-ancestor-distance first-parent ancestors, nearest first, across graph fingerprints.
An optional --green-branch main pointer can prefer an object for the same candidate
commit, but cannot skip a nearer ancestor. ETP checks the pointer's repository, branch,
tool version, scope and immutable metadata hash. For every candidate object it validates,
ETP fingerprints that commit's own tree from the object database with the
etp.graph-fingerprint.v1 rules (so --input-fingerprint and
the store keys must come from etp ci graph-fingerprint; a key from the older
build-impact-snapshot fingerprint never matches), reading each input as a clean checkout
would write it and resolving SDKs on this machine. An object must pass the normal immutable
metadata, proof and archive checks. This can reach the last green snapshot when a later
main merge changed the graph but its run failed.
Discovery lists at most 32 graph-fingerprint directories under each of the selected
package version roots, then checks their scoped commit directories for only those
candidate commits. It applies the same 24 metadata-check and three full-validation
budgets. More than 32 fingerprint
directories in a selected root excludes that root's partial listing while other roots
may still yield a certified snapshot. The receipt includes
snapshot-partial-reuse-fingerprint-scan-truncated if any selected root was skipped;
an unavailable receipt reports that reason when no candidate restores. A shallow checkout
may still restore the exact requested commit when its
tree is present, but cannot use ancestors whose first-parent lineage it cannot prove. If
that exact candidate does not restore, the receipt reports
snapshot-partial-reuse-lineage-unavailable. The bounded counts in the receipt are
partialFingerprintDirectories, partialCandidateObjects and
partialValidationAttempts; it also reports ancestorSearchMilliseconds, and a successful
ancestor restore reports ancestorDistance. Directory listing cost still depends on the
number of commit entries within those 32 fingerprints and the store's latency.
A match proves that every
difference between the snapshot's graph and the workspace's graph is a graph input file
changed between the restored commit and the head; a different effective SDK, a symbolic
link or submodule, or an input the commit does not contain leaves no match. The workspace
must also be exactly its HEAD tree: its fingerprint (--input-fingerprint) has to equal the
HEAD tree's, computed the same way, or the search stops with
snapshot-partial-reuse-workspace-graph-unproven. That rules out uncommitted or untracked
graph inputs, nested repositories and changed checkout conversions, none of which a commit
diff shows. The receipt
uses schema etp.build-impact-snapshot-restore.v2, which planners that predate partial
reuse reject, and adds graphReuse="partial", snapshotInputFingerprint,
commitGraphFingerprintMilliseconds and the reason code snapshot-partial-reuse; the
restored root gets an etp-partial-graph-reuse.json marker, and a plan refuses such a root
without its partial receipt. Planners that predate partial reuse do not know that marker:
handing one only the restored root, without the receipt, is unsupported and is not
guaranteed to be rejected. Always pass the receipt with the root. A failed search ends with
snapshot-partial-reuse-not-found, snapshot-partial-reuse-graph-unchanged,
snapshot-partial-reuse-workspace-graph-unproven or
snapshot-partial-reuse-commit-graph-unavailable; bounded discovery can instead report
snapshot-partial-reuse-lineage-unavailable, snapshot-partial-reuse-validation-limit or
snapshot-partial-reuse-fingerprint-scan-truncated. Without the option nothing changes.
build-impact-snapshot publication publish --green-branch main advances the stable
pointer under green-branches/main.json only with --affected-tests-run-root and
--require-complete-workspace. ETP revalidates the run's terminal green evidence,
exact tested checkout and complete snapshot before publishing the object and atomically
advancing the branch pointer. It recomputes the checkout's graph fingerprint and rejects
a mismatched --input-fingerprint; --dotnet-command selects the same SDK resolver used
for restore. The default GitHub Actions verifier additionally requires
GITHUB_EVENT_NAME=push, matching repository, branch ref, commit SHA and run ID. It
checks that origin identifies that repository, fetches the branch, and proves that the
tested commit remains on its first-parent history. The CLI also accepts
--green-promotion-provider gitlab-ci, which verifies a GitLab push pipeline using
GITLAB_CI, CI_PIPELINE_SOURCE, CI_PROJECT_PATH, CI_COMMIT_BRANCH,
CI_COMMIT_REF_NAME, CI_COMMIT_SHA, CI_PIPELINE_ID and CI_SERVER_URL with the
same remote-branch check. PR, merge-request and manual runs cannot promote the pointer.
Other CI hosts can implement IBuildImpactSnapshotGreenPromotionVerifier and call the
public BuildImpactSnapshotPublisher.PublishAsync(request, verifier, token) overload.
An older run cannot replace a newer first-parent commit;
an unrelated lineage is rejected. Other branches use their own paths, for example
green-branches/release/stable.json. The ordinary fingerprint-local latest.txt and
immutable snapshot objects continue to work without this opt-in.
workflow affected-tests plan then decides per project, in the baseline-partial-reuse
phase, before anything reads the owned baseline copy. Projects are keyed by their
workspace-relative project path, not by prepare's CacheKey. From
git diff --name-status --no-renames between the restored commit and the head, limited to
graph inputs:
- An added or modified project file of the head graph invalidates that project
(
project-added,project-file-changed). - Every project that references an invalidated project, transitively, is invalidated
(
dependency-invalidated:<reference>). Prepare's reference edges come from an evaluation without the build's properties, so a reference that exists only under, say,Configuration=Releaseis missing there. Whenever some project is invalidated this way, the head graph is evaluated again as a normal build with the plan's build evaluation properties (--msbuild-propertyvalues and the configuration), and invalidation follows the union of both edge sets. It also follows the compiler references that the snapshot's own build recorded in each retained carrier: a project whose compilation received the assembly of an invalidated project, known by the assembly names its carriers recorded, is invalidated too (compiler-reference-invalidated:<reference>; received outputs match with or without their extension). Without a readable carrier, the name the snapshot build produced is unknown (build-injected properties, global-property variants, collisions), so every project that received any project assembly is invalidated with it (compiler-reference-ambiguous:<reference>). A carrier whose payload is incomplete may have dropped its reference list, so it counts as having received every invalidated project (compiler-references-incomplete:<reference>; reportreferenceClosure="build-properties+compiler-references"). Those references are the compiler's real inputs, so they include project references that targets add while building, such as the SDK'sIncludeTransitiveProjectReferences, which no evaluation shows. Dependencies expressed throughMSBuildtask calls that produce no compile reference (in EAC,AfterTargets="Build"manifest and localization steps) do not change compiler output and are outside compiler evidence, as they already are for same-graph reuse. AProjectReferenceorImportwhose condition or path reads a property that only ETP's build injects (output layout, artifacts paths, compiler server,EternetTestPlanner*), aProjectReferenceadded inside a target, or a failed evaluation invalidates every project (reference-closure-unproven:<file>); properties assigned from injected ones, directly or through other properties in the scanned files, count as injected. A restored partial root carries a marker until the workflow prunes its owned copy, and every build-impact reader (ci plan, publication, overlays) rejects a marked root. - A solution edit invalidates only projects whose own
Projectentry was added, removed or changed (solution-entry-changed:<solution>), and every member when solution-level configurations change; folders and solution items change nothing. The workflow never evaluates or builds through a repository solution, but lane producers may. A solution that cannot be read invalidates every project (solution-entries-unproven:<solution>). Whether a solution change selects tests is still the planner's decision. - Anything else invalidates every project: a removed or renamed graph input
(
graph-input-removed:<path>),global.json,NuGet.Config, lane or input-exception files (graph-input-changed:<path>), any added or modified.propsor.targets(graph-import-changed:<path>; prepare evaluates without the build's properties, so its import lists cannot bound what conditional imports reach in the real build), a project file outside the head graph (graph-input-unattributed:<path>), any.gitattributeschange, which decides the checkout bytes of unchanged graph blobs (checkout-attributes-changed:<path>), any uncommitted, untracked, assume-unchanged or skip-worktree graph input or.gitattributes, or untracked nested repository, in the workspace (working-tree-graph-input-changed:<path>), a diff that cannot be read, or carriers outside every per-project directory.
Carriers of invalidated projects, of projects outside the head graph, and of layout
directories whose build receipts do not name one project are deleted from the owned copy
and its manifest is rewritten, so every later stage sees them as absent: absent evidence
is already built and planned conservatively. Retained carriers still pass the usual
per-carrier validation. The plan records snapshot-partial-reuse, writes
plan/partial-graph-reuse.json with reused and invalidated projects and their reasons,
and adds a Partial compiler baseline reuse section to plan/summary.md. When no
carrier remains, the baseline is dropped and the plan records
snapshot-partial-reuse-invalidated with build-impact-baseline-unavailable. A partial
baseline cannot seed a checkpoint overlay or parent checkpoint, and its workflow state
uses schema etp.affected-tests-workflow-state.v5-partial-graph, which older builds reject.
The build reports
baseline="owned-full-v3-partial" and uses the baseline for pre-build and post-build
refinement, but snapshot completion and the snapshot and checkpoint publication inputs
never include it, so certified evidence is always built under the head's own graph.
For a first-green same-PR checkpoint, the native publication form is:
etp build-impact-delta publication prepare `
--workspace . --affected-tests-run-root C:/EtpWork/run `
--publication-root C:/EtpWork/publication `
--repository owner/repository --pull-request-number 123 `
--head-sha <tested-head> --fallback-target-sha <merge-base> `
--graph-fingerprint <64-hex-graph-fingerprint> `
--candidate-root C:/EtpWork/publication/checkpoint-candidate `
--pointer-output C:/EtpWork/publication/checkpoint-pointer.json
This consumes the native original plan, build-recorded carrier roots (including
predecessors), and actual restore properties. It verifies the exact clean checkout,
terminal hashes, every build exit code, and exact partition/fragment execution
coverage before creating build/test attestations. Those small JSON receipts bind
the original terminal hash; receipts/native-workflow.json is retained and checked
with the candidate. CI must not generate substitute green receipts or inspect
native payloads to reconstruct these inputs. The plan stays canonical MessagePack.
This form creates a current-head full-v3 seed (baseSnapshotSha == headSha), not
a delta against an inferred parent. Do not combine it with explicit plan, prepare,
carrier, execution, MSBuild or parent inputs. Parent-chain publication still needs
a recorded planning checkpoint context and compiler delta lifecycle; native runs
without that context reject parent overrides rather than silently rebasing them.
A no-op without executed build/tests cannot mint execution evidence. Full seed
coverage remains mandatory; a green run without required carriers is rejected.
Checkpoint carrier requirements use the same test-execution dependency closure as
complete snapshots. A global build input can mark unrelated reverse consumers
(for example benchmarks or tools) affected without making their carriers part of
that closure. If a test references such a consumer, it becomes required normally;
unknown affected projects remain required rather than being assumed unrelated.
Every required target framework must have usable compiler evidence, and first-green
candidate tests require complete inventories. This changes no checkpoint schema or
stored object identity and does not permit publication with missing required evidence.
Publish the matching full snapshot through native snapshot publication before
advancing the checkpoint pointer. The host still owns locking, CAS and transport.
Candidate/pointer outputs must remain below the workspace and outside the input
run, evidence and object-store paths.
Publication I/O runs with bounded parallelism (ETP_PUBLISH_PARALLELISM, default
min(processors, 8)): candidate files are hashed from the bytes as written,
receipts and fragments are read concurrently, and the tool-root fingerprint hashes
files in parallel. Setting ETP_TOOL_FINGERPRINT_CACHE=default (or a directory
path) additionally caches per-file digests of the tool installation under
%LOCALAPPDATA%\etp\tool-fingerprints, keyed by relative path, length and
last-write time, so a re-extracted dotnet tool install only re-hashes files whose
metadata changed. The fingerprint value is byte-identical to the sequential
computation; the cache is opt-in, never written inside the tool root, and any
unreadable cache is ignored and rewritten. Snapshot publication streams each object
through a single hash pass, verifies the copy by read-back and still commits with one
directory move.
Pass --require-complete-workspace when incomplete
coverage must reject publication. Without that flag, a partial but valid
snapshot can be prepared for scopes such as pull-request-<number>; its receipt
and proof report completeWorkspace: false and pointerEligible: false.
A create candidate contains exactly
impact-index.zip, snapshot-metadata.json, and
snapshot-publication-proof.json. The ZIP is byte deterministic; v3 metadata
retains the existing restore contract and binds both archive and uncompressed
content SHA-256 values. The proof binds the metadata bytes and the complete
carrier-coverage identity. An identical existing object is accepted only when
all three artifacts validate and match. The optional existing pointer is never
modified; its SHA-256 is returned as existingPointerSha256 for the host's
compare-and-swap.
Pointer eligibility is a coverage fact, not a scope or event policy:
pointerEligible is true exactly when workspace coverage is complete. The host
owns branch, event, and scope policy. --pointer-output independently expresses
that the host wants ETP to materialize a local candidate; when it is omitted,
pointerPath is null even for an eligible snapshot. For incomplete snapshots,
no pointer is written even if the option is supplied. The host must require
pointerEligible: true before attempting a pointer update.
The host remains responsible only for destination paths, the store lock/CAS, atomic copy of the immutable object, and atomic replacement of the pointer. It must not rebuild the archive, metadata, proof, or publication decision.
Why Dogfooding Matters
Dogfooding is the main way etp gets better. The hard cases do not show up in
abstract examples; they show up when Eternet repositories use real Mediator
pipelines, source generators, analyzer-driven conventions, CLI surfaces,
contracts, shared test fixtures, generated files, and workflow-specific test
commands.
Every dogfood run gives us concrete evidence:
- Which diffs narrowed correctly to method or class scope.
- Which diffs widened to project scope and why.
- Which fallback reasons are acceptable safety behavior.
- Which fallback reasons point to a missing Eternet pattern.
- Whether the emitted MTP fragments run in the target CI environment.
- Whether audit evidence from broad TRX or coverage results confirms the selected subset.
P3 Mediator dogfood showed that generated-step, pure-helper, and validator changes can select one direct method plus one generated test-base anchored class instead of falling back to the whole candidate test project. Repeated fallbacks should follow the same loop as the CLI structural-contract selector: record the case, classify whether the broader selection is still the correct safety behavior, and promote only stable Eternet-owned patterns into deterministic extractors.
That feedback loop is more valuable than trying to design a universal planner
upfront. When a fallback repeats, we can decide whether it is a real risk that
should stay broad or a recognizable pattern that deserves deterministic support.
The CLI structural-contract selector was added exactly that way: a real EAC diff
changed ContractsCommandModule, tests exercised contracts scaffold, and the
old semantic-only graph could not connect those facts. The fix was not a
one-off special case; it became a reusable structural-contract pattern.
The expected workflow is:
- Run
etpin shadow mode on real Eternet changes. - Compare the manifest with the tests humans would have run.
- Use
etp auditand broader suites to catch misses before enforcement. - Promote repeated dogfood findings into deterministic pattern extractors.
- Keep broad safety nets for release, nightly, and high-risk changes.
This keeps the tool honest. It improves because our repos expose real failure modes, not because it claims to understand every possible software system.
CI planning receipts
Native affected-tests workflow
etp workflow affected-tests is the reusable orchestration boundary shared by
local runs and CI. It owns the semantic stages instead of requiring a consumer
PowerShell workflow to reconstruct them:
etp workflow affected-tests plan --workspace . --base-ref origin/main --head-ref HEAD --github-output $GITHUB_OUTPUT
etp workflow affected-tests build --workspace . --run-root artifacts/affected-run --github-output $GITHUB_OUTPUT
etp workflow affected-tests test --workspace . --run-root artifacts/affected-run
etp workflow affected-tests finalize --workspace . --run-root artifacts/affected-run --summary
Planning can bind runner partition capacity with --partition-settings <json> --execution-slot-count <positive-int> and, for a full-suite duration reseed,
--whole-project-fragments. ETP writes the adjusted settings into the run's
owned inputs without changing the source file. To consume an immutable shared
duration baseline within the same planning process, use
--duration-baseline-store-root <directory> --duration-baseline-repository <owner/repo>
instead of --test-duration-baseline <file>. ETP verifies the store pointer,
manifest, payload length and SHA-256 before copying the baseline into the run.
An unavailable store leaves planning without that advisory input; the phase
receipt records the reason and whether a verified snapshot is stale.
run executes the same four stages in-process. test --partition N is the
matrix form; omitting --partition executes every partition with bounded
parallelism. Every stage consumes the portable
etp.affected-tests-workflow-state.v3 under the run root. Generated artifact paths
are relative to the run root so platform adapters can move that directory between
runners. Planning captures its supplied compiler baseline under
inputs/build-impact-base, with a single inputs/build-impact-base.mpack integrity
inventory bound by hash in the small state receipt. When an overlay is supplied, ETP
first verifies its original authority, then rebases it to the owned baseline before
planning. A current-schema fingerprint does not cover the base index root, so the rebase
only moves that location; a legacy overlay is certified once in the current schema. The
overlay is read, verified and written once, and decoding keeps one instance of each
repeated string. A build verifies its consumed parent once for every current-overlay
materialization it feeds. The overlay, its
prepare identity, and its certified parent manifest are copied under inputs/ and
bound by path, size, and SHA-256. Build, finalize, and checkpoint publication recheck
those bindings. The original inputs are not needed on the next runner. No JSON
duplicate of the baseline inventory is written. Transport the run's inputs once,
using the platform's ZIP envelope; do not copy the original baseline separately.
finalize writes the complete machine-readable receipt to the run root and emits
JSON by default. Use --summary in CI to emit the compact Markdown summary instead;
failed runs include reason codes, failed test names, bounded assertion messages, and
TRX evidence paths while retaining the full artifacts for follow-up diagnosis.
Capturing bytes is not a completeness or trust promotion: compiler evidence is still
validated against the current workspace.
Missing, additional, changed or linked files fail instead of reverting to source planning.
Link rejection is scoped to the declared source/run roots and their contents, not
platform ancestors: a runner may mount its RAM-disk through a parent junction.
When required tests do not certify the run, finalize states why, because each cause
needs a different next action:
| Reason code | Meaning | Next action |
|---|---|---|
affected-tests-test-execution-not-started |
The build succeeded, but the current build attempt has no test execution evidence: no partition started. | Fix the first CI error before the test step (for example, environment provisioning), then run the workflow again. |
affected-tests-partition-coverage-incomplete |
Test execution started, but some partitions have no terminal result or were cancelled (for example, a timeout). With a failed build, it keeps its previous meaning next to affected-tests-build-incomplete. |
Find the interruption, then build, test and finalize again. |
affected-tests-partition-failed |
At least one partition ran and failed. | Fix the failing tests or confirm they are transient, then build, test and finalize again. |
Finalization closes the build attempt: the test stage rejects a finalized state, so the recovery is a new attempt that executes every partition, including missing ones.
A failed and an incomplete partition can be reported together. Test and finalization
diagnoses add a Partition coverage line with the succeeded, failed and missing
partitions. It is a diagnostic view: the receipt's CompletedPartitions still counts
only successful execution receipts. An incomplete build is diagnosed before the tests
it prevented. CI can pass --upstream-failure "<step name>" to name the step that
failed between build and test; when tests never started, the diagnosis names it and the
receipt records it as UpstreamFailure. Any other outcome ignores it. It is caller-supplied
context only: it never changes the status or the reason codes, and a blank value is ignored.
The workflow resolves refs to immutable commits and requires the requested
head to equal the checked-out HEAD before it can mint execution evidence.
Build and test recheck that invariant, and the terminal
etp.affected-tests-workflow.v2 receipt records the tested commit, tree SHA,
whether the workspace was dirty at planning time, artifact sizes, and SHA-256
hashes. A trusted publisher can therefore reject dirty local evidence while
using the same command surface as CI. Eventual fields such as a live pull
request merge_commit_sha are not execution authority.
When post-build refinement adds test projects or required build roots, the workflow builds those additional roots and their dependency closure before binding prebuilt modules. Supplemental builds reuse the selected build's output root, configuration, MSBuild properties and private compiler server, with separate graph/log/receipt evidence. New bootstrap roots run first in the bootstrap output root. The full refined selection is retained even if a supplemental build fails; failure prevents test execution and green publication. Supplemental receipts participate in terminal artifact hashes, build timings and checkpoint provenance rather than replacing the initial build receipt.
Timing is measured from native plan entry to the observation immediately before
finalize writes its receipt. StartedAtUtc, CompletedAtUtc, and
ElapsedMilliseconds include preparation, waits between commands, retries, and
inter-job transport within that interval; they exclude earlier runner queue/setup
and later receipt publication. Cross-runner measurements require synchronized UTC
clocks; a backwards interval fails instead of silently reporting zero. Finalizing
again is a new observation, not an immutable first-completion timestamp.
PlanningMilliseconds now measures the whole preparation/planning invocation with
a monotonic stopwatch. BuildMilliseconds sums current sequential build receipts;
TestMilliseconds is the longest current test execution receipt, not the elapsed
matrix interval. Their RecordedStageMilliseconds sum is diagnostic, never total
walltime or a measured critical path. summary.md makes these boundaries explicit.
Use GitHub's workflow timestamps separately for total CI time.
The additive ExecutionMetrics object in terminal receipt v2 and its Markdown summary
use the effective post-build selection/partitions, not the initial plan. They distinguish
semantic method/class/project scopes from whole-project commands introduced by economic
coalescing. Build-root counts are per materialized stage, not a distinct union; unknown
provided-graph counts remain unavailable. Execution attempts include retries; attempts
with command evidence count the detailed command records, not descendant processes or
exceptional launches without a recorded command. Maximum observed concurrency is per receipt, not an inferred
global matrix peak. Weight accounting exposes historical milliseconds, explicit default
weights, startup milliseconds and the accounting/component/fragment/partition totals;
generic default weights are not relabeled as milliseconds. Missing evidence remains null
(unavailable in the summary). These observations do not change selection or seal authority.
Older terminal v2 receipts without the additive object carry no execution-metrics evidence.
workflow affected-tests plan prints etp affected-tests-plan event="phase-end" lines
as ref-resolution, input-capture, prepare, test-inventory, plan-resolution,
partitioning, plan-publication, and state-persistence complete. Prepare reports
cacheHit/fastCacheHit and its changedFilesMilliseconds, projectGraphMilliseconds,
ownershipMilliseconds (including any BASE source-ownership proof) and cacheKeyMilliseconds
sub-steps (zero on a fast-cache hit); a partial restore receipt adds baseline-partial-reuse after
prepare, with reusedProjects, invalidatedProjects, graphInputChanges,
retainedCarriers and removedCarriers; inventory
reports projects, probed, cacheHits, cacheMisses, and parallelism. Planner
StageTimings appear as nested stage.<name> details on resolution/partitioning, not
additional wall time. The final event="plan-end" reports elapsed time, phase count,
recorded phase sum, selected tests, partitions, impact mode, and analysis depth. It also
reports the plan's input identity: inputsSha256 hashes declared inputs (tool version,
commits and tree, uncommitted working-tree paths and contents (including the HEAD and
contents of dirty submodules or nested repositories), request options with a
workspace-relative scope, case-normalized MSBuild property names, and workspace-relative
property values, change fingerprint,
prepare's PortableGraphFingerprint of every selection-relevant evaluated graph field with
workspace and MSBuild toolset paths made relative (fast-cached prepare manifests without it
are reevaluated), input and data-exception policies, effective budgets,
baseline/overlay/checkpoint manifests, and the captured test inventory, duration baseline,
and partition settings the planner reads). The working-tree identity excludes untracked
planner outputs (run root, .etp, cache, summary and GitHub output files; never a root at
or above the workspace, and never tracked changes) and is recomputed after
planning; a workspace that changed meanwhile fails the plan. effectiveInputsSha256 adds
cacheState (per-stage cache hits, misses, and writes; prepare reports fast-hit or
evaluated; --no-cache reports planner:disabled while prepare keeps its fast cache)
and budgetOutcome
(within-budget, or the exceeded budgets, with [wall-clock] on elapsed-time budgets);
selectionSha256 hashes the ordered initial selection ids; baseline is the baseline
manifest SHA-256 or none. Absolute paths are excluded, so runners with different work
directories remain comparable. Equal effective inputs must yield equal selections; equal
inputsSha256 with different selections points to cache reuse or a budget outcome.
The same identity, with the hashed Inputs lines, is stored as EffectivePlanInputs in
workflow state and terminal receipt, and both plan/summary.md and the final
summary.md render an Effective plan inputs section.
PlanPhaseTimings is additive in workflow state and terminal receipt; finalize
renders the Plan phase timings table in summary.md. Old state without this field
still finalizes, showing unavailable timings. State persistence excludes the final
timing snapshot write. workflow affected-tests run sends phase events to stderr,
leaving stdout as receipt JSON.
The final summary.md formats elapsed times as milliseconds below one second,
seconds below one minute, and minutes/hours for longer stages. The receipt and
structured console events retain exact millisecond values for automation. The
build table separates MSBuild time from ETP overhead; overhead is execution cost,
not a saving. When a canonical duration baseline is supplied, finalization also
compares its cumulative historical test durations with the effective selection
after build refinement. It reports estimated test work avoided only when every
uncoalesced method scope has matching owner-specific case-duration evidence, all
historical observations belong to tests in the complete current inventory, and
each scope maps to one execution variant. Class/project scopes, coalesced scopes,
multi-target commands, stale or unmatched observations, failed runs, missing
baselines, and historical totals that disagree with their test observations yield
an unavailable estimate. This is cumulative test work, not measured GitHub Actions
wall time; a baseline from a complete run is needed for the historical total to
represent the full suite. Inspect the same baseline with
etp durations summarize --input <path>.
When no test inventory is supplied, project identity probes run concurrently (default
clamp(Environment.ProcessorCount, 1, 8)). Set ETP_INVENTORY_PROBE_PARALLELISM=1
for sequential evaluation, or a positive integer (capped at 64) to override concurrency;
invalid values use the default. Results retain deterministic project order. Per-project
MessagePack cache entries in <cache-directory>/test-inventory are keyed by what selects
the evaluation before it runs: the tool version, exact probe arguments (configuration and
full project path), the SDK that dotnet --version resolves in the project directory plus
the nearest global.json, the SDK-resolution environment variables shared with prepare's
fast cache (PATH, DOTNET_ROOT*, MSBuild*) plus the MSBUILD_EXE_PATH restored for
child probes, the presence and content of every NuGet configuration file from the project
directory up to the root, and the workspace-relative
imports (with content hashes) that prepare's evaluation found, so files a wildcard import
newly matches invalidate once prepare is regenerated. A cache miss runs a single
MSBuild evaluation: -getProperty returns the identity while a binary logger's
ProjectImports=ZipFile side archive lists every imported file, so no second -preprocess
process runs. The entry records and revalidates on lookup: the project file and every
import (including SDK/package files) with its content hash; the Directory.Build.props,
Directory.Build.targets, and Directory.Packages.props discovery locations from the
project upward, stopping below the first existing file the evaluation did not import
(MSBuild stopped at a nearer file that does not chain upward, so the configuration of an
enclosing checkout cannot churn entries, while a chaining file keeps its parents covered);
the nearest Directory.Build.rsp; and the names and contents of the files matching the SDK's
restore-output wildcards under MSBuildProjectExtensionsPath. When that path lies below (or
equals) the directory value of environment variables (such as a per-run build root), lookups
also check the same relative location below each variable's current value and miss if one is
gone. A discovery file that appears while the probe runs, or a failure of the instrumented
probe, leaves the project uncached rather than failing or pinning a stale result. Entries
deliberately ignore prepare's per-project CacheKey and environment variables outside the
set above: an identity that depends on such a variable, on a file only tested with
Exists(), or on a wildcard import outside the workspace needs --no-cache. A run-local
concurrent memo hashes each distinct full path only once across projects, including
missing-file results; subsequent evaluations start with a fresh memo.
A missing or unrecognized imports archive bypasses caching; corrupt entries are misses.
--no-cache bypasses this probe cache.
Use a stable runner-local --cache-directory to reuse prepare, analysis, and inventory
entries across CI runs; the default <work-root>/.etp/cache is otherwise lost with a
per-run work root. Concurrent jobs may share this cache: writers use unique same-directory
files and atomic replacement (with bounded Windows contention retries), and unreadable
entries are misses unless the analysis cache is explicitly required. Keep each job's
--work-root/--run-root separate. Keys include workspace identity, so different checkout
paths may share storage safely but need not achieve cache hits. Do not clean a shared
cache while jobs are using it.
When workflow affected-tests plan receives neither --work-root nor
--run-root, its run evidence and default cache use an external work root under
the user's local application data directory (etp/workspaces/<workspace-hash>).
The emitted run root remains the path to use for build, test, and finalize.
An explicit work root or run root retains its current placement and is included
in the workspace path scan if it lies inside the workspace.
The project-graph cache observes the workspace file and directory path set as
well as project and import contents. Adding, deleting, or renaming a source file
or an empty directory changes the graph key and triggers a fresh MSBuild
ProjectGraph evaluation; editing an existing source keeps the graph entry
reusable. Other planner caches have independent keys. The path-set scan includes
physical files and directories in generated and Git-ignored trees, excluding
Git metadata, the configured cache root (including all input-policy partitions)
and the proven build-output directories described in
Local shared cache and graph-cache exclusions,
and hashes names rather than source contents. It needs Git only to prove those
exclusions; without proof nothing is excluded. An unreadable path or linked file/directory bypasses the optional cache.
Path comparisons follow Windows case-insensitive and Unix ordinal semantics.
If the configured cache root equals or contains the workspace, optional graph
caching is bypassed; --cache-required reports an invalid cache configuration.
Safe partial graph reuse would require per-evaluation-context identity
(effective globals, target framework, and SDK), observations of glob and
missing-path conditions, unchanged reference/import topology, and differential
proof against a fresh graph. The current cache uses full reevaluation when
source membership changes.
On an EAC checkout with about 11,500 tracked files, the path scan measured
about 125 ms warm; a local tree with extensive build artifacts measured about
3.8 seconds warm. Clean CI checkouts are the intended operating profile.
For a renamed runtime source, the current graph cannot prove every former
owner, including linked or shared source inputs. ETP retains all projects for
that tombstoned path until base-revision ownership evidence is available.
For deleted or renamed sources, ETP can inspect the exact BASE commit in a
temporary detached Git worktree. It fills the worktree from a byte-verified Git
archive, initializes the index to the BASE tree, and requires a clean tracked
status and exact tracked blob hashes before evaluating MSBuild ownership and
former project consumers. Normal-build restore repeats those checks afterward.
The temporary Git registration is removed after the attempt. A project that reads
files inside .git cannot use this proof because linked worktrees represent
.git differently; the manifest records base-git-metadata-layout-unproven.
Evaluation-time source items, project references, imports and file-existence
conditions must also be relocatable into that worktree. Paths that may still
read the original workspace, including indirect or inactive MSBuild property
expressions, retain the bound with base-nonrelocatable-input-evaluation.
When normal build properties are declared, ETP requires restored NuGet imports
and assets for both BASE and HEAD, and restores BASE only within its isolated
staging budget. Missing imports, unsupported project types, restore outputs
outside staging, incomplete target-framework evaluation, or target-time inputs
retain the conservative bound with a BaseSourceOwnershipDiagnostics reason.
ETP also retains the bound when evaluated items or imports can depend on
build-stage properties that the proof did not reproduce. This includes
ordinary SDK restore and output-layout imports when the exact build-stage
context is unavailable. When earlier restore and target-time checks pass, the
reason starts with base-build-stage-source-ownership-unproven. A valid BASE
owner alone does not override that uncertainty.
This proof is optional: a repository with legacy project types or external
build-output roots may still retain the original full project bound.
An exact BASE compiler snapshot may also carry base-ownership-inventory.v1.json.
The native workflow passes its captured BASE snapshot separately from the
current HEAD build-impact root. ETP accepts the inventory only with the
restore admission record, exact BASE commit, repository scope, matching build
configuration, a complete executable test-root and variant universe, and
complete compiler source coverage. It maps every linked source owner and every
possible local compiler-reference producer; uncertain assembly origins,
uncovered variants, missing consumers or legacy snapshots keep the existing
fallback. A BASE owner does not by itself certify current HEAD consumers;
current build evidence must also cover that side before narrowing.
Post-build refinement rechecks both admissions against the plan commits,
build configuration and prepare identity before retiring a broad source scope.
It retains scopes still reachable from the changed input, positive source
selections and scopes without complete test inventory.
Source ownership is independent of method-impact precision: a fully enumerated
and validated compiler source inventory remains useful when a dynamic call or
dispatch uncertainty prevents an exact method proof. Those method fallbacks
still retain their conservative test selection. The ownership universe covers
every executable test root and its dependencies; validated compiled projects
outside that closure can contribute additional ownership evidence. An admitted
non-test project without a carrier outside that closure is recorded explicitly
and does not establish negative ownership for initial build selection.
BaseSourceOwnershipDiagnostics records native-base-source-ownership for a
certified input and a specific native-base-ownership-* reason otherwise.
Inspect those reasons with etp inspect-replay --archive <native-replay.zip>.
When admitted BASE/HEAD inventories have different build-configuration
identities, a candidate can instead carry evaluated-base-source-ownership:
the planner evaluated the exact staged BASE tree under the requested HEAD
properties, with restore/import, target-time, omitted-project and path-safety
checks. Post-build refinement independently repeats that bounded proof only
when the candidate removed a deleted source's project/input pair. It checks
the recomputed owners and former consumers against the candidate before
retiring a broad scope. The repeat uses at most the remainder of the lesser
of 90 seconds and the request's semantic time budget; expiry or any failed
proof keeps the source scope.
The staged route is opt-in (ownershipStagedBaseProof / ETERNET_BUILD_OWNERSHIP_STAGED_BASE_PROOF,
default off; see Eternet.Build.Sessions): SDK projects rarely pass its relocation and
target-time checks, while each attempt stages and evaluates the whole BASE tree in
prepare, plan resolution and the post-build candidate replan. Disabled, each such
source records base-source-ownership-staged-proof-disabled and keeps its
conservative scope; native compiled ownership is unaffected.
Neither proof runs when a changed global build file already keeps every project
in scope (base-source-ownership-skipped-global-build-input), and a BASE tree
tracking a non-C# project fails the staged restore check
(base-restore-unsupported-project) from its Git listing, before staging.
This route does not make build outputs reusable across configuration hashes.
The HEAD inventory is captured after an unscoped full-impact build only when
prepare deletes or renames a .cs, .fs or .vb source and post-build
refinement will run; the compiled-ownership-capture build phase records why it
was skipped otherwise, or its index-load, producer and admission times. A
capture writes impact-index.compiled-ownership-attempt.json beside the
build-impact root.
This diagnostic receipt gives the BASE and
HEAD commits, variant/source/root counts, elapsed time, status and reason codes
for an unavailable HEAD inventory (including a legacy BASE without admission).
It is not an ownership admission; only the separately validated current
inventory and its evidence hashes can authorize narrowing.
An existing runtime source absent from every evaluated input also retains all
projects in scope: directory proximity cannot rule out a linked or conditional
consumer elsewhere. A source with complete evaluated owner and consumer
evidence can use the narrower bound.
Scope and limits of compiled ownership:
- Native ownership applies only to deleted or renamed indexed sources (
.cs,.fs,.vb). Other changes never read the inventories. - Proof narrows which projects are affected, not test precision: proven owners
and their reverse consumers keep whole-project bounds
(
base-source-closure-fallback), so a deletion never gets method-level selection. - Inventory admission is all-or-nothing per run: one unusable carrier, variant or evidence hash leaves the whole HEAD inventory unavailable and every deleted source on its fallback.
- Repository files that tests read at runtime without a declared consumer are
not covered by compiler ownership (
data-input-no-known-consumer).
Local shared cache and graph-cache exclusions
The local developer loop (several worktrees or clones of one repository on one machine) can reuse planner analysis across workspaces, and keeps the project-graph key stable while builds write into the tree. None of this changes the selection: a cache miss, a rejected entry or a disabled option recomputes the same result.
Shared analysis cache. --shared-cache-root <directory> (on etp plan and
etp workflow affected-tests plan/run) or the ETP_SHARED_CACHE_ROOT environment
variable names a directory outside every worktree of the repository (an error
otherwise; if Git cannot report the worktree boundaries the shared cache is
disabled instead). The option enables the persistent cache by itself; the
environment variable only adds sharing to a run whose cache is already enabled
(--cache-directory, --cache-required) and never enables it. Only two
certified, path-independent stages are shared: symbol-reference-index-project
and symbol-impact-graph. Every other stage stays in the local cache. Shared
entries are content-addressed and keyed by the repository identity (root commits
plus the normalized origin URL, never a workspace path), the tool identity,
analysis depth, input-policy partition, evaluation properties and input hashes,
so a different repository, tool build or MSBuild property set never hits. A shared
hit is returned before the local entry; a rejected or corrupt shared entry is a shared-miss (also under --cache-required, where the local
entry decides) and is replaced by the next write. Writes are atomic and
first-writer-wins; a value that contains the workspace root is refused
(shared-write-refused) and an unwritable root only loses reuse
(shared-write-failed). The workflow receipt records the resolved root, the
shared stages and the shared hits and misses. There is no eviction: prune the
directory outside ETP.
Graph-cache key v2. The project-graph key states its item-path membership
as workspace-item-paths:v2:<path-set hash>:<exclusion hash>. The hash of the
excluded directories is part of the key, so a graph cached under one exclusion set
is never reused under another. Project and import contents still identify the
evaluation rules; editing an existing source keeps the entry, touching a
.csproj/.props/.targets file or adding, deleting or renaming a path outside
the exclusions misses.
Evaluated-exclusion manifest and ETP_GRAPH_CACHE_EXCLUSIONS. Build output
under bin/ and obj/ would otherwise change the path-set hash on every build.
A miss therefore evaluates the graph, derives the directories that MSBuild
itself excludes from items (default-item excludes and the glob roots each project
actually evaluates, expanded by MSBuild), keeps only those that git proves
ignored and untracked, and stores them as a project-graph-exclusions manifest
keyed by the evaluation rules. Later runs re-verify the manifest and drop the
proven directories from the path scan; rule files observed inside an excluded
directory (for example obj/project.assets.json) stay in the key. The graph is
written only if the inventory did not change during evaluation
(graph-write-skipped:membership-changed-during-evaluation). Missing metadata,
unreadable or unproven evidence, an unexpandable include or a glob that reads an
output directory yields no exclusions. Set ETP_GRAPH_CACHE_EXCLUSIONS=off to
restore the previous full-inventory key.
Work-root cleanup. etp local gc [--dry-run] [--root <directory>] removes
empty ETP work roots: directories under the default work-root storage
(<LocalApplicationData>/etp/workspaces, and the equivalent under the temporary
directory) that contain no files at all. There is no age-based retention: a root
holding any file (run evidence, a lease, an artifact) or a linked directory is
kept, and one that a concurrent run repopulates wins. --dry-run lists what
would be removed without deleting. --root must be that storage directory, lie
below it, or be a work root with a valid .etp-local-work-root marker (drive roots, the user profile and
ancestors of the recorded workspace are refused). workflow affected-tests plan creates the run root only after the refs
resolve and removes the directories it created if it fails before writing
evidence, so failed plans do not leave empty roots.
Local incremental builds (--local). workflow affected-tests run and build accept the opt-in
--local (alias for --build-mode local-incremental). The selected build then writes into a stable,
ETP-owned root instead of a run-private one, so MSBuild skips CoreCompile for projects whose inputs did not
change; the run root keeps only evidence (about 70 MB instead of about 5 GB). The default isolated mode, its
receipts and its state are unchanged, and local evidence is never certification: green-tree evidence, checkpoint
and snapshot publication and duration export refuse it (local-incremental-not-certifiable), including copied
test/** receipts, which carry the local marker themselves.
- Root:
<work-root>/local-build/<workspace-key>/<configuration>(when--run-rootlies inside the workspace and no--work-rootis given, the work root for this default is the per-user external work-root storage instead of the workspace; an explicit--work-rootinside the workspace is still rejected), or an explicit--build-output-rootthat is empty or already ETP-owned and lies outside the workspace, the run root and the work root (in both directions; junctions, symlinks, 8.3 names and\?\/UNC spellings are resolved or rejected before any check). Roots and the.etp-local-work-rootmarker carry a magic value and the owning workspace. - Lease: one OS lock inside the root (
.etp-lease.lock) is held for the whole process.--build-lease-timeout(seconds, also ontestandfinalize) bounds the wait for a busy root and the wait printswaiting for lease held by <run> (<pid>). The split commands reserve the root for their run between processes (.etp-run-owner) untilfinalize, which releases it whatever the run's outcome. A reservation whose run is finalized, failed, deleted or whose build stage died is stale and replaced by the next run; a live one fails fast withlocal-build-root-busyand thefinalize --run-rootcommand to run.runholds the lock without a reservation of its own but still refuses a live reservation of another run. A run that selects no tests never leases. - Layout: the build stage detects EAC's
EacAffectedTestBuildRootredirect once and records it in the run state, so test and finalize never re-detect it. A redirect is aDirectory.Build.propsPropertyGroupthat assignsBaseOutputPathorBaseIntermediateOutputPatha value referencing$(EacAffectedTestBuildRoot)(literal imports below the workspace are followed); defining or merely reading the property is not one, and the ArtifactsPath layout applies instead. Module binding looks only into the selected project's own output directory and only for projects of the built graph; anything else fails closed. A persistent root keeps outputs of renamed or removed projects, so when binding reportslocal-module-not-in-built-graphorfound 0, evict the root withetp local gc --max-bytes 0(default roots) or delete the explicit--build-output-root, then build again. - Retention:
--keep-runs N(default 3 forrun --local) removes older sibling runs after a green run, but only runs whose own typed state proves that they are finalized runs of exactly that directory, without.keep, links or nested ETP roots. Eligible runs are filtered first, then the newest N are kept; failures are warnings. - Budget:
etp local gc [--max-bytes N] [--dry-run]evicts least-recently-used stable roots under<work-root>/local-build/<key>/<configuration>(exact depth, never through a link) until the work root fits (default 16 GiB). Roots that are leased, reserved by a live run, contain links or nested ETP roots are skipped.--dry-runwrites nothing and lists exactly what apply would delete; failed evictions are counted, reported and make the command exit 2. A root is evicted in place, never renamed: under its lease the evictor writes an.etp-evictingintent file (holding the marker's content), deletes the ownership marker first and then the rest, and only then the intent file, the lock file and the empty directory (the last two best-effort: a failure leaves an empty directory, never contents). While the intent file exists the contents are untrusted, so whoever next holds the lease (a build, or the next collection) finishes the removal and treats the root as new; a root with nothing but its lock file is gone (local-build-root-missing, and the next resolution marks it again); only a non-empty unmarked directory is refused as foreign. An acquirer therefore sees the complete root, a clean new one, or the root gone, never a half-deleted tree. A root whose contents cannot be fully deleted (read-only or locked file) is reported as failed, its remaining bytes stay in the before/after totals, the command exits 2 and the next collection finds it again through its intent file. Explicit roots are caller-owned and never collected. The leased root is laid out by project path (bin/<sha256 of the full project path>, via anetp-local-layout.propsimport, the layout CI shared builds use) so two projects with the same file name never share output directories. - Rejected at parse time:
--localwith an explicit--build-mode isolated, with--complete-snapshotor in a CI environment (CI,GITHUB_ACTIONS,TF_BUILD);--build-output-rootor--build-lease-timeoutwithout--local; negative--keep-runsor--build-lease-timeout.--localalso rejects every caller-owned MSBuild output property (ArtifactsPath,OutputPath,BaseOutputPath,BaseIntermediateOutputPath,UseArtifactsOutput,EacAffectedTestBuildRoot, in any-p://property:spelling, alone or inA=1;B=2).
Local CI baseline import (--local-baseline). A local run can start from the compiler baseline CI already
built for the base commit. etp local import-baseline --evidence-zip <native-plan.zip> --workspace <repo> [--expected-commit <sha>] [--shared-cache-root <dir>] [--output <json>] validates and caches it;
workflow affected-tests plan|run --local --local-baseline <imported root or its local-baseline-import.json> binds it.
An imported baseline is local evidence and is never certifiable, whatever the run selected: --local-baseline
requires --local (rejected at parse time on plan and run), the state and receipt record localBaseline and the
local-baseline mode, and green-tree, checkpoint (including the no-op), snapshot and duration publication refuse them
with local-baseline-not-certifiable, also for a run that selected no tests and never built.
- Import: the ZIP is hashed, then only what its snapshot manifest lists (plus the manifest,
baseline-restore.jsonand the optionalinputs/test-durations.mpack) is extracted, in that order: the manifest is read first and its declared lengths are hard bounds (the streamed bytes must equal the declared length), with entry, byte and 100:1 compression-ratio bounds. Entry names must be plain relative paths: no:(alternate data streams, drives),?,*,",<,>,|,\, control characters, trailing dot or space, device names (NUL,COM1, ...), rooted or..segments, and no case-insensitive duplicates. Extraction goes to a staging directory that is published by rename; a concurrent importer that wins is verified and adopted. Failures (corrupt ZIP, wrong commit, bounds) exit 2 with a one-line message. - Verification: the restore receipt's commit must equal
--expected-commit(or the plan base); a receipt whose status is notrestoredis reported separately. Manifest hashes, the carrier hashes and count and the durations schema are re-derived from the bytes. The import receipt (etp.local-baseline-import.v1) records the ZIP SHA-256, the commit, an input fingerprint (the manifest hash and the carrier inventory), the durations hash andgeneratedAt, the tool version and an advisoryexternalSourcesreport. - Cache:
<work-root>/.etp/local-baselines/<tool-key>/<zip-sha256>(or under--shared-cache-root), keyed by the tool's informational version. A cache hit requires the import receipt to exist and parse, and recomputes the fingerprint, the durations hash and the ZIP identity; a missing or mismatching receipt fails with the directory to delete. The rebasedlocal-restore.jsonis derived again when it is missing, altered or names another root, so a moved cache keeps working. Imported baselines are never garbage-collected:etp local gcdoes not touch them (its budget covers stable build roots only). Delete a<zip-sha256>directory to reclaim it (about 60 MB each); a--purge-baselinesoption foretp local gcis a follow-up. - Precedence: an explicit
--build-impact-rootwins; the import is then neither validated nor recorded (alocal-baseline-unusedevent says so). Durations: an explicit--test-duration-baseline, then--duration-baseline-store-root, then the imported durations. Imported CI durations only partition and order the local run (PartitionDurationBaselinePath): the economic preflight, the selection budgets and the pre-build decision never see them, so what runs is decided as without an import. They are used only while younger than--duration-baseline-max-age-days(default 8); older ones are ignored with alocal-baseline-durations-ignoredwarning while the import stays valid. The recordedlocalBaseline.durationsUseispartition-only,stale-ignored,supersededorabsent. - External package sources: CI carriers record source files that package targets inject from the CI machine's NuGet
folder (for example
C:\NuGetFallback\packages\xunit.v3.core.mtp-v2\3.2.2\...\DefaultRunnerReporters.cs). When such a recorded path is absent locally, the reader maps<id>/<version>/<tail>(separators and..resolved logically) onto the evaluatedNuGetPackageRoot,RestorePackagesPath, theNuGetPackageRootproperty,NUGET_PACKAGESand the user-profile packages folder, trying each root (and eachpackagesfolder of the path) until one holds a file that hashes to the recorded hash; blank, relative and duplicate roots are skipped, and an unrepresentable path rejects the shard. A recorded path that exists is used as recorded and no other root is probed, so CI behavior is unchanged. A changed, missing or differently versioned package never binds. The import'sexternalSourcesuses the same policy and is only true at import time: on a machine that never restored the package the first run stays retained and later runs adopt.
workflow affected-tests build is observable from the console without downloading the
run root. As each subphase finishes it prints one structured line to stdout, in the same
key="json-string" style as the etp test-partition events:
etp affected-tests-build event="phase-end" phase="<name>" outcome="<outcome>" elapsedMilliseconds="<n>" ....
Phases include state-validation, baseline-validation, prepare-rebase, prebuild-decision,
prebuild-producer-build, prebuild-overlay-materialization, prebuild-refinement, bootstrap-build,
selected-build, overlay-materialization, snapshot-completion, post-build-refinement,
supplemental-bootstrap-build, supplemental-build, module-binding, receipt-publication
and compiler-server-shutdown. Build phases carry the
project count, maxNodeCount, exit codes, and msbuildMilliseconds (restore + build process
wall time from the stage receipt) separately from ETP's own bookkeeping. baseline-validation
splits its time into baselineIntegrityMilliseconds (re-hashing every owned baseline file
against the plan's manifest, with bounded parallelism), parentOverlayMilliseconds
(reading and re-verifying a consumed parent checkpoint overlay) and
otherValidationMilliseconds (plan receipt, data-input exceptions and input policy). A terminal
event="build-end" line reports the total elapsed time, status, exit code, baseline and
impact mode, build node count, pre/post-build refinement outcomes, whether refinement
strictly narrowed the selection, projects built per stage, effective tests and partitions.
The same timings are persisted additively in the workflow state (BuildPhaseTimings,
BuildElapsedMilliseconds) and copied into the terminal receipt (BuildPhaseTimings,
BuildPhaseElapsedMilliseconds), which finalize renders as the Build phase timings
table in summary.md. Compiler-server shutdown happens after the state is persisted, so it
appears only on the console. workflow affected-tests run writes the same events to
stderr because its stdout carries the receipt JSON.
Every plan, build and test phase also writes
etp affected-tests-{plan|build|test} event="phase-start" phase="<name>" startedAtUtc="<ISO-8601>"
when it begins, before its phase-end, so a consumer can report which phase is running and
for how long. workflow affected-tests test (and the test stage of run) reports the
partition execution as two phases: preparation (the prerequisite build of the test
modules) and execution. While execution runs, it writes
event="phase-progress" phase="execution" completedFragments="<n>" failedFragments="<n>" totalFragments="<n>" elapsedMilliseconds="<n>"
at most once per phaseProgressIntervalSeconds (build-session policy, default 15 s,
ETERNET_BUILD_PHASE_PROGRESS_INTERVAL_SECONDS), and its phase-end carries the final counts.
A fragment is counted once, by its first attempt; a retry does not count it again.
post-build-refinement writes phase-progress lines with a step as each refinement step
completes. The schema is additive: consumers that read only phase-end lines are unaffected.
Successful output remains limited to those phase lines and the terminal summary. When a
build or partition fails, ETP additionally prints a compact failure diagnosis with the
phase, command/project, exit code, retryability, evidence paths, and next action. Detailed
process output remains in receipts and logs. An isolated MSB4181 is reported as
inconclusive because it does not identify the underlying task error; use the referenced
binlog to find the first actionable diagnostic before retrying.
Post-build refinement diagnostics and plan reuse
When compiler refinement retains a production project's broad scope because its
evidence is incomplete, DiagnosticEvidence.ProductionProjects[].CompilerEvidence
records the observed semantic, test-inventory and Razor-structure completeness.
RazorInputs identifies each input and its concrete failure reason. These facts
come from the index already read by the planner; diagnostics do not evaluate or
read the project again. Receipts that lack this evidence keep the existing
unverified status, including when their prepare identity does not match.
Checksum-bound Razor directive imports containing only C# @using directives
do not require a component type structure. The shared source proof validates
the exact input hash and parses the directives with Roslyn; code, markup and
unsupported directives keep the conservative fallback. New origin sidecars
mark those inputs with InputRole: "directive-import"; compatible older evidence
can establish the same fact from the checksum-verified checkout. Compiler
support methods such as Execute do not by themselves make an import a component.
An ordinary incomplete component still blocks narrowing. Changing an import
retains the owning project's conservative impact when its effect on components
cannot be proved from the index.
Affected builds validate selected, required and bootstrap project paths against the analyzed workspace before invoking their build stages. Missing mandatory projects fail with the stage, project path, requirement kind and recorded partition-settings path. Mandatory roots are never silently omitted. This also applies to supplemental roots introduced by refinement.
post-build-refinement now includes additive stage.<name>="<milliseconds>"
details on its phase-end line. These are also stored in the refinement receipt's
StageTimings, the workflow's BuildPhaseTimings[].Details, and the terminal
receipt/summary.md build-phase table. A directly requested refinement Markdown
summary includes a separate stage table. Older receipts without StageTimings
remain readable. The enclosing phase stopwatch and total wall-time boundary are unchanged.
| Stage | Meaning |
|---|---|
candidate-planner |
Entire candidate CI-plan resolution, including partitioning/publication |
candidate-plan-resolution, candidate-partitioning, candidate-plan-publication |
Candidate phase wall times |
candidate-git-diff, candidate-msbuild-graph, candidate-candidate-test-resolution |
Changed-file discovery, project graph, and candidate resolution; zero when reused |
candidate-conservative-plan-reused / candidate-fast-project-replanned |
Zero-duration marker identifying the chosen path |
build-impact-root-current, build-impact-root-predecessor-0, ... |
Root inspection wall times; predecessors numbered newest first |
build-impact-carrier-parse, build-impact-receipt-validation, build-impact-hashing, build-impact-project-evaluation, build-impact-assembly-impact-read |
Aggregate worker durations across shards/roots |
build-impact-snapshot-load / build-impact-composed-snapshot-load |
Complete portable index read, including semantic shard materialization |
build-impact-index-refine, build-impact-cost-estimate |
In-memory refinement/base-source lookup and cost estimation |
evaluator, safety-union-partitioning |
Safety evaluation and optional reconciliation/repartitioning |
receipt-write, effective-writes |
Initial decision receipt and effective selection/partition writes |
Existing planner/partition pipeline timings are propagated as well. These measurements
are nested, not additive wall time: worker totals can exceed the phase elapsed time,
and receipt validation includes artifact hashing. Reused upstream timings have a
reused-plan. prefix and describe the original plan, not work repeated after build.
The final receipt timing-snapshot write is excluded from receipt-write; it and summary/
GitHub-output publication remain inside the enclosing phase. New diagnostic timings
do not enter economic cost calculations or the existing refinement decision hash.
Existing planner telemetry and its derived cost estimates remain observational.
Post-build refinement can feed the validated conservative selection directly into the compiler-index refiner, avoiding git discovery/project graph/candidate resolution. Reuse requires an unscoped FastProject manifest, matching prepare base/changed paths, unchanged input-policy hash, and no overlay or forced-full/compiler-refined selection. Input policy is reread before reuse; workspace/request checks, carrier/receipt validation, external inventory reconciliation, incomplete-inventory retention, safety-union evaluation, and forced-full rejection are still applied.
Replanning remains necessary for forced-full manifests (which lack changed-file/ownership
evidence), semantic/scoped or already compiler-refined plans (not the original broad
FastProject closure), overlays (which may use a different evidence base), or mismatched
prepare/policy evidence. Workflow state now preserves AnalysisCacheDirectory from
planning and passes it to pre/post-build refinement, so these paths can use the same
persistent analysis cache. --no-cache stores no directory; older state without the
field keeps the previous no-directory behavior. The shared inspection cache below is
independent of this persistent cache.
Current and predecessor roots are inspected concurrently (up to four roots), while
sharing a worker budget capped at min(Environment.ProcessorCount, 8). Composition
still uses current, newest predecessor, then older predecessors, regardless of completion
order. Invalid current evidence still masks older evidence for the entire project.
Each worker retains its own MSBuild ProjectCollection; project evaluations are not shared.
One read-local concurrent cache shares SHA/MVID values across shards and roots, keyed by
normalized full path, algorithm/identity kind, file length, and last-write ticks. Concurrent
requests compute each version once. Metadata is checked again after reading: a detected
concurrent file change fails validation rather than publishing a stale cached value.
Missing/failed reads do not poison subsequent lookups, and nothing survives the read.
All existing source, build-input, carrier and assembly-impact hashes remain validated.
For a reproducible local comparison from the repository root:
dotnet build tools\Eternet.TestPlanner\tests\Eternet.TestPlanner.Tests\Eternet.TestPlanner.Tests.csproj -c Release --no-restore
dotnet test tools\Eternet.TestPlanner\tests\Eternet.TestPlanner.Tests\Eternet.TestPlanner.Tests.csproj -c Release --no-build -- --filter-method '*Warm_candidate_path_benchmark*' --report-trx --results-directory tools\Eternet.TestPlanner\tests\Eternet.TestPlanner.Tests\bin\performance-results
The test records samples in TRX standard output: one SDK test-project fixture with an
accepted synthetic portable carrier, one effective test and command fragment,
one warmup per path, and six alternating pairs
with no persistent cache. Both paths use the same reader and partitioner, isolating
the cost of replanning versus conservative-plan reuse; this is not a measurement of
the full 60-project CI workload or an estimate of the reported 42-second phase.
A local Windows run on 2026-09-18 measured means of 383.78 ms replanning versus
232.91 ms reusing (six samples each). Replan samples were
368.89, 409.76, 432.48, 370.46, 347.83, 373.26 ms; reuse samples were
193.44, 176.67, 316.41, 253.82, 244.62, 212.48 ms. These are fixture observations,
not performance thresholds asserted by the tests.
Migration: v2 replaces the misleading native workflow CriticalPathMilliseconds
field with RecordedStageMilliseconds and mandatory timing evidence. Old v1 run
states must be replanned in a fresh run root; the CLI does not fabricate timestamps
or alias the old field. Historical build/partition receipts keep their own schemas.
State v3 additionally requires an owned baseline and integrity binding. Old v2 states
must also be replanned; absolute baseline paths are not silently rebound to another
runner or migrated by guessing which snapshot was used. The terminal timing receipt
remains v2. Native parent consumption requires a freshly materialized
etp.build-impact-ephemeral-overlay.v3 (or v2 during the fingerprint transition); v1
ephemeral files contain no source fragments and must be materialized again rather than
relabeled or widened through a legacy fallback.
Build materializes the selected graph, uses the SDK artifacts layout for
project-isolated bin and obj, enables current compiler carriers, applies
post-build narrowing, and binds partition fragments to the exact prebuilt
modules. A ProjectReference that changes global properties (for example
AdditionalProperties="Name=Value" or GlobalPropertiesToRemove) makes MSBuild
build the referenced project, and everything below it, once more under the changed
set. Neither the artifacts layout nor the default layout puts global properties in
paths, so both configurations would write the same obj and bin files and their
concurrent compiles fail with CS2012. When ETP owns the output paths it scans the
workspace's literal ProjectReference metadata (AdditionalProperties,
Properties, the Set* forms, GlobalPropertiesToRemove and
UndefineProperties) for the property names involved. Evaluation cannot enumerate
global properties, so the names are needed. A CustomAfterMicrosoftCommonProps
import then probes which of those names each configuration receives as global
properties, with their exact values. MSBuild computes the requested projects' key
from the build's own final arguments, so ETP parses no arguments. Every configuration
with another key moves its compile outputs below <artifacts>/forks/<key>/. The
import runs after Directory.Build.props, the SDK's artifacts naming and the restore
imports, so requested configurations keep their paths (receipts, module binding and
carriers are unchanged) and a fork still reads its project's real restore outputs.
A caller that passes the same property globally (as EAC's CI does) creates no fork,
and environment values never fork. Configuration, platform, target framework, runtime
identifier, the solution-provided properties and reserved MSBuild* properties are not
watched, and names produced by expressions or targets are not seen. Caller-owned output
paths or a caller CustomAfterMicrosoftCommonProps (argument or environment) leave the
layout to the caller. The reason reference-fork-isolation records the
isolation. A project built both as requested and as a fork shares one carrier, receipt
and inputs-cache destination: the requested configuration owns it (a fork writes it
only while no receipt exists and never overwrites one). Build-output reuse stores and
materializes only requested configurations: a receipt or entry rooted below
forks/<key>/ is skipped (publish-skipped-fork-instance) or refused. A retained
forced-full plan is terminally rejected when post-build
evidence is missing or unusable unless the caller explicitly passes
--allow-retained-forced-full.
Full-carrier builds retain the validated planning snapshot for projects not rebuilt
in the current phase. Pre-build refinement combines that baseline with fresh producer
carriers; post-build refinement combines baseline, producer, and selected-build
carriers in that precedence order. Every input is revalidated in the current workspace
and restore context. A newer project's complete set of target-framework shards replaces
the older project, even when the newer evidence is unusable. Unidentifiable, foreign,
or empty supplied roots fail composition instead of resurrecting older authority.
This is in-memory composition, not snapshot publication. Planning captures the baseline
once; composition does not duplicate it for each phase.
The reason build-impact-index-predecessor-composed and timing stage
build-impact-composed-snapshot-load expose this operation. BuildImpactBaseRoot is
relative to the run root, and its manifest binding prevents accidental baseline mutation
between jobs.
For standalone ci refine-post-build, repeat --build-impact-predecessor-root <root>
in oldest-to-newest order; --build-impact-root is always the newest phase. Full-carrier
composition cannot be combined with --build-impact-overlay: delta overlays retain
their own explicit verification and lineage protocol. No selection guard is relaxed.
--work-root allows the run, copied inventory/settings, and build outputs to
live outside the checkout (for example on a runner RAM disk). The test stage
uses that same explicit run root; project inputs still belong to the workspace.
In-process MSBuild evaluation must not change the SDK of child dotnet commands:
ETP restores the caller's original SDK environment for graph materialization,
build, and test execution while retaining Locator's environment for analysis.
Repository-specific build prerequisites are declarative partition settings, not hard-coded ETP project names:
{
"buildMaxNodeCount": 8,
"enablePreBuildRefinement": true,
"buildRequirements": [
{
"projects": ["tests/Product.Tests/Product.Tests.csproj"],
"requiredProjects": ["tests/Product.Fixture/Product.Fixture.csproj"],
"bootstrapProjects": ["src/Product.BuildTasks/Product.BuildTasks.csproj"]
}
]
}
Rules can match project, projects, or projectGlob. Matching prerequisites
survive fragment coalescing. Build parallelism defaults to one and is capped by
the host processor count; bootstrap builds are serialized. Before test builds,
ETP may build affected producers and refine the selection when their dependency
closure is reusable by the conservative build. Complete duration evidence must
predict net savings; otherwise bounded structural limits apply. Missing graph
evidence or global/forced-full changes bypass this optimization. The recorded
prebuild decision and refinement receipt explain whether it narrowed anything;
enabling prebuild is not itself evidence of avoided builds.
Portable carrier inspection (build-index) and snapshot publication accept
the same etp.prepare.v2 content in .json, .mpack, or .msgpack form.
Carrier inspection reads a typed projection directly from the canonical payload;
neither command writes a JSON sidecar or changes the supplied prepare file.
Required coverage, schema and transported-workspace identity checks apply equally
to both encodings. Existing JSON prepare inputs remain supported; migrating a
workflow to MessagePack does not require weakening validation or duplicating files.
prepare, plan, selection, partition, checkpoint and overlay payloads are persisted
once as compressed MessagePack. MSBuild reads prepare and parent checkpoint
projections directly, without expanding them into a JSON bridge. plan and build
can append their small resolved refs,
selection decision, and minimal partition matrix directly to GitHub Actions'
$GITHUB_OUTPUT, including the runner-owned path outside the checkout.
finalize requires complete green partition coverage and writes
one canonical durations/etp-test-durations.mpack payload when tests executed.
It does not emit parallel large JSON copies. JSON is reserved for the small
state, manifests, and receipts. Consumers remain responsible only for checkout,
credentials, runner provisioning, transport, retention, and repository/event
policy.
Checkpoint objects contain checkpoint.mpack, addressed by the hash of its exact
binary bytes; the logical checkpoint fingerprint still uses canonical semantic
content. Owned parent checkpoints are stored as inputs/build-impact-parent-checkpoint.mpack.
Overlays use inputs/build-impact-overlay.mpack and build/current-build-impact-overlay.mpack:
their binary envelope is [tag, payload], where 0 is certified and 1 is ephemeral.
The tag selects the concrete type in one decompression pass and must agree with
its schema. Unknown tags, corrupt bytes and identity mismatches fail closed.
CLI callers may request explicit JSON for inspection; native runs never emit both
encodings. Certified prepare inputs are copied byte-for-byte in their original
explicit encoding because the overlay binds the prepare's raw hash.
Rollout requires publishing the tool and Build package together, then updating consumer checkpoint transport paths and the central version/commit pin. Existing JSON checkpoint objects are not renamed or rewritten in place: their content address would change. Automatic object lookup expects the new canonical filename; an unavailable prior object requires a fresh validated checkpoint. There is no filename probing or JSON-sidecar fallback.
Native build records test modules inside the run root as relative paths in the
canonical partition payload. Transport the entire run root to the next runner;
test resolves those modules against its current run root without a PowerShell
rebase. An explicit --module-root is authoritative for relative test-module
paths: a missing transported module fails, even if a same-named workspace module
exists. Path traversal outside that root is rejected. Custom build outputs outside
the run root retain absolute, host-bound paths and must remain accessible under
the workspace or an explicit module root; copying only the run root does not
transport those external outputs. Rebuild older native run roots to obtain the
portable relative-path manifest; no legacy path-rewriting fallback is applied.
Partition execution
Declared RequiredBuildProjects are conventional runtime payloads: ETP builds
each distinct project/configuration with implicit restore before the first
fragment that needs it, for both execution strategies. Preparation is shared
only within one execution invocation; successes and failures are reused across
its fragments, with no implicit retry or persistent cache. Conventional builds
are serialized because different roots can share obj/bin dependencies.
The shared prerequisite bundle restores every required project in one NuGet
graph, as a solution restore does, so a reference shared by several roots is
restored once even when the bundle builds roots in parallel. A root that does
not import NuGet's targets runs its own Restore target afterwards. Like the
build stage's restore of graph/selected.slnx, the graph restore does not run
a root's own Restore target, so BeforeTargets/AfterTargets="Restore" hooks
in required projects are not part of the restore contract.
Consumers must treat these prepared payloads as immutable during the invocation.
prebuilt-no-build with no requirements starts no prerequisite build, and
build-before-test leaves its own project's restore/build to dotnet test.
Partition concurrency and test-runner concurrency are independent. For serial
local measurements with xUnit v3, plan with --partition-count 1 and a
--partition-settings JSON containing "MtpArguments": ["--max-threads", "1"],
then execute with --max-parallelism 1. The runner arguments are preserved in
each emitted command fragment and execution receipt. A single partition alone
does not override the runner's thread setting. Keep these measurement settings
separate from CI settings; other runners require their own supported options.
Build-time refinement also requires the consumer to load the
Eternet.TestPlanner.Build generator and MSBuild targets. Setting
EternetTestPlannerImpactIndexEnabled=true alone does not install or import
them. If a successful build reports post-build-impact-index-missing, inspect
the consumer's package/import configuration and the run's build/impact-index
before changing selection budgets. Without those producer artifacts ETP retains
the conservative selection; this is not evidence of semantic refinement.
The Build package's EternetTestPlannerMaterializeImpactIndex target is
incremental: it declares MSBuild Inputs (generator carrier, peer generated
sources, intermediate assembly/PDB, reference assemblies, prepare and
assembly-reference manifests, EAC diagnostic paths, the task assembly and a
scalar fingerprint file Eternet.TestPlanner.ImpactIndex.inputs.cache) and
Outputs (copied carrier, peer-sources manifest, Eternet.TestPlanner.BuildReceipt.json,
Eternet.TestPlanner.AssemblyImpact.json.gz, or the delta fragment). ETP's
test-phase preparation build is therefore a true no-op for already-built
projects: the receipt and assembly-impact payload are left untouched instead of
being recomputed. The fingerprint file is rewritten only when a scalar input
(configuration, TFM, RID, SDK version, diff base, SourceRevisionId, delta
parent metadata, reference list, …) actually changes, which reruns the target
without forcing a recompile. Set EternetTestPlannerIncrementalImpactIndex=false
to force the target to run on every build; outputs are identical either way.
Empty and executable CI plans both emit the canonical etp.partitions.v1
partition schema. Consumers must preserve it rather than reconstructing the
payload. Regenerate empty receipts from older releases that emitted the retired
etp.test-selection-partition.v1 label; no schema-rewriting adapter is required.
ETP owns execution of the partition manifest it produced. The same command
supports local bounded parallel execution and a matrix runner's exact single
partition, and emits etp.partition-execution.v2 with per-fragment logs and
fail-closed aggregate status:
etp execute-test-partitions --workspace . --partition-manifest artifacts/test-partitions.mpack --max-parallelism 3
etp execute-test-partitions --workspace . --partition-manifest artifacts/test-partitions.mpack --partition 1
Every run has an isolated run/result root. MTP arguments are forwarded only
after --; path roots are validated inside the workspace/run root, and
cancellation terminates only the child process tree owned by that invocation.
Consumers may transport the receipt and logs but do not reimplement partition
scheduling, strategy resolution, or aggregate success policy.
The receipt's SchedulingMode states how slots were assigned. An exact
--partition lane uses partition-sequential: its fragments run one at a time,
exclusive-host fragments first. All-partitions execution uses fragment-pool
and adds the fragment-pool-scheduling reason code: every selected fragment
enters one ready queue ordered by exclusive-host first, then descending
EstimatedWeight, then manifest order, and --max-parallelism bounds
concurrent fragments instead of partitions. Static partition weights are only
estimates, so this drains a skewed manifest at roughly
max(longest fragment, total / slots) instead of the longest partition.
Fragments sharing an ExclusiveGroup still never overlap: a blocked group defers
the fragment and the slot takes the next eligible one. Exclusive-host fragments
keep the host gate. Per-partition results, ordinals, evidence directories, and
partition-start/partition-end events keep the sequential layout; a
partition's window spans its first queued fragment to its last completion.
Fail-fast
By default a failed fragment does not stop anything: every selected fragment runs
so a red run is still complete evidence. --max-failures N (or --fail-fast, which
means --max-failures 1) opts into stopping early on execute-test-partitions,
workflow affected-tests test and workflow affected-tests run:
etp workflow affected-tests test --workspace . --run-root artifacts/affected-run --max-failures 1
Once N fragments have failed (non-zero after MTP exit-code normalization; fragments
the stop interrupts do not count), no new fragment is dequeued and running fragments
are cancelled through the ordinary cancellation path, which kills each fragment's
entire process tree (including MTP test hosts). The receipt's FailFast block records
the threshold, failed and interrupted fragment counts, and every fragment that never
started. The execution status is failed, the reason codes add
execution-stopped-after-max-failures, interrupted fragments carry the outcome reason
stopped-after-max-failures, and a partition keeps its own failure as its status.
Partitions that never started stay absent, as for any partition that never got a slot.
A fail-fast-stop event marks the moment the threshold was reached.
The threshold is counted across every partition of one invocation. All-partitions
execution schedules every partition from one fragment pool, so a failure in one
partition stops the others. Separate --partition N processes (a CI matrix) count
independently; each stops only its own partition.
finalize reports a stopped run with affected-tests-stopped-after-max-failures and a
"Stopped early" summary line instead of affected-tests-partition-coverage-incomplete,
unless a missing partition was not requested by any invocation that stopped. Without a
stop, finalize keeps its not-started, failed and incomplete classification unchanged. A
stopped run is never
certifiable: its status is failed, and green-tree and checkpoint publication reject it
like any other failure. Without the option, receipts carry no FailFast block and
execution is unchanged.
Fragment watchdog
A hung test process otherwise holds its slot, and the whole run, until the CI job
times out. Every launched fragment is therefore watched, and the partition-settings
block fragmentWatchdog (read by workflow affected-tests test) configures it:
"fragmentWatchdog": {
"stallTimeoutMilliseconds": 600000,
"stallCpuCoreThreshold": 0.01,
"durationCeilingFactor": 3,
"durationCeilingFloorMilliseconds": 600000
}
- Stall detection is on by default (10 minutes,
0disables it). A fragment stalls when, for the whole timeout, its stdout and stderr did not grow and its whole process tree used less thanstallCpuCoreThresholdcores in every 15 s sampling interval. CPU comes from the fragment's job object (see below), so the detector is armed only on Windows withjob-object-treeevidence and disarms if a query fails: the rootdotnet testprocess idles while its test host works, so root CPU alone would terminate busy tests. MTP prints nothing while tests pass, so output alone is no signal either; CPU is. A silent fragment that keeps one core busy (a busy loop) is not a stall, and neither is an idle one that still writes output. - The duration ceiling is opt-in (
durationCeilingFactor, at least 1). A fragment is terminated aftermax(factor x EstimatedWeight, durationCeilingFloorMilliseconds)(floor default 10 minutes), only when its estimate is a complete millisecond estimate (DurationEstimateIsComplete); unitless weights get no ceiling.
A triggered watchdog terminates the fragment's whole job (descendants whose parent
already exited included) and the root's process tree. The fragment fails with exit
code 124, outcome reason fragment-stalled or fragment-timed-out, a
[etp watchdog] line in its stderr.log, and a Watchdog block with the trigger,
threshold, inactive time, last output time, the last 40 cumulative tree-CPU samples
and the last output lines. A fragment-watchdog event (carrying status="failed")
is written when it happens, the receipt adds fragment-watchdog-stalled or
fragment-watchdog-timed-out and a Watchdog block with the thresholds and counts,
and finalize prints a Watchdog: line per terminated fragment. Like any failed
fragment it fails its partition and the run, counts toward --max-failures,
contributes no duration observation and can never certify.
The stall default is deliberately far from normal behaviour: the longest healthy whole-project fragments of recent green full-suite runs took 8.2 minutes in total, and a stall needs ten minutes without a single interval above 1% of one core. A process can idle legitimately while something outside its tree works (a container runtime pulling an image, a service it waits for), but not silently for ten minutes. On a loaded workstation, starting PowerShell processes sometimes idled for a few seconds (script scanning runs outside the tree), two orders of magnitude below the default.
The execution receipt exposes StageTimings for partition-manifest-load,
prerequisite-preparation, and test-execution, plus CriticalPathMilliseconds
as their sum. Preparation completes before fragment clocks and exclusive-host
leases begin. Its duration remains in the total critical path without inflating
fragment history or test parallel-capacity metrics. The native executor emits
the preparation stage even when no prerequisite roots are needed.
Preparation builds the deduplicated union of required roots and
build-before-test targets in one MSBuild session. --build-max-node-count
controls the node budget and enables parallel builds when greater than one.
Identical shared references retain MSBuild's session cache. Configuration and project-reference property
contexts remain intact. Conventional test consumers wait for
the entire bundle to finish. The run's preparation/ directory retains the
generated project, exact command arguments, stdout, and stderr.
Parallel preparation requires distinct build contexts to have isolated outputs.
ETP preserves reference properties and does not rewrite a repository's output layout.
Before launching parallel preparation, ETP evaluates the reference graph and rejects
distinct instances of the same project with shared output directories. The diagnostic
names the project, output directory, and differing global-property names (not values).
preparation/output-validation.txt records this check's elapsed time. It does not
serialize builds or retry failures. Single-node preparation skips this extra evaluation.
Fix conflicting project contexts at their source:
use consistent reference properties for identical outputs, or distinct OutputPath
and IntermediateOutputPath values for intentionally different outputs, as described
in Microsoft's build race guidance.
Isolate restore assets with BaseIntermediateOutputPath (set before SDK imports)
when restore inputs also vary by context. --build-max-node-count 1 is an explicit
workaround for a diagnosed collision; it is not required for shared references
with identical properties. These settings do not change the run-private compiler server.
Version 2 also records the
host ProcessorCount, configured partition limit, observed peak concurrency,
UTC start/end windows for every partition and attempted fragment, and a
single-attempt timeline. A failed attempt is classified as infrastructure
only when process launch or local I/O proves it; a non-zero test-runner exit is
unknown because the runner output may represent assertions, discovery, or
infrastructure. ETP performs no implicit retries.
TotalPartitionBusyMilliseconds is the sum of partition windows.
ParallelCapacityMilliseconds is the observed test-execution wall clock
multiplied by the fragment slot count: min(MaxParallelism, RequestedPartitions.Count)
in partition-sequential mode and min(MaxParallelism, fragment count) in
fragment-pool mode. IdleParallelCapacityMilliseconds is its non-negative
remainder after slot busy time, which is the partition window sum in
partition-sequential mode and the fragment duration sum in fragment-pool
mode because pooled partitions overlap. PartitionDurationBalancePercent is
average partition duration divided by the longest partition duration (100 for
an empty/zero-duration run). PeakConcurrentPartitions counts partitions that
held at least one slot at the same time; PeakConcurrentFragments counts
fragments executing at the same time after the host gate. These values
diagnose scheduling imbalance; they do not claim CPU utilization.
These scheduling values describe the main schedule only. With --retry-failed-fragments
the retry pass runs afterwards, serially and alone on the host, as its own
fragment-retry stage: its second attempts are not in TotalPartitionBusyMilliseconds,
the slot busy time behind IdleParallelCapacityMilliseconds or
PartitionDurationBalancePercent, so busy time never exceeds the test-execution
capacity window it is measured against. The retry time is that stage's own
ElapsedMilliseconds (and part of CriticalPathMilliseconds); the per-partition and
per-fragment ElapsedMilliseconds of a retried fragment still include both attempts.
In fragment-pool mode partition windows overlap, so a low
PartitionDurationBalancePercent does not mean slots idled behind one partition.
The receipt's Makespan block (absent in partition-sequential mode) says what
bound the run, from the durations the fragments actually took:
LowerBoundMilliseconds is host-exclusive work plus the largest of the longest
fragment, the busiest exclusive group, and the remaining work spread over the
slots; BindingConstraint names which (indivisible-fragment:<selection id>,
exclusive-group:<name> or slot-capacity); ExcessOverLowerBoundMilliseconds
is the test-execution time above it, the most any other order of the same
fragments could have saved; LongestFragment names the longest process. The
execution-end event carries the same values. A long run with a small excess and
an indivisible-fragment: constraint was set by one fragment (a slow or hung test
process), not by scheduling. Replays of green six-slot full-suite runs 36265616595
and 36271755869 reproduce their pool makespans (825.8 s and 667.2 s) and take
1,857 s and 1,042 s when each partition runs its fragments in sequence.
The CLI writes one synchronized stderr line for each preparation, execution,
partition, and fragment start/end event while preserving JSON stdout. Fragment receipts retain
the exact inferred command. RunnerParallelism.EffectiveParallelism remains
null with runner-effective-parallelism-not-exposed unless a future runner
contract supplies effective internal concurrency; the outer partition peak is
not presented as internal test-runner parallelism. Fragment completion also
records root-process total/user/privileged CPU time, average cores consumed
(cpuMs / elapsedMs), host-normalized CPU percent, and peak working set. These
unprefixed fields always describe only the root dotnet test process, not the
test host it launches.
On Windows the root process also joins an accounting-only job object right
after it starts (no limits and no kill-on-close, so process lifetime and cleanup
are unchanged). The test host and every other descendant inherit the job, and
one query when the fragment ends records the whole tree: ProcessTreeCpuMilliseconds
(with its user and kernel parts), ProcessTreeAverageCoreUtilization, read,
write and total I/O bytes, ProcessTreePeakCommitBytes (the tree's peak private
commit at one instant; Windows keeps no tree-wide working-set peak) and
ProcessTreeProcessCount. ResourceEvidence is then job-object-tree. Joining
and querying take about 60 µs per fragment. Known bound: the root runs before
the join, so a descendant it starts in that window never enters the job and
nothing detects it; ResourceEvidence stays job-object-tree and the tree
totals undercount that descendant. dotnet test starts its test host hundreds
of milliseconds after it starts (a prebuilt module runs in the root itself), so
only an ETP thread stalled that long before the join, preempted or suspended
by a GC, misses it. Process.Start cannot start the root suspended, which is
what closing the window would take. Totals are cumulative up to the fragment end, so a process that outlives
the fragment, such as a build server it started, is counted only up to then.
Other platforms, and a job that cannot be created, joined or queried, keep
root-process-only evidence and record ProcessTreeUnavailableReason
(process-tree-accounting-unsupported-platform, job-object-create-failed,
job-object-assign-failed or job-object-query-failed). The fragment-end
event line carries the tree values, and the finalize summary adds tree CPU
next to root CPU, the average tree cores, total tree I/O and the largest peak
commit over the fragments with tree evidence.
Historical v1 receipts lack these required observability fields and must not be
treated as v2 evidence.
etp ci plan is the atomic orchestration boundary for the ETP planning stack. It keeps the CLI thin and composes the existing prepare/plan/partition flow in-process, then emits a versioned receipt with the selected or widened outcome and GitHub summary projection. When --build-impact-root identifies a verified compiler snapshot, CI first performs the cheap project-graph pass and then refines it from that portable index. A hit avoids the full Roslyn workspace analysis; missing, stale, or incomplete evidence remains fail-closed at the affected project scope. Incomplete candidate test shards produce a hybrid result: exact methods from trusted shards plus project fallback only for the incomplete candidates. Mixed source and resource/configuration diffs can also produce a hybrid result: declared data ownership retains each consuming test project and its downstream test candidates while independent source changes use compiler evidence. Data files without known consumers remain visible as data-input-no-known-consumer and do not block refinement. A changed .sln/.slnx compiles into no assembly. When the planner attributes it to specific projects (SolutionMemberImpact, or SolutionEntryImpact for entry-level attribution), refinement keeps the whole-project bound for those projects and their reverse-dependent test candidates (an attributed project without candidates adds no bound), and compiler evidence still decides every other candidate. When those bounds already cover every candidate, the refiner skips the declaration diff (build-impact-index-non-source-bounds-every-candidate). Prepare's graph is evaluated without the build's properties, so the refiner also checks each narrowed candidate against the ProjectReferences its carriers were evaluated with for the build: a reference path to a solution-impacted project, or through a project without an evaluated carrier, keeps the conservative selection (build-impact-index-solution-projection-unproven). A solution change the planner could not attribute (incomplete evaluation, members outside the graph, or .sln membership it cannot parse) keeps the conservative selection. Global build inputs retain the conservative selection. Only eng/etp-input-exceptions.json authorizes data triggers, including copied or embedded files. Each rule maps data paths to consuming projects. Declared exceptions take precedence over Markdown/control-plane exclusions; source and build configuration keep their own impact rules.
etp ci plan --workspace . --base-ref origin/main --head-ref HEAD --analysis-depth semantic --test-inventory artifacts/etp-test-inventory.json --build-impact-root artifacts/previous-build-impact --test-duration-baseline .etp/test-durations/etp-test-durations.mpack --planner-p95-ms 15000 --economic-process-startup-ms 1000 --economic-min-saving-ms 10000 --output artifacts/ci-plan.mpack --summary-output artifacts/ci-plan-summary.txt --github-output artifacts/ci-plan-gh-output.txt
After a conservative build has emitted current-head compiler carriers, use
etp ci refine-post-build as the atomic adoption boundary. The command reruns
the cheap compiler-index plan, proves that its candidate is a semantic subset
of the conservative selection, and writes the effective selection and
partition manifests. Project-to-class or project-to-method narrowing requires
a complete test inventory; a widened candidate, a forced-full candidate, or
incomplete evidence exits nonzero and writes a small
etp.ci-post-build-refinement.v1 receipt. An equivalent or unusable candidate
retains the conservative manifests without failing. A retained forced-full
selection fails unless the caller supplies --allow-retained-forced-full for
an explicitly authorized full-suite run.
When the candidate selects a test outside the conservative selection, ETP no longer keeps every conservative project whole. Each conservative scope the candidate does not cover is classified:
- methods and
exact/relatedclasses are positive source evidence and are kept; - a fallback project or class bound is superseded when the candidate was planned
from the same changed inputs, per-project input ownership and planner scope, the
candidate's refiner decided every test of that project from complete compiler
evidence (
CompilerDecidedTestProjects, recorded by the compiler-index refiner for candidate projects without a project fallback), the candidate's own test inventory for that project is complete, and the bound's inventory can be expanded (the proof strict narrowing requires); - any other bound is kept, with
post-build-source-scope-not-compiler-decided,post-build-source-scope-inventory-incomplete,post-build-source-scope-inventory-empty(a complete inventory without tests would prove it vacuously),post-build-source-scope-evidence-mismatch,post-build-source-scope-forced-fullorpost-build-source-scope-unsupported.
Strict narrowing and the union share one drop proof: matching planning evidence, a
compiler-decided project (for a scope with ambiguous ownership, every project that
declares its name), and, for broad scopes, the candidate's own complete, non-empty
inventory. A candidate that records no CompilerDecidedTestProjects proves nothing,
and covering a broad scope method by method only counts against the non-empty
current inventory of a compiler-decided project. Candidates reached through owners
pruned by the unindexed-source directory heuristic, consumers of an owning test
project, and candidates whose current inventory could not be built are not recorded
as decided.
If nothing is kept, the result is post-build-compiler-supersedes-source: the
candidate and its own partition are adopted without the safety-union-partitioning
replan. Otherwise the kept scopes are reconciled with the candidate as
post-build-source-compiler-union. The receipt's optional DiagnosticEvidence
lists SupersededSourceScopes and one WideningScopes entry per remaining broad
(project or non-exact class) scope: its origin (compiler-candidate or
source-plan), reasons, and up to ten causing changed inputs. A candidate project
fallback caused by a changed source without compiler evidence (deleted, or without
declarations such as global usings or assembly attributes) carries
build-impact-index-changed-source-unindexed and names that file exactly; other
causes are derived from the project's triggering ownership. The phase log adds
supersededSourceScopes, effectiveBroadScopes and broadScopeCauses, the
refinement Markdown summary adds a remaining broad scope table, and etp hotspots
exposes both lists under ScopeAnalysis. These diagnostics do not enter the
refinement decision hash.
etp ci refine-post-build --workspace . `
--base-ref <exact-base-sha> --head-ref <exact-head-sha> `
--conservative-selection artifacts/test-selection.mpack `
--conservative-partition artifacts/test-partitions.mpack `
--build-impact-root artifacts/current-build-impact `
--prepare-manifest artifacts/etp-prepare.build.json `
--selection-output artifacts/test-selection.effective.mpack `
--partition-output artifacts/test-partitions.effective.mpack `
--receipt-output artifacts/etp-post-build-refinement.json
For PR delta builds, also pass the verified current-head materialization with
--build-impact-overlay. The prepare manifest is validated against the exact
base/head pair in both direct-root and overlay modes, but it is forwarded to
the planner only with an overlay. This replaces the previous invalid
ci plan --build-impact-prepare-manifest direct-root combination.
A receipt is versioned under SchemaVersion: "etp.ci-plan.v1" and records the final Outcome (selected, empty, refresh, forced-full, or conservative-fallback), the selected-test count, the effective partition count, the reason codes, and the deterministic SchemaHash used to bind the receipt to the current request shape. Use --refresh for an explicit cache-refresh receipt; use --force-full for an explicit full-suite receipt.
--test-inventory accepts a canonical MessagePack etp.test-inventory.v1 payload.
etp workflow affected-tests plan evaluates this inventory automatically when
no external inventory is supplied. Staged CI adapters can use the same evaluator:
etp ci inventory --workspace . --prepare-manifest artifacts/prepare.mpack --output artifacts/test-inventory.mpack
The prepare graph defines standalone test roots; nested fixtures are excluded.
Evaluation is sequential, bounded to 90 seconds per project, cancellable, and
does not restore or compile. Missing SDK, frameworks or assembly identity fails
the command instead of fabricating defaults. Identity inventory has no method
coverage claim (IsComplete=false). Transport only the .mpack; consumers no
longer need a PowerShell MSBuild evaluator or a duplicate JSON projection.
An empty fallback with identical exact base/head commits remains empty; unknown
or symbolic diff identities retain the conservative behavior.
The native evaluator launches each
test project from its own directory so the closest global.json selects that
project's SDK. Every project records SdkVersion, AssemblyName, and all
TargetFrameworks; ETP emits one prebuilt module fragment per TFM and retains
semantic class/method inventory from the planner. This supports repositories
that mix SDKs as well as projects that multitarget without guessing net10.0
or treating a single TFM as a scalar string.
StageTimings on the receipt is the normalized planning timeline. It includes
discovery (git-diff), compiler snapshot loading
(build-impact-snapshot-load), index refinement, and partitioning when those
stages run. CostEvidence.ImpactEvidenceMode distinguishes planner-only,
snapshot-refinement, and future snapshot-overlay decisions. The same evidence
records selection counts, cache disposition, the timing-baseline source, and
separate historical restore, build-only, and test-duration estimates.
When durations export --merge-baseline incorporates a partial test run, ETP
retains observations for tests that did not run and applies the configured
merge alpha to observations that did. TotalDurationMilliseconds is then
recomputed as the sum of positive retained test durations, not blended with
the partial run's total. This also repairs a stale aggregate on the next
export without changing the baseline schema or creating a second payload.
The aggregate represents test-observation time; it is not elapsed suite time
and does not include project runner startup overhead.
TRX display names are not unique case identifiers: parameterized tests can print the same truncated arguments for different cases. Export coalesces matching names within each normalized project into a duration bucket, summing their observed durations and retaining failure/flaky evidence. Merge applies alpha once to each old/new bucket, not repeatedly to individual colliding rows. Observation counts track the bucket's run observations; project test counts still come from the raw result rows. Equal names in different projects remain separate. Existing baselines containing duplicate rows are normalized on read-for-merge with the same rule; no case is silently dropped and no second JSON payload is written.
Duration export also reads native partition-execution-receipt.mpack files when
given --partition-manifest (JSON or MessagePack). It verifies fragment identities
against that manifest and records runner time for a project when its planned
fragments completed successfully and either are all whole-project scopes, or are
class and class-group fragments that, for each target framework, together ran
every class of the project's complete inventory. The inventory comes from
--selection-manifest, the run's selection manifest; native finalization uses its
effective selection. A class split records one observation: the sum of each
process's elapsed time minus its startup allowance, plus one startup allowance per
target framework, which is what one process per framework running every class
would take. That keeps it comparable with a whole-project observation,
which both the whole-project estimate and the class-split decision use. Summed
test durations would leave out process and fixture work outside test bodies,
double count tests that overlap, and need TRX. Incomplete, failed, all-skipped and
method selections, a class that did not run, and a missing or incomplete inventory
do not become project observations and leave the previous average untouched.
Duplicate identical receipts are not counted twice;
conflicting observations and mismatched manifests are rejected.
An explicitly supplied partition or selection manifest must exist, deserialize,
and use the supported schema even when the result directory has no native
receipt. Invalid explicit manifests no longer silently become an unscoped export;
omit the option only for intentionally unscoped TRX exports.
These observations use RunnerDurationSource: "partition-execution-receipt".
They are usable for project estimates even without TRX, but do not synthesize
per-test observations, a suite total, or a selected/suite duration ratio.
Every successful fragment that ran a known set of whole classes also records class
process times under the project's Classes: class and class-group fragments,
and whole-project fragments whose classes the inventory or the run's TRX names.
The process's elapsed time beyond its startup allowance is split among its
classes by that process's own TRX durations. A class its TRX does not name uses its
previous class process time, then its previous summed test durations in the same
project, and a class without any evidence takes the mean share. A process that
finished within its allowance records zero for its classes, which the partitioner
treats as evidence, not as missing data. A class run by several processes, one per
target framework, records the mean. Merge applies the
alpha to each observed class and keeps the others. The partitioner uses a
class process time instead of summed test durations for class selections, for
classes coalesced from methods, and for class-group shares. When every class of
a process-isolated-safe project has one, their sum replaces the project average
for the split decision, for each class group and for the project kept whole,
including a project that cannot split.
Work beyond the startup allowance, such as a per-process build, is attributed to
the classes of the process that paid it. durations summarize omits class
process times.
For per-test duration observations, set "reportTrx": true in the partition
settings passed to partition, ci plan, or workflow affected-tests plan/run.
This is opt-in: the selected test modules must provide
Microsoft.Testing.Extensions.TrxReport. ETP preserves the reporting flag on each
final executable fragment after batching/coalescing and adds --report-trx at
execution time, without changing semantic filters, costs, or partition weights.
The report is written in the fragment's isolated results directory and consumed
by the same duration exporter. Missing reporter support is a runner error, not a
reason to retry the suite with different filters. Existing settings remain off.
TRX test durations update per-test history, not the project's measured runner time:
a partial report cannot overwrite a complete-project observation or increment its
runner observation count. Whole-project runner timing comes from execution evidence.
The planner and partitioner enforce that same boundary when reading the baseline:
method/class observations are not summed into an alleged complete-project duration.
TestCount is an observed count, not a certificate of inventory coverage. Without
a project runner observation, whole-project cost remains unknown (or uses an
explicit configured default), while failure, flakiness, and exclusivity constraints
remain available. Existing per-method observations still support economic
coalescing when the planner has a complete inventory and costs every included test;
an untyped DefaultWeight is not a duration in milliseconds. No baseline format
migration or duplicate payload is required.
For economic method/class-to-project coalescing, a measured whole-project runner
duration takes precedence over summing method durations (which can overlap under
runner parallelism). The complete current inventory gate is unchanged. The
configured process-startup allowance remains a separate planning reserve added
once per emitted fragment; it is not a measurement of extra wall time. When no
whole-project observation exists, ETP can still estimate every inventory method
from observations or explicit millisecond defaults.
The duration baseline can also ingest build-timings.json files with
SchemaVersion: "etp.build-timings.v1". Each entry records Graph,
ProjectPath, RestoreDurationMilliseconds, and BuildDurationMilliseconds.
ETP distributes one parallel graph observation across its build roots and
compares total wall-clock cost: planner startup + restore/build + selected test
duration. This prevents a cheap-looking test assembly from being selected
without accounting for the compilation graph required to run it. The receipt
reports the broad and selected build estimates, expected net saving, and the
economic decision separately from correctness-driven fallback reasons.
etp ci build retains its restore/build stage timings and now starts its clock
before graph handling. Its receipt reports either graph-materialization or
graph-validation, together with the graph source, root-project count, and
serialized graph bytes. This keeps ETP graph overhead visible instead of hiding
it before the reported build interval.
ETP-driven builds (CI build stages and the test-phase prerequisite bundle) keep
MSBuild node reuse and the Razor build server disabled, but compile through a
run-private Roslyn compiler server: every command carries
/p:UseRazorBuildServer=false /p:SharedCompilationId=etp-<hash>, where the id is
derived from the run root and the ETP process so all stages of one run share one
warm VBCSCompiler and no other server on the machine is ever reused. ETP does
not force UseSharedCompilation as a global property: the SDK already defaults
it to true, and keeping it project-scoped lets ETP switch satellite-assembly
compilation to in-process csc (the SDK's satellite Csc call forwards
UseSharedCompilation without SharedCompilationId, which would otherwise
start and leak the machine-wide server). The switch is injected into every
project ETP builds through /p:MSBuildUserExtensionsPath=<tmp>/etp-compiler-server/targets/<content-hash>/msbuild
(Current/Microsoft.Common.targets/ImportAfter/Eternet.TestPlanner.CompilerServer.targets),
so projects that do not reference Eternet.TestPlanner.Build are covered too;
the package ships the same targets for builds outside ETP. The location is
content-addressed (the first 16 hex digits of the SHA-256 of the targets text)
rather than per server id, the file's LastWriteTimeUtc is pinned to a fixed
past instant (2000-01-01 UTC), and it is never deleted at shutdown: every
imported file joins $(MSBuildAllProjects), a timestamp input of CoreCompile
and other incremental targets, so a per-run or freshly rewritten import would
force every project to recompile in the test phase even though the build phase
had already produced it. ETP also forces
/p:ImportUserLocationsByWildcardAfterMicrosoftCommonTargets=true (an
executor-owned property, like MSBuildUserExtensionsPath) so a project or
Directory.Build.props that disables wildcard user imports cannot silently
skip the injection; the redirected extensions path contains nothing else.
Set EternetTestPlannerIsolateSatelliteCompilation=false to opt out;
a caller /p:UseSharedCompilation=false still disables the server entirely.
ETP shuts that server down through its own pipe when the stage finishes (the
receipt records a compiler-server-shutdown timing with its exit code) and
never invokes the machine-global dotnet build-server shutdown. Workflow
callers that chain stages pass one compilerServerId with keepCompilerServer
and call CompilerServerScope.ShutdownAsync once at the end of the phase.
PR-scoped green checkpoints and delta authority
etp.build-impact-delta.v2 is a PR-lineage authority, not a global last-green
hint. It binds the repository and pull-request number to a certified head, the
trusted PR base or merge-base, an optional parent-head/checkpoint pair, terminal
plan/build/test receipt hashes, and schema/tool/compiler fingerprints. Delta
operations are canonicalized as deterministic upsert, delete, or
tombstone records by project build identity and input path.
delete removes a contribution introduced in the checkpoint lineage;
tombstone explicitly masks a contribution from the complete base snapshot.
The parent chain is explicit: parentCertifiedHeadSha is a Git commit used for
ancestry, while parentCheckpointFingerprint is the SHA-256 identity of the
previous checkpoint. Receipt evidence contains a contained relative path,
length, and SHA-256; validation reopens each file under --artifact-root and
checks its real bytes. Build and test receipts use
etp.external-execution-receipt.v1: the workflow records the terminal result of
its explicit dotnet build or dotnet test command. ETP validates and seals
that evidence only; it does not run the command or transport build outputs.
Validate a transported delta before choosing it as the planning base:
etp build-impact-delta validate --manifest .etp/build-impact-delta.json \
--workspace . --repository Eternet/Eternet.Agents.Control \
--pull-request-number 376 \
--base-ref origin/main --head-ref HEAD \
--base-or-merge-base <trusted-base-sha> \
--artifact-root .etp/receipts \
--schema-fingerprint sha256:<schema> \
--tool-fingerprint sha256:<tool> \
--compiler-fingerprint sha256:<compiler>
Exit code 0 accepts the checkpoint. Exit code 2 emits a versioned fallback
receipt with actionable reason codes and fallbackTargetSha; callers must plan
from that trusted base/merge-base instead. A different PR, foreign repository,
force-pushed or rebased non-ancestor head, incomplete/non-green receipts, or any
schema/tool/compiler mismatch fails closed. Checkpoints are never selected
across PR lineages merely because they are the newest available artifact.
After the host restores the content-addressed object and its portable base
snapshot to local paths, resolve the complete planning state through ETP:
etp build-impact-delta resolution decide \
--pointer .etp/current.json \
--manifest .etp/current/checkpoint.mpack \
--artifact-root .etp/current \
--base-snapshot-root .etp/base-snapshot \
--workspace . --repository Eternet/Eternet.Agents.Control \
--pull-request-number 376 \
--base-ref origin/main --head-ref HEAD \
--graph-fingerprint <graph-sha256> \
--validation-request-output .etp/checkpoint-validation-request.json \
--output .etp/checkpoint-resolution.json
The versioned receipt decides checkpoint, source-only, or fallback and
contains the chosen base, mode, identities, and nested physical validation.
ETP validates pointer v2 identity, content address, Git ancestry, graph and
runtime fingerprints, snapshot presence, and terminal receipt bytes. Pointer
v1 is deliberately rejected at the boundary; migration is an explicit
one-time operation, never a runtime fallback. The host may locate and restore
objects, but it must not reconstruct this semantic decision.
When transport cannot supply a pointer or restored object, the adapter reports only that bounded platform fact and ETP still owns the fallback base and receipt:
etp build-impact-delta resolution unavailable \
--workspace . --repository Eternet/Eternet.Agents.Control \
--pull-request-number 376 \
--base-ref origin/main --head-ref HEAD \
--reason checkpoint-not-found \
--output .etp/checkpoint-resolution.json
Allowed reasons distinguish store, pointer, object, and snapshot-restore
availability. ETP resolves both refs and their merge-base; the host never
constructs baseRef, fallbackTargetSha, mode, or baseline usability.
When the pointer itself is available but its object or restored snapshot is
not, pass --pointer .etp/current.json; ETP retains its certified ancestor as
source-only when safe and otherwise falls back to the merge-base.
An advanced merge-base can still use that source ancestor when ETP proves the
original base precedes both the certified head and current merge-base, and the
certified head precedes the current head. The compiler snapshot remains unused.
Preproduction v1 pointers can be converted once, after restoring their exact base snapshot and object, without enabling a v1 reader in normal planning:
etp build-impact-delta pointer migrate-v1 \
--source-pointer .etp/current-v1.json \
--manifest .etp/current/checkpoint.mpack \
--artifact-root .etp/current \
--base-snapshot-root .etp/base-snapshot \
--workspace . --repository Eternet/Eternet.Agents.Control \
--pull-request-number 376 --current-head <head-sha> \
--graph-fingerprint <graph-sha256> \
--output .etp/current-v2.json
Migration reconstructs the missing v2 snapshot identity from the v1
baseOrMergeBaseSha, then runs the full v2 resolver. It writes a distinct v2
file only for an accepted checkpoint; otherwise it emits a rejection receipt
and leaves the source untouched. The host may atomically replace its trusted
pointer only after that receipt reports migrated.
Publication is monotonic and idempotent: an identical checkpoint is a no-op, a late run cannot replace a descendant checkpoint, and incomparable force-push lineages can replace one another only after the candidate has independently passed the complete green validation contract. Ask ETP for the publication decision before taking the platform-specific store lock:
etp build-impact-delta publication decide \
--candidate-manifest .etp/candidate/checkpoint.mpack \
--candidate-artifact-root .etp/candidate \
--existing-manifest .etp/current/checkpoint.mpack \
--existing-artifact-root .etp/current \
--workspace . --repository Eternet/Eternet.Agents.Control \
--pull-request-number 376 --current-head <head-sha> \
--base-or-merge-base <trusted-base-sha> \
--schema-fingerprint sha256:<schema> \
--tool-fingerprint sha256:<tool> \
--compiler-fingerprint sha256:<compiler> \
--output .etp/publication-decision.json
The command validates both physical receipt sets and returns publish,
no-op, or reject without writing the store. A complete seed may safely
re-seed a newer or diverged lineage; a delta must bind the exact valid parent.
The host adapter owns credentials, paths, locking, atomic object/pointer copy,
retention, and branch/event policy. It must not reinterpret ETP's decision.
For a changed production closure, set
EternetTestPlannerImpactIndexMode=delta together with the normal prepare
manifest path. The ISG then suppresses the complete project carrier and emits
only the declared changed source contributions. The MSBuild target writes one
etp.build-impact-delta-fragment.v1 per project + target framework +
configuration + platform + runtime identifier + SDK version.
Deletes require matching base-carrier evidence; a removed compile item becomes
an explicit tombstone. A missing base contribution fails the target instead of
inventing a hash.
Point EternetTestPlannerImpactDeltaBaseRoot at the prior complete index
snapshot. The package resolves the exact etp-layout-v3 carrier for every
project inner build, including multi-project, multi-TFM, multi-RID, and
multi-SDK graphs. It rejects a carrier whose embedded identity differs even if
the file is present at the requested path.
Seed the cache once with a complete full etp-layout-v3 snapshot. Version 2 is
not used as an implicit fallback, and a PR delta with no compatible v3 project
carrier fails with a seed-required error rather than publishing partial
authority. The sole exception is a project/build identity first introduced by
the PR: it may emit only upserts with null base fingerprints. Deletes or
tombstones still require the exact base carrier or a certified parent
checkpoint.
Fragments are deliberately not checkpoint authority. After external build and test commands have terminal green receipts, compose them into the certified contract:
Transport many small fragment files as one deterministic, validated envelope:
etp build-impact-delta archive pack \
--fragment-root .etp/current-fragments \
--promotable-only \
--output-envelope .etp/transport-envelope \
--receipt-output .etp/receipts/archive-pack.json
etp build-impact-delta archive unpack \
--envelope .etp/transport-envelope \
--output-root .etp/restored-fragments \
--receipt-output .etp/receipts/archive-unpack.json
The ETP v1 envelope contains only manifest.json and
impact-delta-fragments.zip. Its manifest binds SHA-256, compressed and
uncompressed lengths, file count, and fragment count. ZIP entry names,
timestamps, metadata, and ordering are normalized so identical inputs produce
identical payload bytes. Pack is strict by default and rejects incomplete or
fallback fragments. Explicit publication mode (--promotable-only) first
validates every discovered fragment's schema, current authority, schema
fingerprint, and unique build identity, then excludes only incomplete/fallback
identities and archives each complete fragment with files in that identity's
directory. The receipt reports excludedFragments. If no complete fragment
remains, the command succeeds with a zero-fragment receipt and does not create
or replace the output envelope; callers can skip optional publication. Invalid
schema, authority, fingerprint, or duplicate identity always fails and is
never treated as an excluded fragment. Unpack validates size and
compression-ratio bounds,
path traversal (including Windows drive/UNC/backslash forms), duplicate and
case-colliding names, symlink/reparse metadata, every fragment schema and
authority, then manually extracts to staging and atomically replaces the
output. Only the current ETP envelope schema is accepted; loose fragment trees
and EAC v1 envelopes must be regenerated. An invalid envelope never falls back
to another representation. Receipts are typed JSON on
stdout and may also be persisted with --receipt-output. The caller-supplied
artifact roots and everything beneath them reject symlink/reparse entries;
intentional runner mount or junction ancestors outside those roots are allowed.
etp build-impact-delta compose --request .etp/checkpoint-request.json \
--parent-manifest .etp/previous-build-impact-delta.json \
--fragment .etp/index/net10/Eternet.TestPlanner.ImpactDelta.fragment.json \
--fragment .etp/index/net11/Eternet.TestPlanner.ImpactDelta.fragment.json \
--artifact-root .etp/receipts --workspace . \
--output .etp/build-impact-delta.json
The request supplies repository, PR and lineage identities, the tool
fingerprint, and physical plan/build/test receipt evidence. The compose command
hashes and parses those receipts and proves Git ancestry before it writes a
checkpoint. Composition rejects
incomplete or mixed-schema fragments, derives the compiler-set fingerprint
from every project build identity, canonicalizes all operations, and produces
etp.build-impact-delta.v2. Validate that result before selecting the
checkpoint as a diff base.
--parent-manifest is optional for C1 and required when advancing an existing
PR checkpoint. Composition verifies its repository, PR, merge-base, head,
checkpoint hash, schema, tool, SDK/TFM, and per-project compiler identity, then
folds current operations into one cumulative merge-base-to-head delta. An add
followed by a delete cancels out; later updates retain the original base
fingerprint. This keeps one bounded checkpoint per PR instead of requiring an
SMB chain walk. Pass the same parent to the MSBuild graph with
EternetTestPlannerImpactDeltaParentManifestPath and its trusted hash with
EternetTestPlannerImpactDeltaParentCheckpointFingerprint; this lets C2 prove
a delete of a file first introduced in C1.
Materialize the validated cumulative overlay without copying build outputs:
etp build-impact-delta materialize \
--manifest .etp/build-impact-delta.json \
--validation-request .etp/checkpoint-validation-request.json \
--base-root .etp/full-v3 \
--prepare-manifest .etp/prepare.json --workspace . \
--output .etp/build-impact-overlay.mpack
etp ci plan --workspace . --base-ref <merge-base> --head-ref <head> \
--build-impact-root .etp/full-v3 \
--build-impact-overlay .etp/build-impact-overlay.mpack \
--output .etp/ci-plan.json
Materialization validates physical receipt hashes and Git lineage, reopens the
full-v3 carriers, verifies every baseContributionFingerprint, and applies the
cumulative operations in memory. It writes etp.build-impact-overlay.v2; ci plan verifies that receipt and its exact base root before refinement and marks
cost evidence as snapshot-overlay. Missing, deleted, duplicated, or foreign
project/build identities fail closed and cannot advance a checkpoint.
Overlay materialization fingerprints (etp.build-impact-overlay.v2 and
etp.build-impact-ephemeral-overlay.v3) certify content, not location. The base index
root records where the base snapshot lies on the current runner; every consumer still
compares it with its own base root, but the fingerprint leaves it out, so moving the run
root between runners never re-certifies an overlay. The rest is split into sections: the
header (the materialization without its fingerprint, base index root and shards), each
semantic shard without its entries, and that shard's declarations and tests in runs of
4096. Each section's canonical JSON (sorted properties, default string escaping,
normalized numbers) is digested with SHA-256 on up to two workers, and the fingerprint is
SHA-256 over etp.build-impact-overlay-fingerprint.v2 followed by one line per section in
document order: its name, a space and its digest. Changing, adding, removing, moving or
reordering any entry, shard or header field changes a line or the line sequence, and
relabelling a fingerprint under the other schema never verifies.
Readers accept the previous schemas (etp.build-impact-overlay.v1,
etp.build-impact-ephemeral-overlay.v2) during the transition and verify them with their
original algorithm: SHA-256 of the canonical JSON of the whole materialization, base
index root included. ETP never writes them. Overlays are materialized in the run that
consumes them and are not stored across runs, so the transition only matters for a run
root planned by an older ETP. Planning certifies such an overlay once in the current
schema when it rebases it; checkpoint manifests and their fingerprints are unchanged.
Before the current head has terminal build/test receipts, use the separate local-only overlay contract. It never composes or implies green evidence:
etp build-impact-delta materialize-ephemeral \
--repository Eternet/Eternet.AspNetCore \
--base-or-merge-base <merge-base> --current-head <head> \
--base-root .etp/full-v3 --prepare-manifest .etp/prepare-current.json \
--parent-overlay .etp/certified-parent-overlay.mpack \
--fragment .etp/current/Product-net10.fragment.json \
--fragment .etp/current/Product-net11.fragment.json \
--workspace . --output .etp/current-head-overlay.mpack
etp ci plan --workspace . --base-ref <parent-or-merge-base> --head-ref <head> \
--build-impact-root .etp/full-v3 \
--build-impact-overlay .etp/current-head-overlay.mpack \
--build-impact-prepare-manifest .etp/prepare-current.json \
--output .etp/ci-plan.json
etp workflow affected-tests plan --workspace . \
--base-ref <parent-checkpoint-head> --head-ref <head> \
--build-impact-root .etp/full-v3 \
--build-impact-overlay .etp/current-head-overlay.mpack \
--build-impact-prepare-manifest .etp/prepare-current.json \
--build-impact-parent-checkpoint .etp/parent/checkpoint.mpack
Omit --parent-overlay for the first head after the full-v3 seed. When it is
present it must be an already validated etp.build-impact-overlay.v2 (or legacy v1)
for the same repository, merge-base, and ancestry. The output schema is
etp.build-impact-ephemeral-overlay.v3; it carries its verified source fragments
for later publication by the same native affected-tests run. It keeps
authority=current-head-local-nonpublishable and nonPublishable=true, so it
cannot be supplied directly as publication authority. It binds the exact prepare file,
resolved Git base/head, every production build identity present in the trusted
full-v3 seed, every changed production input, compiler/build identities, and
current source contribution hashes. Repeat --fragment once per affected
production SDK/TFM/configuration/platform/RID identity; a test-only change may
omit --fragment. Global, non-source, unattributed, partially covered, or
graph-identity changes fall back to the full planner path. ci plan reopens the
same prepare file and rejects a stale, foreign, incomplete, tampered, or
SDK/TFM/configuration/platform/RID-mismatched overlay before refinement. This
path materializes semantic evidence only and never copies assemblies. The standalone
ci plan form remains read-only. The native workflow form owns the three inputs and,
after terminal green build/test evidence, supplies their parent identity and exact
current fragments to build-impact-delta publication prepare --affected-tests-run-root.
Use --publication-root when the immutable candidate and pointer must be staged outside
the tested checkout. ETP requires both outputs to remain below that explicit boundary
and rejects overlap with the workspace, native run, tool installation, or object store.
An absent, foreign, stale, non-canonical, or differently fingerprinted parent manifest
fails closed; an empty current fragment set may advance only the explicitly consumed
parent and never creates a seed from nothing.
GitHub Action
Consumers can use the thin adapter in this repository to install a pinned ETP version and run the atomic planning receipt without any repository-specific changed-path or partition policy:
- name: Resolve the CI plan
uses: Eternet/Eternet.AspNetCore/.github/actions/etp-ci-plan@<immutable-sha>
with:
etp-version: 0.1.0
package-source: ${{ github.workspace }}/artifacts/etp-tool-package
workspace: ${{ github.workspace }}
base-ref: ${{ github.event.pull_request.base.sha }}
head-ref: ${{ github.sha }}
analysis-depth: semantic
output-file: ${{ github.workspace }}/.etp/ci-plan.json
summary-output-file: ${{ github.workspace }}/.etp/ci-plan-summary.txt
github-output-file: ${{ github.workspace }}/.etp/ci-plan-gh-output.txt
The action pins the exact tool version, validates the CLI identity with etp --version, rejects mutable version refs like @main, and fails when the resulting receipt schema is not etp.ci-plan.v1. A local feed or package directory is allowed as a bounded startup fallback, but it is never used to invent new selection or partition policy.
Install
Install the package from NuGet when it is published:
dotnet tool install --global Eternet.TestPlanner
For local validation from this repository, pack the CLI and install from the local artifact directory:
dotnet pack tools/Eternet.TestPlanner/src/Eternet.TestPlanner.Cli/Eternet.TestPlanner.Cli.csproj -c Release -o artifacts/etp-nupkg
dotnet tool install --global Eternet.TestPlanner --add-source artifacts/etp-nupkg --version 0.1.0 --ignore-failed-sources
During development, run the project directly:
dotnet run --project tools/Eternet.TestPlanner/src/Eternet.TestPlanner.Cli/Eternet.TestPlanner.Cli.csproj -- plan --workspace . --base-ref origin/main --head-ref HEAD --analysis-depth balanced --output artifacts/test-selection.json
Commands
etp prepareevaluates the MSBuild project graph once and emits a deterministicetp.prepare.v2manifest for later builds and semantic shard reuse. It is a planning receipt only; missing or unknown inputs remain conservative. Consumers can pass the absolute manifest path throughEternetTestPlannerPrepareManifestPath; the transitive build targets validate the file and expose it as anAdditionalFilesinput without running Git or re-evaluating the project graph.etp plancompares a workspace diff and writes an affected-test selection manifest.etp indexprecomputes semantic analysis cache entries for laterplanruns in the same workspace and cache directory.etp explainexplains one selection from an existing manifest.etp hotspotsaggregates witness paths from an existing manifest by node and edge kind, including the number of distinct selection entries traversing each node. It helps find broad dispatch bridges without rerunning planning. Bothexplainandhotspotsalso read the final selection directly from a native replay ZIP (build/effective-selection.mpack). Counts describe recorded witness paths and selection entries, not the number of tests that could safely be omitted; project and class entries can represent many executable tests. If the ZIP includesbuild/post-build-refinement.json, the report also includesScopeAnalysiswith before/candidate/effective counts, project and method scopes, narrowing status, and the planner's reason codes. This shows when a conservative project scope offsets more precise candidate selections;WideningScopesnames the changed inputs behind each remaining broad scope.etp inspect-replay --archive <native-replay.zip>reads the terminal receipt (or the saved workflow state if finalization did not complete), phase timings, reason codes, partition completion, and which selection artifacts exist. It works whenhotspotscannot because the effective selection is absent. The reported selection count is a workflow scope count; a build selection does not establish the effective or executed test scope. Missing partition evidence remains unknown for a state-only archive; available partition receipts supply observed outcomes and timings.BuildProcessMillisecondssums build process durations, whileBuildStageMillisecondsreports elapsed wall time for the build stage. A damaged terminal receipt falls back to the saved state, and a receipt from an earlier workflow attempt is markedsuperseded. Partition receipts are scoped to the state's active test attempt or the terminal receipt's listed test artifacts.EffectiveScopeStatusreportsinvalidfor a damaged or structurally incomplete effective manifest. When the effective selection is available,BaseSourceOwnershipDiagnosticslists each deleted source's BASE compiler or evaluation outcome, mapped owners and former consumers, and elapsed time. An unproved input names the fallback reason.etp partitionwrites deterministic partitions from a manifest without rerunning git, MSBuild, or Roslyn analysis.etp auditcompares a selection manifest against broader evidence such as TRX results and coverage maps.etp evaluateruns planner corpus cases to measure selection behavior across known diffs.etp ci lane-planapplies the repository's lane policy. DeclaredcontrolPlanePathskeep that classification when deleted or renamed; ordered exclusions still apply. A runtime/unknown old path remains a global invalidator even if renamed into a control-plane directory. This policy does not implicitly classify every script undereng/as control-plane.etp ci lane-test --workspace . --lane-plan artifacts/receipts/lane-plan.mpack --lane-manifest eng/lanes/core.json --run-root artifacts/lane-tests/core --report-trxexecutes that lane's recorded decision after its build/pack step. The run root must be fresh and inside the execution workspace. ETP validates the lane schema and project closure, preserves selected filters, costs and exclusivity, and executes prebuilt outputs without replanning or rebuilding. It writes onepartitions.mpack, a terminalpartition-execution-receipt.mpack, a smalllane-test-receipt.json(status, timing, counts, distinct projects and the detailed receipt path), process logs andsummary.md; pass this run root to duration export. No detailed JSON copy is emitted. A failed module or an empty method match returns nonzero. Full-lane commands use explicit dimensionless default weights, not invented millisecond durations. Upgrade native execution, finalization and duration export together: new run roots discover only the canonical binary execution receipts. Old JSON run roots are archival evidence, not inputs to a newly resumed workflow. Start a fresh run when upgrading; adapters must read the small lane summary instead of parsing detailed execution fragments. The receipt's logical v2 schema is unchanged.etp benchmarkruns a reproducible cold/index/warm cache benchmark and writes a machine-readable performance manifest.
Example planning flow:
Use --input-policy eng/LaneWorkflowPaths.json on plan, ci plan,
ci refine-post-build, or workflow affected-tests plan|run to share the existing
etp.ci-lane-policy.v1 control-plane rules. ci lane-plan passes its policy to
the underlying planner automatically. This is explicit: no repository file is
automatically trusted by name. General planning accepts control-plane patterns
only under infrastructure directories (eng/, .github/, scripts/ci/, .etp/)
and only script/config extensions; source, project, solution and imported build
files keep their normal impact policy. Ordered negative patterns remain effective.
A repository whose tests do not validate Markdown content can set
"markdownAffectsTests": false in that policy. This excludes undeclared .md
inputs from test ownership, lane routing and change
budgets. The original diff and declared-test-neutral-markdown decision remain in
the evidence. Source and build-file changes in the same commit still affect tests.
Markdown fixtures that must trigger validation belong in eng/etp-input-exceptions.json;
those exceptions take precedence over this setting. This is test selection
policy, not permission to omit changed content from packages or deployment.
CI and test-execution inputs are classified explicitly:
| Class (reason code) | Built-in paths | Default impact | Explicit mapping |
|---|---|---|---|
github-workflow |
.github/workflows/** |
none for product tests; recorded as data-input-no-known-consumer |
eng/etp-input-exceptions.json rules select the tests that validate workflows |
ci-infrastructure |
.github/actions/**, scripts/ci/**, eng/ci/**, eng/Test-*.ps1, .etp/ci-*.json |
same as workflows | same as workflows |
declared-control-plane-input |
the policy's controlPlanePaths |
same as workflows; a plan of only these skips the project graph | not needed |
test-execution-config |
*.runsettings, xunit.runner.json, testconfig.json, *.testconfig.json, etp-partition-settings.json, .etp/partition*.json, and the policy's testExecutionInputPaths |
every top-level test project (TopLevelTestFallback) |
an eng/etp-input-exceptions.json rule narrows it to the declared consumers |
global-build-file |
Directory.Build.props/.targets/.rsp, global.json, NuGet.config |
every project | none |
Workflow and CI-infrastructure files cannot change what product code does; they can change how tests
run, which the CI validates with its own contract tests or scripts. Test settings and partition settings
are read by the test host or the partitioner, so they fail closed. controlPlanePaths cannot make a
test-execution input neutral, and testExecutionInputPaths (repository-relative globs, never source,
project or MSBuild files) can only widen a CI, data or documentation classification. A path that matches
no class, such as .github/dependabot.yml, keeps the repository-file classification, so a plan that
changes only such files widens to the full suite.
For a repository that has verified Markdown files are not evaluated build inputs,
set "ignoredExtensions": [".md"] in the same policy. The opt-in allows only
reviewed document/data suffixes (.md, .resx, .txt, .csv, .tsv); it does not
certify that the files are semantically irrelevant to tests. A repository that embeds
Markdown as EmbeddedResource cannot ignore those files. The policy removes
verified matching paths from test ownership and lane impact, including declared
data that is not evaluated as a critical build input. ETP checks the unscoped
evaluated graph before ignoring a present path. Deleted, renamed-away, missing,
or unverified paths stay in the changed set and conservatively widen impact.
The selection manifest records verified ignored
paths separately in ignoredInputs; the lane receipt records ignoredFiles
alongside the original changedFiles. All other extensions, including source,
project, build, script and configuration formats, are rejected. Evaluated Compile, Analyzer, analyzer
config files, EmbeddedResource, AdditionalFiles, markup source, project and imported build inputs cannot be
ignored even when their extension is configured. Changes with other
extensions in the same diff retain their normal impact.
Repositories that accept missing document/data validation can instead set
"unevaluatedIgnoredExtensions": [".md", ".resx", ".txt", ".csv", ".tsv"].
Choose only suffixes whose tests should be omitted. This reusable opt-in applies to every matching
file, including EmbeddedResource, AdditionalFiles, generator inputs, deletions and both sides
of renames, without evaluating MSBuild to prove irrelevance. It accepts the risk of missed resource,
generator and test failures; publish or document evaluation workflows can use a stricter policy.
Source and build/configuration suffixes remain rejected, and unknown suffixes retain normal impact.
Mixed changes select tests from the other changed files. Receipts emit
input-policy-unevaluated-ignore-risk-accepted and per-file UnevaluatedInputIgnoreRiskAccepted.
Use the same --input-policy on prepare and subsequent planning commands; native CI propagates it.
Prepare filters changed-file ownership but keeps exact evaluated compiler inputs and content hashes,
so changing embedded content can still invalidate build evidence and require a rebuild.
Selection evidence records the policy schema, path and SHA-256. Cache storage is
partitioned by policy content hash. Native workflows copy the policy into their
owned input snapshot and verify it before build/finalize. Post-build refinement
rejects a missing or changed policy with post-build-input-policy-mismatch,
retaining the original selection in the rejected receipt. Pass the same policy
to manually staged commands; do not introduce it only at refinement time.
Selection files support .mpack/.msgpack (compressed MessagePack) and .json.
plan --output writes the format selected by the extension; explain and
audit read that same payload directly, without creating a JSON sidecar. Prefer
.mpack for large persisted selections. JSON remains supported for explicit
inspection; null or malformed selections fail instead of becoming empty work.
Compiler-index selections also record one deterministic shortest impact witness per
exact method selection in SymbolImpactGraph. etp explain renders its labels from
the existing canonical payload: changed declaration, referencing members (including
cross-carrier dispatch bridges), and selected test. Tests selected because their own
source changed record that source instead of inventing a symbol reference. Shared
nodes and edges are deduplicated; unselected graph branches are not serialized.
The reverse-dependency traversal visits each reachable node/edge once rather than
repeatedly scanning every declaration until convergence. This does not change the
selection, receiver analysis, fallback guards, partition weights, or publication
authority. In particular, a witness crossing System.Object.ToString or another
virtual declaration documents conservative possible dispatch, not proof of a specific
runtime receiver. Existing v1 selection readers remain compatible; older payloads
without recorded paths must be replanned to gain witnesses, not inferred from labels.
Solution layout comparison distinguishes an absent file at either revision from
an existing empty or malformed .slnx. An added/deleted valid solution is a
known layout change; invalid XML remains incomplete and conservative.
A .slnx edit is diffed entry by entry, and SolutionChanges in the selection
manifest (and the plan summary) records each entry's classification and impact:
| Edit | Impact | Reason code |
|---|---|---|
| Folder moves, folder renames, element order | none | solution-layout-no-op |
| Project entry added | that project and its reverse closure | solution-entry-added |
| Project entry removed, project still in the graph | that project and its former dependents | solution-entry-removed |
| Project entry removed, nothing in the graph references it | none | solution-entry-removed |
Solution configurations, platforms, properties or root attributes; solution items (File); a project entry's own content (configuration or platform mapping, BuildDependency, Build/Deploy, Type); an added or removed BuildDependency target |
members from both revisions and their reverse closure | solution-change-unclassified, solution-member-impact |
| Unknown element, text content, duplicate or non-literal member, malformed XML | every graph project | solution-layout-evaluation-incomplete |
Entry-level impact (solution-entry-level-impact) also has to prove that the graph
holds every project reference that can reach the impacted projects: the graph is
evaluated without the build's global properties, so a reference behind a
condition, Choose branch or target, one dropped by a conditional Remove, one a
task outputs into ProjectReference, or one declared in an import the evaluation
skipped can be missing. Every ProjectReference declared in a graph project's file
or in a build file of the import closure must either appear in that project's
evaluated references, or point neither into the impacted projects nor to an
existing project outside the graph (whose own references were never evaluated).
The closure is every
workspace .props/.targets, every evaluated import, and every file an Import
names, whatever its extension or condition and inside or outside the workspace
(package and custom SDK targets included). Only imports rooted at an MSBuild
toolset location such as $(MSBuildToolsPath) are not read. Changed build files
are also read at the base revision, with their imports followed, so a reference or
import the change deleted still counts. The closure is read once per evaluated
graph and effective build-property set, so prepare and plan in one workflow
process share it. Declared and imported paths must resolve without evaluation:
literals, $(MSBuildThisFileDirectory), or properties whose scanned definitions
resolve. Properties supplied to the build with /p: use their effective global
values, which override scanned fallback definitions.
A task Output whose item type a property names must resolve to another item
type, a skipped conditional SDK import is unprovable, and MSBuild %XX escapes are
decoded before paths are compared. Otherwise every graph project is kept with
solution-entry-reference-unproven, because the hidden referrer may be outside
the solution. An added entry outside the evaluated graph
keeps the solution-member-graph-incomplete fallback. Classic .sln files keep
layout-only detection and otherwise their existing conservative impact.
etp plan --workspace . --base-ref origin/main --head-ref HEAD --output artifacts/test-selection.mpack
etp explain --manifest artifacts/test-selection.mpack --selection 'method:My.Tests.SomeTests.Some_case()'
etp hotspots --manifest artifacts/test-selection.mpack --top 20
etp hotspots --manifest artifacts/native-replay.zip --top 20
etp inspect-replay --archive artifacts/native-replay.zip
etp audit --manifest artifacts/test-selection.mpack --broad-results artifacts/broad.trx
etp prepare --workspace . --changes base-head --base-ref origin/main --head-ref HEAD --output artifacts/etp-prepare.json
etp index --workspace . --analysis-depth balanced --cache-directory .etp/cache --output artifacts/etp-index.json
etp plan --workspace . --base-ref origin/main --head-ref HEAD --analysis-depth balanced --cache-directory .etp/cache --output artifacts/test-selection.json
etp explain --manifest artifacts/test-selection.json --selection-id method:My.Tests.SomeTests.Some_case()
etp partition --manifest artifacts/test-selection.json --partition-count 4 --process-startup-ms 1000 --max-command-fragments 120 --max-method-fragments-per-class 3 --method-selection-class-coverage-threshold 0.6 --max-expanded-class-fragments 60 --max-class-fragments-per-project 12 --class-selection-project-coverage-threshold 0.6 --output artifacts/test-partitions.json
To profile a cold reference-index build while replaying an exact base/head pair:
etp plan --workspace . --base-ref <base-sha> --head-ref <head-sha> --analysis-depth semantic --no-cache --reference-index-profile artifacts/reference-index-profile.json --output artifacts/test-selection.json
etp index accepts the same --reference-index-profile option. The optional
etp.reference-index-profile.v1 JSON contains per-project timings, total
allocation estimates, and the 20 slowest source-tree visits. Status is
computed when the index ran, or identifies the cache/budget/analysis reason
when it did not. With a partial project cache hit, only recomputed projects
appear in Projects; the selection still uses all cached and recomputed
projects. Without the option, etp does not collect or write this profile.
For local CI-shaped iteration, use the repository harness instead of pushing each planner experiment to GitHub:
pwsh -NoProfile -File tools/Eternet.TestPlanner/scripts/Test-CiImpactSelectionLocally.ps1 `
-BaseRef <base-sha> `
-HeadRef <head-sha> `
-PlannerMode local-source `
-PartitionCount 3 `
-SmbRoot .eac/scratch/etp-ci-simulation-smb
The harness reproduces plan, partition accounting, SMB pointer/metadata
preflight, and optional main-only promotion. Use
-PlannerMode published-tool -EtpPath <etp.exe> to compare the installed
remote planner with the local source planner. It writes a receipt that keeps
selection, partition, cache, and optional test evidence separate.
Example benchmark flow:
etp benchmark `
--workspace D:\src\Chat\Eternet.Agents.Control `
--base-ref 47fbb560e6206128f1eac7357ae325e5bae68138 `
--head-ref 45d05abb94d9fb61e7e60df272957972bd09d23e `
--analysis-depth semantic `
--cache-directory .tmp\etp-benchmark-cache `
--output artifacts\etp-benchmark.json
The benchmark uses existing plan and index mediator pipelines. It records
cold plan --no-cache, cold index, warm plan after the semantic-index
cache hit, and a second warm plan after the symbol-impact-graph cache hit.
The manifest includes elapsed time, selected test count, reason-code counts,
selected test identities, graph node/edge counts, cache diagnostics, stage
timings, and equivalence flags. When --cache-directory is supplied, it must
point to an empty directory so the cold-index lane cannot reuse prior state.
balanced is the recommended rollout depth and maps to the candidate-scoped
semantic planner path. semantic remains accepted for compatibility with
earlier dogfood workflows, fast-project avoids Roslyn, and deep enables the
widest semantic closure for shadow validation and corpus evaluation.
Microsoft Testing Platform
Selection and partition output is intended for Microsoft Testing Platform
arguments after dotnet test --. Do not pass these fragments through legacy
VSTest --filter arguments.
Project, class, and method selections produce MTP-friendly fragments such as:
dotnet test tests/Some.Tests/Some.Tests.csproj -c Release -- --filter-class "*SomeTests*"
dotnet test tests/Some.Tests/Some.Tests.csproj -c Release -- --filter-class "*SomeTests*" --filter-method "*Some_case*"
Partition manifests contain command fragments per selected test group. A GitHub
Actions job can run each matrix row by joining the project path, configuration,
and ArgumentsAfterDoubleDash emitted for that row.
etp partition normalizes selections hierarchically. A project fragment removes
class and method fragments for the same project. A class fragment removes method
fragments for the same class. Optional thresholds can widen methods to class or
classes to project when the fine-grained set is no longer cheaper than the
broader scope.
--process-startup-ms assigns the fixed cost paid by every emitted test
command. With complete test inventory, ETP compares selected method/class
execution plus every process startup against the broader class/project total
and coalesces only when the latter is cheaper. This comparison requires observed
durations or an explicit positive DefaultTestDurationMilliseconds in partition
settings for every estimated test. Unitless DefaultWeight remains useful for
balancing work, but is not evidence that a test takes that many milliseconds.
Explicit scope thresholds remain a separate widening policy.
--max-command-fragments defaults to 120; zero disables it. To meet the limit,
ETP batches exact class filters, or method filters belonging to the same class,
without adding tests. Batches preserve framework, module, execution strategy,
exclusive group and build requirements, and bound filter command-line length.
If exact batching cannot meet the limit, partitioning fails with an actionable
error instead of widening to whole projects. The partition
receipt reports source, emitted, and coalesced fragment counts and keeps
historical duration, explicit default weights, and process startup cost
separate so no weight can disappear during coalescing.
When --expand-project-fragments-to-classes is enabled, ETP can split a
whole-project fragment into class processes, but only for projects listed in
ProcessIsolatedSafeProjects whose inventory is complete and declares no shared
collection. ETP keeps a global budget before expanding project fragments.
--max-expanded-class-fragments defaults to 60; if all planned expansions would
exceed that budget, project fragments remain project-scoped so CI runs each
affected project once instead of starting hundreds of class-scoped dotnet test
processes.
With observed durations for the project or all of its classes, the split is
driven by makespan. ETP computes a lower bound on the wall time of the whole
fragment set: host-exclusive fragments run alone, fragments sharing an exclusive
group run one at a time, and the rest share ExecutionSlotCount slots (partition
settings; zero means the partition count). A project that fits under that
target stays whole with class-sharding-within-target-makespan. A longer project
is packed longest-processing-time-first into the fewest processes, up to
ExpandedClassProcessCount, whose longest one fits the target. Every process pays
--process-startup-ms again. When no count fits, a process is added only while
it saves more than its startup (class-sharding-no-makespan-gain otherwise).
Class durations only set each class's share of the observed project run, because
they can be sums of tests that ran in parallel. Without duration evidence, the
configured ExpandedClassProcessCount still applies. A project that needs the
host alone (class-sharding-exclusive-host) or belongs to an exclusive group
(class-sharding-serialized-by-exclusive-group) is never split, because its
shards would still run one at a time. Set ExecutionSlotCount to the executor's
--max-parallelism when every partition runs from one fragment pool. The
partition summary reports the bound and what sets it, for example
model lower bound with current duration weights over 6 slots: host-exclusive 259900 + shared 982200 = 1242100 (bound by exclusive-group:plugin-global-store).
The bound belongs to the model: it is computed from the current duration
weights, not measured, so it is neither a floor nor a target for real wall time.
A run can finish below it when weights overestimate (EAC run 36273306642: bound
188.6 s, observed 181.6 s) or far above it when they underestimate.
etp ci plan appends these partition lines to its summary, and
workflow affected-tests finalize reports, under ## Partition estimate, the
command cost of the partition that ran and its bound over the slots the fragment
pool actually used, next to the observed test execution time. It does so only
when one successful run executed the whole partition manifest. When any fragment
has only unitless default weights, the section says those values are not
wall-clock predictions.
The model treats slots as independent: a fragment takes as long on a busy slot
as on an idle one. On EAC's CI hosts (20 logical processors, six slots, xUnit
--max-threads 1) the data supports that; see
partition-replay-validation.md. On a host whose
processors are already saturated, lower ExecutionSlotCount (and the executor's
--max-parallelism) to the parallelism the host can actually sustain.
Deferred affected-tests publication
workflow affected-tests finalize --defer-publication writes the normal certified receipt, summary, and savings,
then writes publication-intent.json (etp.publication-intent.v1). workflow affected-tests run accepts the same
flag. Without it, both commands write exactly what they wrote before and no intent. The intent binds the
certification receipt hash and the small manifests each item consumes (hashes taken from the receipt's own
artifact list, plus any snapshot-context.json); finalize does not read large evidence such as test/** or
binlogs. Its runRoot is always .: the intent describes the directory that contains it, so a run root can be
transported. A new intent rotates a publication-receipt.json bound to an older intent to
publication-receipt.<first 12 hex of the old intent hash>.json.
workflow affected-tests publish --run-root <dir> holds an exclusive publication.lock in the run root for the
whole invocation and fails fast when another publisher holds it. It reads the certification receipt once and
checks its hash and status against the intent; a mismatch fails every selected item. A run root without an intent
is rejected: hosts keep their existing always-run evidence staging for runs that never reached a deferred
finalize, and publish never archives an uncertified run. Each outcome is appended to publication-receipt.json
(etp.publication-receipt.v1), bound to the intent hash, and written after every item. An item is settled by its
latest decisive attempt: a skip caused by a missing host input cannot hide an earlier failure, while a
certification skip (certification-not-green, snapshot-not-certified-complete) can. The exit code is 0 only
when every item is settled as published, already-present, or skipped-precondition. An unexpected error in
one item records failed with its message, and later items still run. A build-outputs attempt that publishes some entries but skips a row on an I/O failure, or fails to replicate to the shared tier, records published-incomplete (not success); the next publish runs the idempotent publisher again for that item.
Items run in this order; --items <id> selects a subset:
| Item | Preconditions | Behavior |
|---|---|---|
green-tree |
green | Needs --green-tree-root and --green-tree-trusted-store-root (missing: failed). Missing or non-etp.green-tree-seal-preparation.v1 seal-preparation.json: skipped-precondition. The prepared record must name the receipt's tested head and tree and, when frozen, the same native receipt hash; then GreenTreeWorkflowPublisher publishes it. |
checkpoint |
green | Needs --repository, --pull-request-number, --fallback-target-sha, --graph-fingerprint; --base-snapshot-sha defaults to the certified head. Prepares locally below --checkpoint-staging-root (default <run-root>/publication/checkpoint/), reading the existing pointer and objects from --build-impact-store-root. Writes the full preparation to <staging>/preparation.json and <run-root>/checkpoint-publication.json, and records its hash and an identity hash in the receipt. no-op is already-present with the preparer reason. A rerun resumes without restaging only for the same identity and an unchanged staged preparation. |
snapshot |
green, complete-snapshot | Runs only when the receipt certifies a complete snapshot (snapshot-completion-validated, not a checkpoint reseed); otherwise skipped-precondition. Needs --snapshot-store-root, --repository, --snapshot-input-fingerprint, --etp-version. Scope comes from --snapshot-scope (default repository); the latest pointer moves only with --snapshot-update-pointer, which also requires --snapshot-green-branch (the protected branch the CI promotion verifier proves; this run root is the terminal-green run) and otherwise fails with snapshot-pointer-update-requires-green-promotion. Writes <run-root>/snapshot-publication.json in the standalone command's --output shape. |
build-outputs |
green | Publishes certified, reusable build outputs when reuse is enabled; otherwise reports skipped-precondition. |
evidence-archive |
none | Last. Needs --evidence-archive <file>. |
The evidence archive has the entry set of EAC's "Stage canonical replay evidence outside RAM-disk mount" step:
top-level affected-tests-*.json, summary.md, snapshot-publication.json, checkpoint-publication.json,
inputs/*, plan/*, build/*.json, build/*.mpack, durations/*, build/*/logs/*.binlog,
build/*/*/logs/*.binlog, and recursive test/, build/impact-index/, build/snapshot-impact-index/,
inputs/build-impact-base/. Hidden and system files are skipped as Get-ChildItem does without -Force;
symbolic links are rejected. --checkpoint-resolution <file> adds checkpoint/resolution.json and
--green-tree-root <dir> adds green-tree/**. Each file is read once: it streams into the ZIP while its SHA-256
is computed, compared with the intent's bound hash (durations are not bound because a host reseed may rewrite
them), and recorded in the receipt entries; the item sha256 digests that entry list. An existing destination is
already-present only when its entry names, lengths, and CRC-32 values equal the freshly streamed archive;
otherwise the item fails.
EAC adoption notes:
- Keep the existing standalone publication steps during rollout. After archive and checkpoint parity is shown,
report status right after
finalize --defer-publication, then runpublish. - The checkpoint transport keeps the SMB copy and consumes
<staging>/preparation.jsoninstead of preparing again. It must compare-and-swap the store pointer against that file'sexistingPointerSha256, and whenbaseSnapshotShaequals the tested head it must still publish the PR-scoped snapshot (--scope pull-request-<n>) before exposing the pointer, exactly asPublish-PrBuildImpactCheckpoint.ps1does. - Native replay publishes snapshots without
--snapshot-update-pointer; only a green protected-branch lane publication passes it. - Keep the existing green-tree finalize step before
publishso its seal preparation is available, and finish any run-root mutation other than a duration reseed beforefinalize --defer-publication. - Runs without an intent (for example, a failure before finalize) keep the legacy always-run evidence staging.
GitHub Actions
Use etp plan once, upload or keep the manifest as a workflow artifact, then
use etp partition to create the execution matrix. The planner only selects
tests; retry, timeout, parallel execution, and full-suite safety nets remain
workflow policy.
Use a single configured cache directory for the workflow. .etp/cache is the
recommended local directory because it is ignored by normal source control
conventions and can also be reused by local agent loops. Shared CI cache state
belongs on the authorized SMB share, not in GitHub Actions cache storage.
Cache correctness is never required for safety: when an SMB pointer is
missing, stale, unavailable, or schema-incompatible, etp plan --cache-directory ... falls back to the cold semantic path and records cache
diagnostics in the manifest.
Repeated complete builds of the same commit can produce different receipt bytes because receipts retain
execution paths and diff provenance. For scheduled rebuilds, use
build-impact-snapshot publication publish --require-complete-workspace --reuse-existing-snapshot to retain
the first certified snapshot for the exact repository, commit, ETP version, scope and input fingerprint.
ETP verifies the stored archive and content hashes, metadata proof, carrier count and complete coverage
against the current workspace before returning already-published with reason existing-object-validated.
It never overwrites an existing snapshot. Without this opt-in, different snapshot bytes remain a rejection.
The recommended CI shape is:
- Compute the evaluated-input fingerprint locally and read only the small
latest.txtpointer and snapshot metadata from SMB. - Copy the SMB payload only when the pointer, schema, repository, tool version, cache directory, and fingerprint all match.
- On a successful
pushtomain, stage and promote the local cache to the immutable SMB snapshot. Pull requests and failed runs never promote. - Run
etp plan --cache-directory .etp/cacheand let SMB misses fall through to cold planning. - Upload the selection, partition, cache diagnostics, and index manifests as artifacts so misses and timings can be reviewed.
- name: Resolve compatible etp cache from SMB
shell: pwsh
run: |
# Read latest.txt and snapshot-metadata.json first.
# Copy .etp/cache only after all compatibility checks pass.
# Missing or incompatible state means cold planning.
- name: Warm etp semantic index
if: github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
shell: pwsh
run: |
etp index `
--workspace . `
--analysis-depth balanced `
--cache-directory .etp/cache `
--output artifacts/etp-index.json
- name: Plan affected tests
if: github.event_name == 'pull_request'
shell: pwsh
run: |
etp plan `
--workspace . `
--base-ref ${{ github.event.pull_request.base.sha }} `
--head-ref ${{ github.sha }} `
--analysis-depth balanced `
--cache-directory .etp/cache `
--max-selected-methods-before-fallback 120 `
--max-selected-classes-before-fallback 40 `
--max-selected-method-coverage-before-fallback 0.6 `
--max-selected-class-coverage-before-fallback 0.6 `
--output artifacts/test-selection.json
- name: Partition affected tests
shell: pwsh
run: |
etp partition `
--manifest artifacts/test-selection.json `
--partition-count 4 `
--process-startup-ms 1000 `
--max-command-fragments 120 `
--max-method-fragments-per-class 3 `
--method-selection-class-coverage-threshold 0.6 `
--max-class-fragments-per-project 12 `
--class-selection-project-coverage-threshold 0.6 `
--output artifacts/test-partitions.json
etp partition --manifest selection.json --partition-count 4 --output partitions.json
reads an existing selection manifest only. It does not call git, MSBuild, or
Roslyn. The output contains structured arguments intended to be passed after
dotnet test --, partition weights, exclusive grouping, a GitHub Actions
matrix, and a compact text summary.
For local EAC or Codex wave loops, keep the cache inside the repo workspace and reuse it between waves:
$cache = Join-Path (Get-Location) '.etp/cache'
etp index --workspace . --analysis-depth balanced --cache-directory $cache --output artifacts/etp-index.json
etp plan --workspace . --base-ref origin/main --head-ref HEAD --analysis-depth balanced --cache-directory $cache --output artifacts/test-selection.json
Do not delete .etp/cache between waves unless validating cold behavior. A warm
cache should keep repeated balanced or semantic plan calls close to the P5
dogfood warm-plan range of about 5.6-6.0 seconds after the expensive index entry
has been written. If the cache is empty, incompatible, or incomplete, the same
command remains safe and pays cold planning cost instead.
etp plan also has project-impact guardrails enabled by default so massive
diffs can skip Roslyn and widen immediately to candidate test projects. The
default gate requires two independent signals before it cuts over, preferring
relationship fan-out over raw bulk:
- more than 150 changed files;
- more than 60 changed project graph input files;
- at least 25% of project graph input files changed;
- more than 20 affected projects;
- more than 10 candidate test projects;
- more than 16 affected-project to candidate-test-project graph edges;
- more than 5 candidate test projects triggered by any one affected project.
Project graph input files come from evaluated MSBuild inputs, imports, and
project files, so docs-only churn does not count as runtime impact unless a repo
models those files as project inputs. Repositories can tune the gate without
changing workflow code by passing overrides such as
--max-project-input-files-before-project-fallback,
--max-project-input-file-ratio-before-project-fallback,
--max-affected-projects-before-project-fallback,
--max-candidate-test-projects-before-project-fallback, and
--max-project-test-impact-edges-before-project-fallback,
--max-project-test-impact-edge-ratio-before-project-fallback,
--max-tests-per-affected-project-before-project-fallback, and
--project-impact-fallback-signal-threshold. A threshold of 1 makes any one
configured signal enough to widen; the default 2 avoids overreacting on small
repositories.
Optional --settings partition.json can configure weights by project, class
glob, trait, or fallback reason, plus exclusive groups for tests that must stay
together. An exclusive group can additionally set RequiresExclusiveHost when
its fragments must not overlap any other test fragment on the runner. ETP runs
those fragments first and then restores normal bounded parallelism. Because
host-exclusive fragments never overlap anything, ETP merges the exact class or
method filters of one test module into a single process (coalescing reason
exclusive-host-single-process) so expensive fixtures such as containers or an
Aspire AppHost start once instead of once per class; set
"CoalesceExclusiveHostFragments": false to keep one process per fragment:
{
"ExclusiveGroups": [
{
"Name": "container-runtime",
"Project": "tests/Sample.Web.Tests/Sample.Web.Tests.csproj",
"RequiresExclusiveHost": true
}
]
}
Settings can also assign an execution strategy per project. The
default strategy is prebuilt-no-build, which expects the workflow to restore
previously built outputs and execute the MTP module directly with
dotnet <test-module.dll> --minimum-expected-tests 1. This avoids the VSTest
compatibility path where zero discovered tests can exit successfully. Use
build-before-test for projects that need a local build in the test job before
the direct test-module invocation, such as projects with generated runtime
metadata or host assets:
{
"ExecutionStrategies": [
{
"Project": "src/Sample.Web.Tests/Sample.Web.Tests.csproj",
"Strategy": "build-before-test"
}
]
}
Use mtpArguments for stable runner options that must be passed to every
fragment. ETP appends them after the selection filters it owns:
{
"mtpArguments": ["--max-threads", "4"]
}
History And Audit
Optional --history history.trx or --history history.json supplies local
historical signals for partition quality. TRX input contributes test method
duration and recent failure markers from UnitTestResult rows. Compact JSON
uses the etp.history.v1 shape:
{
"SchemaVersion": "etp.history.v1",
"Tests": [
{
"FullyQualifiedName": "Sample.Tests.SomeTests.Case()",
"ClassName": "Sample.Tests.SomeTests",
"ProjectPath": "tests/Sample.Tests/Sample.Tests.csproj",
"DurationMilliseconds": 2500,
"RecentFailure": true,
"Flaky": false,
"ExclusiveGroup": "database",
"LastObservedAt": "2026-06-12T10:15:00Z",
"Source": "nightly"
}
]
}
History never removes a test required by correctness evidence. Method history
wins first, then class duration, then project duration, then configured weights
and default weight for partitioning. Restore/build history participates in the
economic choice between semantic analysis and conservative project execution;
project-scoped duration matching requires an explicit ProjectPath and never
infers ownership from historical result-directory names.
missing or corrupt history keeps the fail-closed selection and falls back to
configured partition weights. Set "PrioritizeRecentFailures": true in
partition settings to order recent failures first inside the deterministic
partition output.
etp audit is the safety loop for rollout. It should be used in shadow mode to
compare narrow selections against broader result evidence before a repository
starts enforcing narrower gates.
Selection Reasons
Every selection carries reason fields so reviewers can distinguish candidate scope, selection evidence, fallback, widening, and confidence. Important reason families include:
ChangedSymbolReferencedByTest: a test method directly references a changed symbol.ChangedMemberReachedThroughCaller: a changed member is reached through a production caller that a test references.ChangedTestMethodSourceorChangedTestClassSource: the test source itself changed.ChangedCliCommandContractUsedByTest: a changed CLI command declaration and a test share a deterministic command contract such ascontracts/scaffold.ChangedMediatorStepSelectedPipelineTests: a changed Eternet.Mediator step, request, handler, or response contract maps to direct step or owning pipeline tests.ChangedMediatorPolicySelectedStepTests: a changed step execution policy maps to tests for the managed step or owning pipeline.ChangedMediatorRetryProviderSelectedPipelineTests: a changed retry provider maps to retry behavior tests for the owning step or pipeline.ChangedMediatorValidatorSelectedPipelineTests: a changed validator maps to validation path tests for the owning step or pipeline.ChangedMediatorBranchStepSelectedBranchTests: a changed branch step maps to branch-specific tests only when both graph metadata and test evidence identify the same branch path.ChangedMediatorPureHelperSelectedPipelineTests: a changed synchronous source-owned pure helper maps through caller generated steps to direct step tests and owning pipeline tests.ChangedMediatorInfrastructureAdapterSelectedPipelineTests: a changed narrow source-owned adapter maps to consuming generated steps and owning pipelines.ChangedMediatorWorkflowDependencySelectedPipelineTests: a changed source-owned workflow dependency maps to consuming pipelines when the dependency fan-out stays within the conservative Mediator budget.GeneratePipelineTestBaseAnchoredPipelineSelection: a generated pipeline test base anchors an owning pipeline test class.MediatorImpactGraphFallback: Mediator graph facts identify impact, but no method or class test evidence is safe enough to prove a narrow selection.MediatorImpactGraphBudgetFallback: helper or dependency graph facts identify consuming pipelines, but the dependency is shared across unrelated source-path feature areas or exceeds the same-feature budget, so candidate test projects are selected instead.central-package-consumer: project is selected because its evaluated package references restore a modified central package entry.central-package-no-op: central package file changed in git diff but contains no package entry modifications.central-package-entry-impact: the central package file changed only throughPackageVersion/GlobalPackageReferenceentries, so impact is limited to each changed package's consumers and their project-reference dependents. The plan summary lists every package with its change and consumers underCentralPackageChanges. With a declared build configuration (--msbuild-property,workflow affected-tests) consumers come from the head graph evaluated as a normal build with the build's global properties; otherwise from the planner's graph.central-package-property-value-unproven: an unchanged entry reads a property that is neither a build global property nor a literal defined earlier in the same plain file, so that package keeps its consumers.central-package-build-graph-dependent: a dependent reached only through a project reference the build's properties enable.central-package-transitive-pinning: central transitive pinning may be enabled, so every project in the file's scope consumes each changed package.central-package-change-unproven: the change is not attributable to package entries (malformed XML, imports, properties, project attributes, unknown elements, entries outside top-level item groups, non-literal ids) or the consumer evidence is unproven (build evaluation failed, a package reference or import reads a property only the build stage injects, a package reference in a target); every project in the file's scope is kept.analyzer-or-source-generator-package: modified central package is an analyzer or source generator, affecting C# compilation and generated code for consuming projects.project-impact-budget-fallback-candidate-scope: the diff is broad enough on project graph metrics that semantic analysis would likely cost more than the saved test time, so candidate test projects are selected before Roslyn loads.UnresolvedSemanticMappingFallback: semantic analysis found changed declarations but no narrow test mapping.semantic-fallback-candidate-scope: the final selection widened to candidate test projects because narrower evidence was unsafe or unavailable.
Fallbacks are not failures. They are explicit evidence that the tool chose a safe wider test set instead of silently skipping uncertain tests.
Cache
etp can persist expensive analysis outputs under .etp/cache when a cache
directory is configured or cache is required. Cache entries are versioned by
schema, tool identity, analysis depth, scope, inputs, and stage. The current
cache schema is invalidated when serialized analysis models change.
Entries use the current MessagePack schema only. JSON cache entries are not
read or written and should be removed from shared cache stores.
Cache is an optimization only. A cache miss should produce the same selection as a fresh run.
etp index is the cache warm command for CI. It computes the workspace semantic
index once and writes the semantic-index cache entry. A later etp plan with
the same --analysis-depth and --cache-directory can use that entry for diff
plus lookup instead of loading Roslyn compilations again. The cache should be
kept even when planner or partition thresholds widen execution to project scope;
the next run may still benefit from the warmed inventory and reference index.
Current-only persistence migration
ETP does not dual-write or probe obsolete persistence representations. During an
upgrade, delete JSON analysis-cache entries and regenerate EAC v1 or loose
build-impact archives with the current producer. Duration baselines must contain
an explicit ProjectPath; historical directory-name slugs are not treated as
project authority. MessagePack remains the single payload for analysis caches
and duration baselines, deterministic ZIP remains the transport for fragment
trees, and JSON is reserved for small versioned manifests and receipts.
Pipeline Architecture
The CLI uses System.CommandLine only for parsing and console dispatch. Command
orchestration is implemented as Eternet.Mediator generated pipelines:
PlanTestSelection: discover changed files, build the MSBuild graph, resolve owning projects and candidate test projects, decide semantic scope, load narrowed Roslyn compilations, discover declarations and test inventory, build the symbol and structural impact graph, then write the manifest.ExplainTestSelection: load an existing manifest, find a selected test, and render the recorded reason path without rerunning repository analysis.PartitionTestSelectionManifest: load an existing manifest, load optional partition settings and history, assign deterministic weighted partitions, build MTP command fragments, then write the partition manifest.AuditTestSelectionandEvaluateTestPlannerCorpus: provide rollout evidence, regression checks, and corpus-level comparison for precision hardening.
Cheap gates run before expensive work: git diff and MSBuild ownership first, candidate test scope second, and Roslyn plus structural analysis only for the narrowed candidate closure.
Rollout Guidance
Start with etp plan in shadow mode and keep the existing broad suite. Review
manifests for selection mode, reason codes, selected scope, and missing pattern
families. Use etp audit against broad TRX evidence before making narrow
selection required.
Good rollout signs:
- Most ordinary source diffs select method or class scopes.
- Project-scope fallbacks are explainable and tied to broad-impact inputs or missing known patterns.
- MTP fragments run successfully in the target runner.
- Nightly or release broad suites remain available.
- New Eternet patterns are added as deterministic, tested extractors before relying on AI or manual judgment.
For now, Eternet.Agents.Control is the primary dogfood repository. Its CLI
command tests are the first structural-contract use case: changes to
ContractsCommandModule can now select ContractsCliTests methods by the
contracts/scaffold contract instead of widening to the whole CLI test project.
For an opt-in, portable observation contract that distinguishes an actionable planner regression from justified cold or conservative work, see dogfooding. ETP emits evidence only; EAC or another orchestrator owns observation ledgers, thresholds, repair checkpoints, and resume.
Fragment environment and partition settings
Every test-running child process, including a prebuilt module from an external module
root, the dotnet test fallback and the legacy runner capability probe, starts with its
working directory set to the absolute workspace. ETP_WORKSPACE contains that same
path and is reserved. ETP does not set product-specific workspace variables. A product
can map one through partition settings:
{
"TestEnvironmentVariables": {
"EAC_REPOSITORY_ROOT": "{workspace}"
}
}
Or pass a repeatable --test-environment NAME=VALUE to
workflow affected-tests test|run, execute-test-partitions, or ci lane-test.
The literal {workspace} token expands to ETP_WORKSPACE. Values apply in this order and
the last one wins: the environment already recorded in the command fragment, the resolved
settings, then the explicit --test-environment mappings. ETP_WORKSPACE cannot be
overridden from any source. Names must match [A-Za-z_][A-Za-z0-9_]* (at most 256
characters), are unique ignoring case, and values are at most 32,767 characters without
NUL; violations fail before any work and never echo the value. The execution receipt
lists the CLI override names only; the values are never written to receipts, events or
summaries, but they are visible in the process arguments of the ETP invocation, so pass
secrets through the environment of the test host instead. Settings entries are persisted
in command fragments, so keep secrets out of partition settings. Unspecified variables
retain inherited values. ArgumentsAfterDoubleDash remains the MTP command contract.
Partition settings resolve in order:
- an explicit
--settings(etp partition) or--partition-settings(execute-test-partitions,workflow affected-tests plan|test|run,ci plan); - the settings source already recorded with the workflow state or the partition manifest;
ETP_PARTITION_SETTINGS(a relative value resolves against the workspace; a value that points to a missing file only warns and discovery continues);<workspace>/.etp/partition-settings.json(discovered);- none.
The workspace is --workspace or the current directory; a workspace path recorded inside a
transported manifest never decides where settings are read from. An explicit relative
--settings resolves against the current directory in etp partition and
execute-test-partitions, and against the workspace in the workflow and ci commands, as
before discovery existed. The manifest (PartitionSettingsSource) and the workflow state
record the original source kind, its workspace-relative path when it is inside the
workspace, and the SHA-256 of the exact bytes read. Manifests stay byte-identical when no
settings exist: the property is written only when settings were used or explicitly
disabled, and the workflow state carries partition-settings-not-found in its reason
codes instead.
A discovered file (environment variable or .etp/partition-settings.json) contributes only
exclusiveGroups and testEnvironmentVariables; every other key, such as configuration,
mtpArguments, weights or coalescing, is ignored so that discovery can never change the
selected tests. Pass such a file with --settings/--partition-settings to use all of its
keys. A link at the default path that resolves outside the workspace is ignored with the
reason partition-settings-reparse-point. Settings files larger than 8 MB, malformed
files and a directory at the default path fail with the source and path in the message.
--no-partition-settings disables every source, is recorded as an explicit opt-out
(partition-settings-disabled) and wins over --partition-settings with a warning. At execution
it also strips the environment variables a partition-time settings file baked into the manifest's
fragments (the capability probe and the test process then see neither them nor anything else the
settings supplied; --test-environment and ETP_WORKSPACE are unaffected), and the receipt
carries no-partition-settings-stripped-baked-environment when it removed any. Only an
explicit source given at execution time fails when unreadable. A recorded source is best
effort at execution: a missing or edited file produces a warning event and the receipt
reason recorded-partition-settings-missing or recorded-partition-settings-changed, and
the run continues with the settings already baked into the fragments. The fragment watchdog is
not baked into the fragments, so the partition manifest records the source's fragmentWatchdog
policy (an empty policy when the file has no such block) in partitionSettingsSource, and the
recorded policy still applies when the file is missing or edited. A manifest written before the
policy was recorded cannot say what the file configured: the default watchdog applies and the
receipt carries recorded-partition-settings-watchdog-unavailable with a warning event. A
fragmentWatchdog in the settings an execution resolves applies to etp execute-test-partitions
as well as to the workflow test stage. A manifest that
records no settings source (written before provenance existed, or partitioned with no
settings at all) receives the exclusive groups discovered at execution, but only the
fragments a rule matches by selection id, project or class glob run alone; unrelated groups
never serialize the run. Existing CI commands that pass explicit settings keep their
precedence.
Failed-test details
When a test fragment fails, ETP reads its TRX reports and emits test-failure
events with the test name, outcome, assertion message and stack trace. These
fields are also retained in the partition receipt. Enable the runner's TRX
reporter to obtain these details; ETP does not require optional reporters for
otherwise valid test projects. Missing or malformed reports preserve the
original failure and emit a failure-evidence diagnostic with stdout/stderr
paths. Parsing and emitted details are bounded, with truncation reported
explicitly; original reports and logs remain available for full diagnostics.
Project boundaries and partition policy
Compiler impact from a changed declaration is bounded by its evaluated reverse
project-reference closure when ownership and evidence are complete. Shared
interface dispatch (for example IDisposable.Dispose) cannot make a helper
select tests in unrelated projects. Unknown ownership remains conservative.
Explicit interface methods reconcile compiler evidence against source-mapped IL
using the declaring type and qualified member identity.
ci plan honors the partition policy's MaxClassFragmentsPerProject; it no
longer supplies an implicit 150-class threshold that widens to a whole project.
Without an explicit widening policy, command limits use exact filter batching
and fail clearly if they cannot be satisfied without running additional tests.
Proven local interface receivers
Compiler carriers can include optional receiverDispatches on methods and tests.
Each entry names an interface contract, its concrete targets and
allSitesComplete: true. This proof is emitted only when every observed call to
that contract in the owner has a bounded, known receiver. Local object creation,
ordinary assignments, conversions, conditional/coalescing expressions and
synchronous/asynchronous using disposal are supported. Assignments are unioned
across the whole method, so later assignments can widen earlier calls.
Unknown factories/parameters, closures, reference escapes, unsupported receiver expressions and open virtual implementations preserve conservative dispatch. The carrier retains raw contract references for older readers. A new reader uses validated proof to replace only the matching source-owned dispatch edges, including explicitly mapped state machines, with concrete targets and a separate contract-change dependency. Unrelated IL edges remain intact; missing or ambiguous metadata does not authorize narrowing. Optional proof participates in contribution fingerprints and survives delta/checkpoint transport.
Existing archived carriers without receiver proof remain conservative. Measuring this improvement on a repository therefore requires fresh compiler evidence; replaying old evidence cannot establish its reduction.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.125 | 41 | 10/4/2026 |
| 1.1.124 | 40 | 10/4/2026 |
| 1.1.123 | 194 | 10/4/2026 |
| 1.1.122 | 45 | 10/4/2026 |
| 1.1.121 | 48 | 10/3/2026 |
| 1.1.120 | 130 | 10/2/2026 |
| 1.1.119 | 66 | 10/2/2026 |
| 1.1.118 | 86 | 10/1/2026 |
| 1.1.117 | 70 | 10/1/2026 |
| 1.1.116 | 77 | 10/1/2026 |
| 1.1.115 | 90 | 10/1/2026 |
| 1.1.114 | 77 | 10/1/2026 |
| 1.1.113 | 95 | 9/29/2026 |
| 1.1.112 | 203 | 9/28/2026 |
| 1.1.111 | 96 | 9/28/2026 |
| 1.1.110 | 111 | 9/27/2026 |
| 1.1.109 | 100 | 9/27/2026 |
| 1.1.108 | 153 | 9/26/2026 |
| 1.1.107 | 107 | 9/26/2026 |
| 1.1.106 | 95 | 9/26/2026 |