Skip to content

Fourth cross-app spelling: the C# stage needs a root literal as Path.Combine's first argument, so a specific-file read with a variable base is invisible — and four in one night is the finding #3082

Closed

Description

@erikdarlingdata

The cross-app guard's C# stage has now produced four distinct spellings it could not see, in one night, each found while fixing the previous one. This issue records the fourth — and argues that four is enough evidence to ask whether the shape is wrong rather than widening it a fifth time.

The fourth spelling

The C# stage's second matcher requires the root literal to be the first Path.Combine argument. A cross-app read of a specific file whose base directory sits in a variable is therefore invisible. Live instances, all confirmed absent from the found set:

  • Lite/Database/Schema.cs — CollectorStateContractTests:84
  • RemoteCollectorService.DefinitionRunner.cs and .QueryStoreBackfill.cs — CollectorStateContractTests:130, QueryStoreStatePruneTests:187,228,230
  • FactCollectorFailureReportingTests:174
  • StoreSqlClockDisciplineTests:619
  • FileGrowthAlertStoreTests:134 — which uses a segment-list helper with no Path.Combine at all

This is the most consequential of the four so far, because these name specific files. A path filter entry could genuinely be written for them, which is not true of a whole-tree enumeration.

Nothing is unguarded today. All of these live under Lite/**, which the darling filter reaches, so the suites run. As with the previous three, what is missing is the guard's ability to know that — and its failure direction is the same one: it fails toward not noticing, silently.

It is already pinned as a recorded limitation in TheCSharpMatchers_StillCannotSeeTheseSpellings (PR #3079), so this gap is written down in code rather than implied. Closing this issue means promoting a limitation that is currently honest.

The four, and why the pattern is the finding

# issue what the stage could not see cause
1 #3063 a read expressed as an MSBuild Compile Include population — project XML was never opened
2 #3067 Lite.Tests/… from Darling.Tests anchor breadth — the app dir cannot see a sibling test tree
3 #3076 a bare directory name, no separator anchor shape — a directory is not a path to the matcher
4 this a specific file with the base in a variable argument position — the root must be Path.Combine's first argument

Each fix was correct and each revealed the next. That is the signature of re-implementing a language rather than bounding a problem — and it is precisely the argument #3074 makes about the project-file half:

There is no bottom to it, because the guard is re-implementing a language.

#3074's answer for project files was to stop parsing and let MSBuild evaluate (dotnet msbuild -getItem). The C# half has no equivalent yet, and this issue is the evidence that it needs one. A regex over source text asking "which cross-app files does this code read" is the same unbounded problem in a different language.

What to decide, rather than what to build

Do not simply widen the matcher again. The measurement from #3076 is the reason: bare-root tokens number 37 on one arm and 20 on the other, the on-disk filter discriminates nothing (all 57 survive, since a root is a real directory by construction), and only a used-as-a-directory shape cut it to 2. Precision here is bought with structure, not with a looser pattern, and each loosening has to be paid for again.

Three options, none obviously right:

  1. Semantic analysis over the C# half — a Roslyn-based pass that resolves string-valued expressions reaching a file API, the way -getItem resolves item paths. Highest precision; a genuinely larger dependency and the first such in this suite.
  2. Accept the boundary and state it. The backstop already runs both suites, so no reference is unguarded; the guard's remaining job is to report what it cannot see. That is what the recorded-limitation pins now do. This option makes that permanent and stops the widening cycle — and is the honest reading if the answer to "will there be a fifth spelling?" is yes.
  3. Invert the guard. Instead of finding cross-app reads and checking filter reachability, assert that every file a test actually opens at runtime is filter-reachable — which sidesteps spelling entirely, at the cost of needing the tests to run to know.

Requirements if anything is built

Provenance

Found by the #3076 lane (PR #3079) after it took the measurement this issue quotes, using the shipped matcher and filter code by reflection rather than a retyped copy. Instances verified absent from the found set rather than assumed.

Activity

  1. erikdarlingdata commented on Sep 6, 2026

    @erikdarlingdata
    OwnerAuthor

    Claude posting for Erik Darling

    Resolved by #3084, merged to dev as f4891e9ed23. Closing by hand — a closing keyword does not fire on a dev-targeted PR here.

    Decision: accept the boundary. The matcher was not widened, and the widening was built and run before being rejected rather than argued away:

    • It reaches 3 of 7 shapes and is wrong on one. The helper-boundary shape resolves to a bare Lite — the whole tree, for a read naming one directory — because the trailing segment is a variable. That is the trade CrossAppGuardCiGateTests cannot see a cross-app read spelled as a bare directory name, and there is a live one #3076 already refused.
    • The other four compose across members (Path.Combine([RepoRoot(), .. dir, name]) is the live sink). Data-flow analysis, not a wider pattern.
    • On the live tree it turns the build red with a false positive. Lite.Tests/ParitySource.cs locates the repo root via Directory.Exists(Path.Combine(dir.FullName, "Darling")), and the widened arm reports that as a whole-tree read of Darling — on the Lite arm, which has no exemption list and no backstop job. The only exits are Darling/** in the lite filter or another carve-out.

    And the answer to this issue's own question — is a fifth spelling likely — is that there are already seven, and four were unrecorded. Measured by driving the shipped matcher through reflection, blanking one root-token site at a time and diffing the found set, then reading every unfound site individually. Those four were invisible to all three PRs that fixed the first three spellings.

    Option 3 (invert the guard) rejected on measurement: it is circular — a Darling.Storage-only change fires neither filter, so the runtime observation never runs on the change that broke reachability — and instrumenting the reads means ~1,040 BCL file-API call sites.

    Option 1 (Roslyn) is technically the only thing that reaches the remaining four. The dependency was deliberately not added; it is recorded as a standing option, and the seven-shape census is its acceptance criteria if it is ever taken up.

    Nothing is unguarded: every instance lives under Lite/**, which the darling filter reaches. The boundary is now recorded as a named seven-shape census asserted by equality against empty, plus a CONTRIBUTING.md note — because a bound accepted but undocumented is how the next four unrecorded shapes get written.

    Three false doc claims in #3079 are corrected here: the Lite-arm bare-token count is 21, not 20 (and was 21 at #3079's own merge commit), and "every one of its 20 is a label, an assertion key or one of this class's own root constants" is false for 13 of 21 — ten are first-argument Path.Combine reads of real files and three name a tree in an invisible spelling. The replacement is an accurate enumeration plus why the variety is irrelevant to what the pin asserts, rather than a corrected numeral attached to a claim that was also wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions