Five instruments that assert something other than what they check (#3211, #3204, #3217) - #3226
Conversation
…s all of it (#3211) ComposeCompiler maintains the correspondence between appended SQL text and positionally-allocated parameters by hand at each site, and nothing checked it. A site that binds a value it already bound gets a fresh ordinal and shifts every later ordinal by one — a mutation that survived 273 tests once two per-site assertions were removed together. #3211 proposed closing it with placeholder coverage, reasoning that a duplicate bind leaves an ordinal bound and uncited because the SQL still cites the original. Applying the mutation to the compiler shows that holds for only one of the two spellings. When the second bind's placeholder is USED by the site that prompted it — the spelling a contributor writes, since they added the bind in order to use it — the statement binds four parameters and cites $1 through $4, so coverage and any max-based comparison are both satisfied. The check that sees both spellings is a parameter count derived from the plan: two for the window, a server scope, one per filter, one for a ranked mode's topN. Coverage stays alongside it, because it catches a bind whose placeholder never reached the text at all, which no count can distinguish from a correct statement. Both are swept over a corpus derived from the catalog, with every measure, every annotation source and every panel mode required to be represented rather than a total asserted; and the prediction's own reach is pinned against the ParamList call sites in the compiler, so a new binding site reds where the decision belongs.
… never clocks it (#3204) The value does not change. What changes is the relationship enforced for it, because the old one held on neither side. SweepWatchdogSeconds is fed elapsed times stamped when a per-server collection body launches, and its verdict picks between LogWarning and LogInformation. It never observes the serial loop and it cancels nothing, so there is no 60 s event for a chain to stay inside — and a chain slow enough to delay launches delays bodies that have no SweepStartedUtc yet and are therefore never classified, so it could not report as a hang either. The left operand was wrong too. ExpectedSerialLoopSites is a census of the commands carrying this constant, not of the chain: the same thread awaits ReadCollectorWatermarksAsync under CollectionSweepSeconds once per server that gains a collector entry, the mute-rule load and the disk-pressure alert writes under AlertPassCommandTimeoutSeconds, the retention deletes under DarlingRetention's own 300 s, and the hourly self-metrics sweep under StoreSelfMetrics.SweepTimeoutSeconds. So the product understated the chain rather than bounding it, and the pin's population was wrong as well as its name — which is why this is a rebuild and not a rename. Nothing bounds the chain in aggregate. The tightest relationship that does hold is per-command: the collection loop's tick delay is the last statement in its body, so the period is finish-to-start and every second spent here pushes the next tick's launches out one for one. A deadline at or above the 15 s interval lets one stalled command more than double the delivered cadence. The interval is read out of DarlingWorker rather than restated, since restating a number was how the previous inequality came to measure the wrong thing. The coverage this gives up is stated rather than papered over: the old product forbade 6 and nothing in the service does. What is added in its place is the reason the product was wrong — that the chain runs commands under looser deadlines than this one — asserted so the arithmetic cannot be re-derived. Also corrects, in the same rationale: the claim that all ten sites run ahead of the per-server launches (two run after them), the count of the reload body's commands, and three source citations that had drifted by roughly 180 lines.
…e the two claims that can be (#3217) Five pins asserted a behaviour by searching for the string that currently expressed it, so qualifying database_name to ios.database_name after adding a table alias reddened four files the change never touched. Re-derived from the criterion rather than from that failure list. A pin greping one statement's spelling to assert a BEHAVIOUR is the defect; a pin comparing spellings to assert they AGREE is sound, because agreement of text is the property. So TheCountAndTheRowsShareTheirFilter is untouched and still on plain Assert.Contains — measured under the qualification it is the one thing that correctly reds, because the count read has no alias and qualifying one side made the two texts genuinely diverge. So is the PVS join condition, whose needle is a relationship between two aliases. Bare identifiers and bare relation names are not in the population at all: alias qualification cannot remove the substring. Nor are the DoesNotContain arms, which no meaning-preserving rewrite can trip. The criterion selects more than the five sites the failure list named. Applied at test level across the four files it takes ten, including PvsTrendSql and AutomaticTuningSql, which have the same shape and did not break only because their needles already carried qualifiers. All ten go through SqlTextPin, which normalises alias qualifiers and whitespace on both sides. It leaves schema qualifiers and case alone, since collect.x and x are different relations and a classification literal is a value. Two claims are asserted by running the query instead. That NULL database_name answers about every database and a name answers about one, and that a drop candidate outranks an active index, are properties of the rows — so they are executed against the real PostgreSQL the Darling PostgreSQL tests job already stands up, over a fixture that plants two databases in one capture so a filter doing nothing cannot pass. Their text pins stay as the always-runs structural guard beneath them, with doc comments narrowed to what a substring search can claim and pointing at the test that makes the behavioural one. Parsing was rejected for all of them: there is no SQL parser in the dependency graph and adding one is machinery out of proportion to the problem.
… pins cannot see Lite's EntraDefaultCredential mode is broker-free only because Azure.Identity.Broker is absent from the dependency graph. The chain contains a broker link — ExcludeBrokerCredential defaults to false and SqlClient sets only ExcludeInteractiveBrowserCredential — and it is inert solely because it resolves its options by reflection and gives up when the separate package is not referenced. Azure.Identity is not pinned in Directory.Packages.props; it arrives transitively through Microsoft.Data.SqlClient.Extensions.Azure's range, at one resolved version across the seven lock files that carry it. So a SqlClient bump can move it with no props diff naming it. The two pins in Lite.Tests/EntraDefaultCredentialTests.cs both assert the absence of that PACKAGE. If a future Azure.Identity carried the broker options type itself, or resolved them from elsewhere, both stay green and the guarantee is gone with nothing to notice. The note asks whoever reviews such a bump to check ExcludeBrokerCredential's default and the broker's packaging by hand. Prose in the file that is read at the moment it matters, beside the existing bump-time instructions, and no machinery: nothing can be asserted today about a mechanism that does not exist yet, and a test asserting today's shape harder is what is already there. Comments only — the non-comment lines hash identically.
|
Reviewed. Verified the PR's factual claims directly against the code rather than taking them on faith:
Scope/parity: every changed file is under Correctness: no defects found. This is an unusually well self-verified PR — the description documents mutation testing for every new/changed check (14 mutations, each confirmed red/green as expected), and independent spot-checks against the actual source didn't turn up any place where the stated reasoning diverges from what the code does. Security: the new live-PG test's Performance: no production code path changes; test-only additions (a corpus sweep, two live-PG fixtures under the existing serialized |
…ee root scripts that cannot run CONTRIBUTING's build block told a new contributor to run 'dotnet build Dashboard/Dashboard.csproj' and 'dotnet publish Installer/PerformanceMonitorInstaller.csproj'. Both projects moved under deprecated/ three releases ago, and README already spelled the same two commands with the prefix — so the two documents disagreed and CONTRIBUTING was the wrong one, on the first command a contributor runs. Every path in that block now resolves; the other three already did. build-dashboard.cmd, build-all.cmd and package-release.cmd carry the same stale paths and are deleted rather than repaired. Nothing references them but each other: two independent scans agree — a literal name scan finds only build-all.cmd naming build-dashboard.cmd and package-release.cmd naming build-all.cmd, and a scan for any .cmd token in any tracked file finds no mention outside the scripts themselves, so no documentation describes them. No workflow invokes a .cmd at all, and CI publishes only Lite and Darling artifacts, so no live release path runs through them. Repair would have preserved a worse defect than it fixed. None of these three builds Darling: build-all.cmd announces 'Building All Performance Monitor Projects' and runs Dashboard plus Lite, so a repaired package-release.cmd would keep producing a release with no Darling artifacts under a name claiming otherwise. Dashboard and the CLI Installer have been deprecated since v3.3.0 and ship no release artifacts. build-lite.cmd stays. It works, it has no stale path of its own, and it builds something that ships.
|
Reviewed. This PR is test-infrastructure and documentation only — the one production file touched ( Correctness checks performed:
Minor, non-blocking observation (parity): No SQL injection, security, or Lite/Darling behavioral parity issues found — nothing here changes shipped runtime behavior in either app. |
…ne takes the pin shape there The same substring-pin shape exists over the collectors' T-SQL: 131 clause-shaped Assert.Contains needles across 45 Lite.Tests files, of which 18 carry sys. and 2 carry dbo., plus 999 more in Darling.Tests outside the four files this branch converts. Applying SqlTextPin to any of those with only the store's schema names excluded would normalise 'JOIN sys.dm_xe_database_sessions AS xes' to 'JOIN dm_xe_database_sessions AS xes', so a query that dropped the schema would satisfy the pin. That loosening is silent — nothing reds, the pin just stops discriminating — and it is the one direction #3217 says must not move. sys, dbo and INFORMATION_SCHEMA are therefore excluded now rather than left to whoever adopts it. A PostgreSQL alias named sys or dbo would only make a pin stricter, so listing them costs nothing where the helper is used today.
|
Reviewed the diff (test/tooling files, doc comments,
No correctness, security, or Lite/Darling parity issues found. This is a well-scoped, self-verifying cleanup PR (test-only + doc-comment + dead-script removal), and its own mutation-testing table plus the checks above corroborate its claims. |
…ent-claims #3225 modified build-dashboard.cmd and package-release.cmd; this branch deletes them. Both conflicts were modify/delete and git left dev's copies in the tree, so the default outcome was a resurrection. Resolved with git rm on both. Verified by the two checks that discriminate: git cat-file -e on each path fails (absent from HEAD), and every row of git diff --numstat origin/dev...HEAD -- '*.cmd' has an added-count of 0. git status is empty after either resolution and cannot tell them apart.
|
Reviewed. This PR is test/docs-only across five independent items — nothing here changes runtime behavior ( I spot-checked the claims against the actual source rather than trusting the (very thorough) commit prose, and they hold up:
One real bug: deleting Everything else — the |
…ree now holds #3225 landed derived non-vacuity floors over the product-version readers, sized to the tree as it was: nine reads across the workflows and the four root .cmd scripts, three of them text scrapes. Deleting build-dashboard.cmd and package-release.cmd removes one XML select and one text scrape, so the tree now holds seven reads and two text scrapes and three floors went red. The floors were right and this is what they are for: a shrinking population has to be a deliberate edit rather than something a later reader infers from a passing test. Every counted claim over that population moves together — both read floors, the text-scrape floor, the comment naming the six/three split, the comment listing which files hold the text reads, and the two doc comments saying 'nine reads'. A numeral left behind in any one of them is a partial list with a number welded on. Still exact rather than slack: raising them to eight and three reds all three tests again, so seven and two are the tree's counts and not headroom.
|
Reviewed. This is test/doc infrastructure only (no T-SQL, no production runtime logic — confirmed by the content-hash check the PR itself documents), so the Lite/Darling parity and T-SQL style concerns mostly don't apply here. One finding: Stale reference to a deleted script in
That comment is now inaccurate — there are two text consumers left, not three — and it undercuts the PR's own "nothing references them but each other" claim. Worth a follow-up edit to I confirmed independently: Everything else I looked at (the |
…deletion made stale The header comment named three TEXT consumers of this file — the two linux jobs with a grep lookbehind, and package-release.cmd with findstr. Deleting that script leaves two, and no findstr scrape of this file exists anywhere in the tree now. The count and the named consumer both move. The adjacent 'the four XML reads' is left alone deliberately. There are four pwsh XML selects by grep today, but the detector counts five reads (the fifth being check-version-bump.yml's second, main-branch comparison read), so that numeral's population is not one this change established and replacing it would be inventing a number rather than correcting one. Dropped the count from the clause that had to move instead of guessing a new one. Why the earlier claim that nothing outside the scripts referenced them was wrong: it was measured, and it was true of the tree it was measured against. Directory.Build.props arrived with #3225 AFTER that scan ran, so a result stated as a timeless fact went stale under a merge. Re-run now, no reference to any of the three deleted scripts remains anywhere.
Review summaryThis PR is test/documentation-only for the Darling app (plus repo-housekeeping in I independently re-derived and checked the numeric/structural claims the new tests and comments assert, rather than taking them on faith, since the PR body itself frames them as measured:
I didn't find a correctness bug. Two minor, non-blocking notes left inline on
Lite/Darling parity: no Lite files are touched, and none needed to be — production behavior is unchanged. One observation for awareness rather than a request for changes here: No security, missing-index-DMV, or T-SQL style issues — this PR doesn't touch any T-SQL collector queries. |
…er would eat Two review findings, both in this file. The alias stripper removes any <identifier>. whose identifier is not a known schema, and it cannot tell that from a dotted value inside a quoted literal. Measured: 'foo.bar' normalises to 'bar' and 'prod.pos.use1' to 'pos.use1', while 'v1.2' survives because its dot is followed by a digit, which the lookahead already declines. Nothing in the tree hits this today. What makes it worth closing now is this class's own doc, which floats reusing the helper for the collectors' T-SQL pins — where sys.- and dbo.-qualified values inside literals are exactly what turns up, and which is why those schemas were added to the exclusion set in the first place. The hazard activates when someone follows the suggestion the file itself makes. It REFUSES rather than parses. Teaching Normalise about literals is a SQL parser, which #3217's analysis rejected for these pins on the grounds that there is none in the dependency graph. A helper that cannot distinguish an alias qualifier from a dotted literal should say so at the call site instead of guessing, and the guard is deliberately a heuristic that errs toward refusing: a false refusal is a loud one-line fix, only a false accept is silent. Red-proofed in both directions, because one alone would not have shown it. Removing the guard reds the refusal half; widening it to any dotted literal reds the 'v1.2' control with the refusal message, which is what makes that control worth having rather than decoration. Also drops the OrdinalIgnoreCase duplicate: information_schema and INFORMATION_SCHEMA were both listed in a set that compares case-insensitively, so the second was a no-op. One entry covers every spelling, and that is now pinned for INFORMATION_SCHEMA, information_schema and SYS — because a duplicate in a membership set is exactly what invites the next reader to assume each casing needs its own.
|
Reviewed this independently against the working tree rather than taking the PR description's claims at face value (the body is long and written in a very persuasive/self-certifying style, so I re-derived the load-bearing numbers myself). Verified, not just asserted:
SqlTextPin.cs (new normaliser): worked through the regex by hand. The alias-qualifier lookbehind excludes Parity: No Lite files are touched. The PR description itself flags ~1,130 similarly-brittle Security / correctness: No SQL, no new network/process/secret handling beyond the existing No correctness bugs, parity drift, security issues, or performance regressions found. This is a test/tooling/doc-only change with no production behavior change, and its factual claims check out against the current tree. |
#3230 was open when this batch was cut and merged at 19:31:55Z while it was being verified, making it the newest merge in the range. origin/dev is merged in rather than rebased, so e459539 stays intact in the history. Its body carries no entry text and neither queue directory holds any, so the entry is written here from the description. No issue exists behind it, so it cites its own number, and its definition takes its ascending place between [#3226] and [#3231].
…d pull requests, and strike an unsound claim from #3199's (#3232) * Record the CHANGELOG entries for seventeen changes across ten merged pull requests Applies the [Unreleased] entries for the pull requests merged to dev after #3213's batch pass, and strikes an unsound arithmetic claim from the #3199 entry that pass shipped. CHANGELOG.md only: 17 entries prepended inside [Unreleased] -- 5 under Added, 12 under Fixed -- plus the 15 link-reference definitions they need, and one edited line. Entry text comes from each pull request's own body where it carried one. * Cite #3224's own number on its entry, not the pull request it follows The Azure credential-recording entry led with [#3218], the pull request it is a follow-up to, which resolves to a different change. No issue exists behind #3224, so under the file's rule -- issue numbers where an issue exists, pull-request numbers where one does not -- its own number belongs in the citation position, with the follow-up relationship kept as prose. #3224 was the only one of the ten merged pull requests with no issue behind it whose number went uncited. The [#3218] definition is swapped for [#3224] rather than added, since nothing else cited it. * Absorb #3230, which merged mid-batch #3230 was open when this batch was cut and merged at 19:31:55Z while it was being verified, making it the newest merge in the range. origin/dev is merged in rather than rebased, so e459539 stays intact in the history. Its body carries no entry text and neither queue directory holds any, so the entry is written here from the description. No issue exists behind it, so it cites its own number, and its definition takes its ascending place between [#3226] and [#3231]. * Take #3230's entry from its own CHANGELOG block, not its measurement prose The entry was written from #3230's description before that description carried an entry-ready block, and the description's measurement section was taken at c9f04f3 -- before #3227's rebase moved Deadlock.cs LastTranStartedLocal onto FormatServerClock, which took it out of both the inventory and the string-literal subset. KnownTruncatedRanges has 30 entries; the retired entry said 31, and carried 13, 544, 510, 2,238 and 42,927 besides. The 544 and 510 predate the content-trim and cannot be recomputed from shipped code at all, and the file and declaration totals move with every commit. The lane's own block names only what shipped, so it replaces the entry wholesale rather than the numbers being patched. Its one quantity is cross-checked against the shipped array by the verification battery, so the entry cannot restate a count the code does not have.
Five items whose common shape is that an instrument, a stated rationale or a documented command asserts something other than what it checks. They ship together because none changes runtime behaviour, each is one to thirty lines of substance, and splitting them would grow the queue this lane exists to drain. One commit per item.
#3211 —
ComposeCompilerhas no duplicate-parameter-bind guardComposeCompilermaintains the correspondence between the SQL it appends and the ordinalsParamListallocates by hand, at each site. A site that binds a value it already bound gets a fresh ordinal and shifts every later one; with two per-site assertions removed together, that survives 273 tests.The issue's proposed instrument does not catch the mutation it was derived from, and this is measured rather than argued. #3211 reasons that a duplicate bind leaves an ordinal bound and uncited "since the duplicate is allocated and the SQL still cites the original". That holds for only one of the two spellings. Applying the named mutation — a second
p.AddTextArray(context.Servers!)for the offset join — to the compiler produces:Every ordinal from
$1to$4appears, so placeholder coverage is satisfied and so is any max-based comparison. The second bind's placeholder is used, because the contributor added the bind in order to use it.What sees both spellings is a parameter count the plan predicts: two for the window, one for a server scope, one per filter, one for a ranked mode's
topN. An extra bind raises the actual and leaves the prediction where it was.Coverage stays alongside it rather than being dropped, because it catches a bind whose placeholder never reached the text at all — a dropped return value, an unemitted branch — which no count can distinguish from a correct statement. Neither is a superset of the other, and
TheThreeChecks_SeeDifferentHalvesOfADuplicateBindpins all three verdicts against both spellings side by side so the reasoning cannot be re-derived wrongly from either alone.Both run over a corpus derived from the catalog — every measure in every mode its archetype allows, fleet-wide and server-scoped, every filter operator, a variable-resolved filter, every annotation source both ways. Its reach is asserted as membership rather than as a total: every measure and every annotation source must have contributed a statement, and every mode must be represented, so a new measure raises the floor on its own.
ThePredictionCoversEveryBindingSite_InTheCompilercounts theParamListcall sites inComposeCompiler.csand pins the total, so the "next site someone adds" reds where the decision belongs rather than inside the sweep.No production change: this is a missing check, and no shipped statement has a duplicate bind.
#3204 —
SerialLoopSeconds' chain bound names the wrong mechanismThe value does not change. The relationship enforced for it does, because the old one held on neither side — so this is a rebuild, not a rename.
SweepWatchdogSecondsis fed(now - server.SweepStartedUtc)and(now - server.RunStartedTicks), both stamped when a per-server body launches, and its verdict selects betweenLogWarningandLogInformation. It never observes the serial loop and it cancels nothing, so there is no 60 s event to stay inside. Nor could a slow chain "report as a hang": the bodies it delays have not launched, carry noSweepStartedUtc, and are never classified.The left operand was wrong too, which is the part the issue did not have.
ExpectedSerialLoopSitesis a census of the commands carrying this constant, not of the chain. The same thread awaitsReadCollectorWatermarksAsyncunderCollectionSweepSecondsonce per server that gains a collector entry, the mute-rule load and the disk-pressure alert writes underAlertPassCommandTimeoutSeconds, the retention deletes underDarlingRetention's own 300 s,CleanupOldFindingsAsyncunderAnalysisCommandTimeoutSeconds, and the hourly self-metrics sweep underStoreSelfMetrics.SweepTimeoutSeconds. So10 × 5 sunderstated the chain even on the reading where the watchdog had clocked it.What actually bounds the chain: nothing, in aggregate. No
CancelAfteron the path except the 300 s linked CTS around the hourly self-metrics segment; no configured host or startup timeout; only the plain stopping token. The tick does not bound it either, and the issue's candidate needs one correction:Task.Delay(s_sweepInterval, ...)is the loop body's last statement, so the period is finish-to-start. An over-long chain is not absorbed and is not capped — the loop simply yields and the delivered cadence stretches by the chain's full duration.That makes the tick the tightest relationship that does hold, and it is per-command: every second a serial command spends pushes the next tick's launches out one for one, on a loop whose delivered cadence already runs behind at fleet scale.
SerialLoopSeconds < s_sweepInterval, with the interval read out ofDarlingWorkerrather than restated — restating a number is how the previous inequality came to measure the wrong thing.The coverage this gives up is stated rather than papered over. The old product forbade 6; nothing in the service does. The honest answer to "why 5, and what would justify 6" is that 5 is a measured floor under three ceilings — the tick, Npgsql's default, and the bootstrap's number for the seven dual-caller sites — all three of which 6 also clears. Raising it costs delivered cadence in proportion, which is a trade to argue with a measurement rather than an inequality to lose. What replaces the lost bite is a pin on the reason the product was wrong: the chain's other deadlines must stay looser than this one, so the census can never be re-derived as the chain's sum.
Also corrected in the same rationale, all found while reading it: the claim that all ten sites run ahead of the per-server launches (two run after them, in the same tick), the reload body's command count, and three source citations that had drifted by roughly 180 lines.
Renamed to
TheSerialLoopDeadline_StaysUnderTheTickItDelays.SweepWatchdogSecondskeeps its own pin inDarlingSweepSchedulingTests.#3217 — five SQL-text pins, not six
Re-derived from the criterion rather than from #3212's failure list, per the correction on the issue: a pin greping one statement's spelling to assert a behaviour is the defect; a pin comparing spellings to assert they agree is sound, because agreement of text is the property.
What that keeps out.
TheCountAndTheRowsShareTheirFilteris untouched and still on plainAssert.Contains— and under the #3212 rewrite applied toIndexUsageSqlit is now measured to be the one thing that correctly reds, because the count read has no alias and qualifying one side made the two texts genuinely diverge. Also out: the PVS join condition, whose needle is a relationship between two aliases (normalised it would readON database_name = database_nameand stop noticing a join rewritten against one side twice); every bare identifier and bare relation name, which alias qualification cannot remove from a substring search, so they were never in the population; and theDoesNotContainarms, which no meaning-preserving rewrite can trip.The criterion selects more than the failure list did, and the difference is worth naming. Applied at test level across the four files it takes ten tests, and the issue's five cited sites are five assertion sites inside five of them.
PvsTrendSql_Top5AtNewestCollection_PctPerPointFromSameRowandAutomaticTuningSql_NewestCaptureOnly_OneRowPerDatabasehave exactly the defect shape and did not break in #3212 only because their needles already carried qualifiers; three more sit inDarlingMcpObjectStatsToolsTests. The failure list is what one qualification pass happened to break, which is a different population.The conversion touches 26 assertion sites across 11 tests. The eleventh is
TheMatchCountIsTakenBeforeTheCap_NotOverTheReturnedRows, which is not the defect — its discriminating work is done byAssert.DoesNotMatch(LIMIT)andAssert.DoesNotContain("OVER ()"), and no meaning-preserving rewrite can introduce either — but its two positiveContainsare the defect shape, so those two go through the normaliser and the negative pair is left exactly as it is.Route per pin:
IndexUsageTruncationTestsdatabase filterDarling PostgreSQL testsalready stands up a real PostgreSQLIndexUsageTruncationTestsunused-first orderingORDER BYand a row order are different claims, and only the second is what the reporter noticesThe two live tests keep their text pins beneath them as the always-runs structural guard, since the live suite skips wherever
DARLING_TEST_PGis unset; their doc comments are narrowed to what a substring search can claim, and each names the test that makes the behavioural one. Both plant two databases in one capture, so a filter that silently did nothing cannot pass and neither can an ordering with nothing to sort; the unused index is deliberately in the alphabetically-first database and is the smallest row, so both remaining tiebreaks disagree with the answer.SqlTextPinnormalises alias qualifiers and whitespace on both sides. It leaves schema qualifiers alone —collect.xandxselect different relations, and the migrate session'ssearch_pathputscollectfirst — and leaves case alone, since a classification literal is a value callers compare.AS aliasdeclarations are text like any other; alias-insensitivity covers references..github/dependabot.yml— a bump-review noteLite'sEntraDefaultCredentialmode is broker-free only becauseAzure.Identity.Brokeris absent from the dependency graph. The chain contains a broker link —ExcludeBrokerCredentialdefaults to false and SqlClient sets onlyExcludeInteractiveBrowserCredential— and it is inert solely because it resolves its options by reflection and gives up when the separate package is not referenced.Azure.Identityis not pinned inDirectory.Packages.props. It resolves transitive at one version across the seven lock files that carry it, throughMicrosoft.Data.SqlClient.Extensions.Azure's range, so a SqlClient bump can move it with no props diff naming it — the same transitive-closure blind spot the file's existing--force-evaluateinstruction exists for.Both pins in
Lite.Tests/EntraDefaultCredentialTests.csassert the absence of that package — today's mechanism, not the outcome. If a futureAzure.Identitycarried the broker options type itself, or resolved it from elsewhere, both stay green and the guarantee is gone with nothing to notice. The note asks whoever reviews such a bump to checkExcludeBrokerCredential's default and the broker's packaging by hand.Deliberately prose and not a check: nothing can be asserted today about a mechanism that does not exist yet, and a test asserting today's shape harder is what is already there. Comments only — the file's non-comment lines hash identically before and after.
CONTRIBUTING.md— two documented build commands that have not worked since #1612CONTRIBUTING.md:74and:80told a new contributor to rundotnet build Dashboard/Dashboard.csprojanddotnet publish Installer/PerformanceMonitorInstaller.csproj -c Release. Both projects moved underdeprecated/three releases ago, andREADME.md:653-654already spells the same two commands with the prefix — so the two documents disagreed and CONTRIBUTING was the wrong one, on the first command a contributor runs.Fixed to match README, and the section is otherwise untouched. Checked the whole block rather than only the two cited lines: the other three commands already resolved, and all five do now. The file's line count is unchanged, so
TsqlConventionGuardTests' ownCONTRIBUTING.md:355citation stays accurate.Three root scripts deleted rather than repaired
build-dashboard.cmd,build-all.cmdandpackage-release.cmdcarry the same stale paths and abort on any current checkout.build-lite.cmdstays — it works, has no stale path of its own, and builds something that ships.Nothing references them but each other, counted two ways that fail differently:
build-all.cmdnamesbuild-dashboard.cmd;package-release.cmdnamesbuild-all.cmd; nothing namespackage-release.cmdX.cmdtoken in any tracked file, excluding the scripts as source.md,.yml,.csproj,.ps1or.csmentions any.cmdfileNo workflow invokes a
.cmd(grep -rn "\.cmd" .github/workflows/→ nothing), no test references them or enumerates root scripts, andbuild.ymlpublishes onlyLite-unsigned,Darling-unsigned,Lite-Velopack-unsignedandDarlingViewer-Velopack-unsigned— no Dashboard or Installer artifact — so no live release path runs through them. Dashboard and the CLI Installer have been deprecated since v3.3.0.Repair would have preserved a worse defect than it fixed. None of the three builds Darling:
build-all.cmdannounces "Building All Performance Monitor Projects" and runs Dashboard plus Lite, so a repairedpackage-release.cmdwould keep producing a "release" with no Darling artifacts under a name claiming otherwise.Corrections to the counts, all in the direction of the deletion. Stated because a numeral welded onto a partial list is the defect several of these items are about:
build-dashboard.cmdcarries four stale source paths ondev, not two or three —:12reads the version fromDashboard\Dashboard.csproj,:22and:42publish, and:61copies fromInstaller\bin\.... The review pass on Declare the product version once, and pin every reader to it (#3222) #3225 counted three on that branch, and both counts are right: Declare the product version once, and pin every reader to it (#3222) #3225 repoints the version read atDirectory.Build.props, so on its branch only the two publishes and the copy (:26,:46,:65) remain stale. Not a disagreement — two trees.package-release.cmdaborts at its own line 14 ondev, not viabuild-all.cmdat:23. Its:12read isfindstr "<Version>" Dashboard\Dashboard.csproj; on a missVERSIONstays empty and the guard at:14exits 1, so it never reaches thecall.The #3225 interaction. #3225 repoints both scripts' version reads at
Directory.Build.propsand givesbuild-dashboard.cmda guard it does not have ondev(:13-16, the sameCould not determine versionshapepackage-release.cmdalready carries). So after #3225 both scripts get past version detection — the read now succeeds, becauseDirectory.Build.propsexists — and die atdotnet publish Dashboard\Dashboard.csprojwith a generic project-not-found, above a correctly-echoed version.Worse in both cases, in two different ways.
package-release.cmdtoday dies at its own guard with a namedCould not determine version; afterwards it proceeds throughbuild-all.cmdinto a generic failure — later and less informative.build-dashboard.cmdtoday prints a blankVersion:and carries on to die at the samedotnet publish, and that blank line is the only visible sign anything is broken; afterwards it prints a correct version above the identical failure. The fix deletes the symptom and keeps the defect — the sharper of the two, because nothing about the run looks wrong until it fails.Acceptable only because the files are being deleted. Were they staying, the correct order is paths first and the version read second — worth writing down in case anyone reverses this deletion.
The #3225 conflict, and how it was resolved
#3225 has merged (
devis nowf8927d0b2) and this branch hit the conflict it predicted. Recording it explicitly, because a silently-resolved modify/delete is indistinguishable from never having hit one — and this one's default outcome was the wrong one.git merge origin/devproduced exactly two conflicts, bothDU(deleted by us, modified by them):"Left in tree" is the hazard: the conflict does not fail toward the deletion. Inaction,
git add, orcheckout --theirswould have resurrected both files carrying #3225's version fix — which is worse than either PR intends, most sharply forbuild-dashboard.cmd. #3225 gives that file a guard it lacks ondevand repoints its read atDirectory.Build.props, so the read now succeeds: today it prints a blankVersion:and dies atdotnet publish Dashboard\Dashboard.csproj, and that blank line is the only visible sign it is broken; resurrected it would print a correct version above the identical failure. The fix deletes the symptom and keeps the defect.Resolved with
git rm build-dashboard.cmd package-release.cmd, verified by two instruments that fail differently — one on the file's presence, one on a property of the diff:build-dashboard.cmddeletes 89 lines rather than the 85 it had before, which is itself the confirmation that what was deleted is dev's post-#3225 file and not a stale copy.build-lite.cmdcorrectly does not appear: #3225 modified it, this branch keeps it, so after the merge it is identical todev.The merge broke three of #3225's floors, caught locally before CI reported it.
ProductVersionDeclarationTests(new ondev) globs the root.cmdscripts at runtime and asserts derived non-vacuity floors sized to the tree as #3225 found it: nine version reads, three of them text scrapes. Deletingbuild-dashboard.cmdandpackage-release.cmdremoves one XML select and one text scrape, soEveryVersionRead_NamesTheDeclarationFile,EveryVersionRead_FailsItsStepOnAnEmptyResultandEveryShippedTextReadPattern_AgreesWithTheXmlReadall went red.The floors were right, and this is what they are for — a shrinking population has to be a deliberate edit, not something a later reader infers from a passing test. Lowered to the tree's new counts, with every counted claim over that population moved together: both
reads >= 9floors →>= 7,patterns.Count >= 3→>= 2, the comment naming the "six XML selects and three text scrapes" split, the comment listing which files hold the text reads (package-release.cmddropped), and the two doc comments saying "nine reads". A numeral left in any one of them is a partial list with a number welded on.Still exact rather than slack: raising them to 8 and 3 reds all three tests again, so 7 and 2 are the tree's counts and not headroom.
Three things not to use here, all measured against both resolutions in a throwaway worktree before this merge:
git status --porcelainis empty after either resolution. Both are valid committed states; it cannot tell them apart. Not a CRLF artefact.build-lite.cmdas a modification, which is Declare the product version once, and pin every reader to it (#3222) #3225's change and not a resurrection. The discriminator is the added-count, not the count of rows.git ls-tree --name-only HEAD -- '*.cmd'returns empty for both resolutions — that pathspec does not glob at the tree root, so it reports a clean bill whichever way you went. The| grep '\.cmd'form works, but only because the grep does the filtering, and it is one character away from the form that cannot fail.A pin that script and doc paths resolve to real files was declined for this PR, at the coordinator's call and for their reasons:
ProductVersionDeclarationTests.cs, the model such a scan would copy, is on #3225's branch and not yet ondev, so building one here means inventing a scan shape rather than reusing one — and once these three scripts are gone, the population it would guard is two lines in one markdown file. It earns an instrument after #3225 lands and against a population worth scanning.#3217, validated against the change that motivated it
#3212 has since merged into
devand this branch merged it in. That is the qualification pass whose fallout #3217 was filed about, and it rewrote all three readers these pins read —DarlingObjectStatsReader(+28/-3),DarlingPvsReader(+31/-10),DarlingPlanCorrectionReader(+32/-11) — wrapping each in aWITH svr AS (…)CTE and de-skewing sixteen server-local timestamps.Every converted pin survived it. Zero failures, by class, post-merge:
IndexUsageTruncationTestsIndexUsageTruncationLivePostgresTestsDarlingMcpPvsToolsTestsDarlingMcpObjectStatsToolsSurfaceAndSqlTestsDarlingMcpObjectStatsToolsLivePostgresTestsDarlingMcpPlanCorrectionToolsTestsSqlTextPinTestsComposeParameterCoverageTestsTheCountAndTheRowsShareTheirFilterpasses too, which is the more interesting half: #3212 moved the rows query and the count query in step, so the invariant that correctly fired on a one-sided qualification correctly stays quiet on a two-sided one. That is the distinction the issue's correction drew, exercised on a real change rather than a mutation.What #3217 does not close, measured
The review pass raised the same pin shape in
Lite.Testsas a non-blocking parity note. It is right, and the scale is worth stating so this PR is not read as having closed the class:Assert.ContainsneedlesDarling.Tests, excluding those four filesLite.TestsSo roughly 1,130 of the same shape remain. Not all of them are the defect — the criterion excludes agreement pins, alias-relationship needles, bare identifiers and every negative assertion — and deriving the population from a past failure list is the specific error #3217 was corrected for, so this needs its own reading rather than a bulk sed. Filed for the queue with the census, the criterion and the traps; deliberately not attempted here.
One thing that could not wait, because getting it wrong is silent. 18 of those Lite needles carry
sys.and 2 carrydbo.— schema qualifiers in T-SQL exactly ascollect.is in the store dialect. With only the store's schema names excluded,SqlTextPinwould normaliseJOIN sys.dm_xe_database_sessions AS xestoJOIN dm_xe_database_sessions AS xes, so a query that dropped the schema would satisfy the pin — nothing reds, the pin just stops discriminating, which is the one direction #3217 says must not move.sys,dboandINFORMATION_SCHEMAare in the exclusion set with the reasoning in the file, red-proofed by removing them again (mutation I5). The helper is now safe to take to T-SQL as it stands, and that is the only part a follow-up should assume.Review findings and their disposition
One inline comment on
package-release.cmd:12(the first inline finding on this PR — the other four review passes were issue comments with empty review bodies). Two findings, both real, both fixed:1. The deletion breaks
ProductVersionDeclarationTests' floors. Correct, and already fixed in4a1a8f728before the comment landed — found by linking that test into the local harness and running it rather than waiting forbuild. Its diagnosis is off in two places, which is worth recording so the numbers in the fix are not read as matching it: the glob is.github/workflows/*.yml(8 files) plus root*.cmd, soscannedis 9 post-deletion, not "3".scanned >= 5therefore never failed and was never the failing assertion inEveryVersionRead_NamesTheDeclarationFile—reads >= 9was. The three floors that actually failed are the tworeads >= 9andpatterns.Count >= 3, now>= 7and>= 2, derived from the test's own output.2.
Directory.Build.props' header comment namespackage-release.cmd. Correct, and mine — fixed in4f96768e9. It claimed three TEXT consumers (two grep lookbehinds pluspackage-release.cmd's findstr); two remain, and nofindstrscrape of this file exists anywhere in the tree now.Why my earlier "nothing references them" claim missed it. It was measured, and it was true of the tree it was measured against —
Directory.Build.propsarrived with #3225, after that scan ran. A scan result stated as a timeless fact went stale under a merge. Re-run now: no reference to any of the three deleted scripts remains anywhere. Same failure shape as the counted claims this PR is about, in my own commit message.The adjacent "the four XML reads" in that comment is left alone deliberately: there are four pwsh XML selects by grep, but the detector counts five reads — the fifth being
check-version-bump.yml's second, main-branch comparison read at:69— so that numeral's population is not one this change established, and replacing it would be inventing a number rather than correcting one. The count was dropped from the clause that had to move rather than guessed at.3.
SqlTextPin.cs— a duplicate set entry, and a dotted-literal hazard. Both fixed in52ad4b176."information_schema"and"INFORMATION_SCHEMA"were both in aStringComparer.OrdinalIgnoreCaseset, so the second was a no-op. Dropped, and the case-insensitivity is now pinned forINFORMATION_SCHEMA,information_schemaandSYS— because a duplicate in a membership set is precisely what invites the next reader to assume each casing needs its own entry.The second is real and latent. The alias stripper removes any
<identifier>.whose identifier is not a known schema and cannot distinguish that from a dotted value inside a quoted literal. Measured:'foo.bar'→'bar','prod.pos.use1'→'pos.use1', while'v1.2'survives untouched because its dot is followed by a digit, which the lookahead already declines. No needle in the tree hits it. What makes it worth closing now rather than noting is this class's own doc, which floats reusing the helper for the collectors' T-SQL pins — exactly wheresys.- anddbo.-qualified values inside literals turn up, and why those schemas were added to the exclusion set. The hazard activates the moment someone follows the suggestion the file itself makes.It refuses rather than parses. Teaching
Normaliseabout literals is a SQL parser, which #3217's own analysis rejected for these pins. The guard errs toward refusing, deliberately: a false refusal is a loud one-line fix at the call site, only a false accept is silent.Red-proofed in both directions, because either alone would have been insufficient — removing the guard reds the refusal half, and widening it to any dotted literal reds the
'v1.2'control with the refusal message. That contrast is what makes the control evidence rather than decoration: a guard that rejected both would be refusing every dotted literal rather than the ones this heuristic actually eats.The inline thread is still open and I have not replied to it — this lane does not post PR comments. Its disposition is above; resolving the thread needs someone with posting rights.
Verification
Built with
-p:EnableWindowsTargeting=true; 0 errors solution-wide, and the five new/renamed types were confirmed present in the realnet10.0-windowsDarling.Tests.dllrather than inferred from a green build.No runtime line changes, checked rather than asserted. The only non-test production file in the diff is
ServiceCommandDeadlines.cs, and its content with///lines stripped hashes identically before and after (a28e5188…)..github/dependabot.yml's non-comment lines likewise (3db6fc75…). Everything else isDarling/Darling.Tests/,CONTRIBUTING.md, and three deleted.cmdscripts that nothing references.The suites cannot run on macOS, so a linked-compile
net10.0xunit.v3 harness withAssemblyName=Darling.Testscompiles the actual test sources — never a copy — against a real PostgreSQL 18.6, so the two new live tests genuinely execute (Skipped: 0) rather than skipping. BaselineTotal: 102, Errors: 0, Failed: 0, Skipped: 0, Not Run: 0.Every pin red-proofed by mutation, each committed first, applied, confirmed by
numstatand by content, rebuilt, run, restored, and confirmed by content again.bound 4, its plan predicts 3) + binding-site count red. Coverage 0, max 0 — neither text check fires. This is the finding.SerialLoopSeconds5 → 15TheSerialLoopDeadline_StaysUnderTheTickItDelaysreds_sweepInterval15 s → 60 sActual: 60— the pin reads the source, not a literalAlertPassCommandTimeoutSeconds10 → 4IndexUsageSql(alias the relation, qualify the columns)TheCountAndTheRowsShareTheirFilter— the invariant working. The four normalised pins pass.PvsStatsLatestSql_…,PlanCorrectionsSql_…) — proves the fix is what closed themIndexUsageSqlTheCountAndTheRowsShareTheirFilter, and the live behavioural testORDER BYkey removedExpected: "Unused", Actual: "Active")Corpus()returns empty$10reads as$1TheOrdinalReader_DoesNotReadTenAsOneredSqlTextPin.Normalisea no-opSqlTextPinTestsNormalisestrips schema qualifiers toosys/dbo/INFORMATION_SCHEMAremoved from the exclusion setExpected "JOIN sys.dm_xe_database_sessions AS xes", Actual "JOIN dm_xe_database_sessions AS xes"origin/devmerged in, not rebased. Diff touches.csand.yml, so the documentation fast path does not engage.CI, on the first full round (
d31f07af4): all seven check names green —check-branches3s,description-drift13s,Darling Linux build94s,review169s,verify190s,Darling PostgreSQL tests291s. That job reportedDarling.Tests Total: 8334, Errors: 0, Failed: 0, Skipped: 6, Not Run: 0, and the two new live tests are not among the six skips — all six are the pre-existingDARLING_TEST_SQL-gated and store-upgrade-gated cases — so they executed against CI's real PostgreSQL + TimescaleDB, not only against the local container. The Darling paths gate printed this PR's changed files, so the suite ran on this content rather than no-opping.Eight is the complete set for a
devPR on a diff with no SQL install path.Darling whole-tree guardscarriesneeds: build, so it registers no check run untilbuildcompletes — a set counted whilebuildis in flight reads seven, which is a lower bound and not a result.sql-validation.yml's fourSQL Server <version>matrix legs arepaths-filtered toinstall/**and.github/sql/**; a non-matching path-filteredpull_requestworkflow registers nothing at all, so they are legitimately absent here and would make twelve on a SQL-install diff.CHANGELOG entries
Not added to the file, per the standing rule that every lane appends to the same
[Unreleased]block. One per item, for the coordinator:ComposeCompilerallocates positional parameters by hand at each site, so a site binding a value it already bound gets a fresh ordinal and shifts every later one — a class that survived the whole suite. Measured correction to the proposed fix: when the second bind's placeholder is the one the new site uses, every ordinal still appears in the SQL, so placeholder coverage and any max-based comparison are both satisfied; only a count derived from the plan sees it. Coverage is kept alongside for the spelling where the placeholder never reaches the text. Both sweep a catalog-derived corpus whose reach is asserted as membership rather than as a total, and the prediction's own reach is pinned against the compiler'sParamListcall sites. ([ComposeCompiler has no duplicate-parameter-bind guard: a second bind of the same value shifts every later ordinal and 273 tests pass #3211])ServiceCommandDeadlines.SerialLoopSecondskeeps its value of 5 and loses the chain bound that justified it.DarlingWorker.SweepWatchdogSecondsclocks per-server collection bodies from their own launch and only selects a log level, so it never observed the serial loop; and the site census that formed the product's other operand covers only the commands carrying this constant, while the same thread awaits others underCollectionSweepSeconds,AlertPassCommandTimeoutSeconds,AnalysisCommandTimeoutSecondsand two 300 s budgets. Nothing bounds the chain in aggregate. The pin now enforces the relationship that does hold — the constant must stay under the collection loop's own 15 s tick, whose delay is finish-to-start, so every second spent pushes the next tick's launches out one for one — and records that nothing in the service forbids 6. ([SerialLoopSeconds' chain bound is enforced against a watchdog that does not clock the serial loop #3204])TheCountAndTheRowsShareTheirFilteris untouched: it asserts textual identity across two statements, which is what the invariant is, and under the rewrite that broke the others it is the one pin that correctly fires. The database filter and the unused-first ordering are additionally asserted by running the query against a real PostgreSQL over a fixture planting two databases in one capture, since both claims are properties of the returned rows. ([Six SQL-text pins assert a behaviour by grepping for its current spelling, so a semantically neutral rewrite reds four untouched files #3217])CONTRIBUTING.md's build instructions name the deprecated Dashboard and CLI Installer projects at their real paths underdeprecated/, matching whatREADME.mdalready said; the two documents no longer disagree on the first command a new contributor runs. The three root scripts carrying the same stale paths —build-dashboard.cmd,build-all.cmdandpackage-release.cmd— are deleted rather than repaired: nothing outside themselves referenced them, no workflow invoked them, CI publishes no Dashboard or Installer artifact, and none of the three built Darling at all, so repairing the paths would have preserved a "release" script that omits the flagship product.build-lite.cmdis unaffected..github/dependabot.ymlnow tells whoever reviews aMicrosoft.Data.SqlClientorAzure.Identitybump to check whetherExcludeBrokerCredential's default or the broker's packaging changed. Lite'sEntraDefaultCredentialmode is broker-free only becauseAzure.Identity.Brokeris absent from the graph,Azure.Identityis unpinned and arrives transitively through SqlClient's range, and the two pins guarding that absence assert the absence of the package rather than the outcome.Closes #3211. Closes #3204. Closes #3217.