From 4d7544efac343e36fd9d33f820fc15f63bbe6119 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 18:34:41 -0400 Subject: [PATCH 01/46] docs(platform-probes): record that the encoded row is the contract, and re-plan M2 M2 built a report oracle that reads the RENDERED TEXT and checks the prose half against the NDJSON half. Its charter said that had to be so: "checking state would miss precisely this defect class -- in the original finding the state was consistent and the two renderings of it were not." Re-checked against the code, that sentence is wrong about its own evidence, and both defects it rests on were defects in the encoded row. **The alarm beside the agreeing verdict.** The alarm is a `writeln!` into the prose and the row has no key for it, while `cross_check` IS a key and read `agree`. So the row certified a clean agreeing measurement on a host where the probe had detected its own bug. A survey mining it would have been wrong with no way to know. The prose alarm was not the defect -- it was the only trace that the row was, which is why a human found it and no instrument did. **`efficiency classes: [0]` against `"efficiency_classes":1`.** Both halves were correct derivations of one consistent value. Note how it was repaired: the row now publishes the set. The fix was to change what the row publishes; the prose comparison was how a reviewer noticed, not the repair. **A correction made while writing this up, and it strengthened the case.** The first draft said the alarm's condition is unpublished so the verdict can still read `agree`. Checked before committing: the second half is no longer true -- `cross_check` now pushes `SummaryMissing` onto `parse_incomplete`, which forces the verdict away from `agree`. Writing a superseding decision against code as it used to be would have been the exact defect class this branch spent fifteen review rounds on. What the check found instead is live and larger. The row publishes `not_compared`, `parse_incomplete` and `enumeration_anomalies` as COUNTS, where the prose prints each entry's text. A survey reading `"parse_incomplete":1` cannot tell a probe self-bug from a self-contradicting core record from an unmeasured topology. The row is impoverished relative to the prose: the artifact that gets mined carries less than the artifact that gets read. So the decision recorded here is that they are not peers. The row must be CORRECT, machine-enforced. The prose must be ACCURATE AND READABLE, enforced by review. Nothing is required to hold between them. Counted, for what the other reading cost: of 38 top-level functions in report_oracle.rs, ten are correspondence rules and four are comparison helpers -- twenty-three exist only to extract values back out of rendered text. That is a parser for a format this crate writes, and it behaved like one: a large share of PR #88's review rounds were defects in the reader rather than the thing read. M3 queues the work in dependency order: publish each diagnostic as itself rather than as a count (M3.1); relocate the three surviving correspondences onto the observation as invariants checked before rendering (M3.2); emit the row from a typed value through one writer, which makes injection and field-order defects unrepresentable and subsumes the typed-banner question (M3.3); retire the prose-against-row family and its extraction layer, keeping only a well-formedness check on the row (M3.4); re-aim the corpus and fact accounting at the row's fields (M3.5); and split DESIGN-NOTES.md, now 88 KiB and XL, into Tier 1 and Tier 2 (M3.6). M2.18 is dissolved into M3.3 rather than answered alone -- it was the smallest instance of the same question. M2.17 is re-scoped by M3.5 and should follow it, since crossing prose shapes that are about to stop being checked aims at the retiring half. The nine remaining M2 items are unaffected. Supersedence markers are adjacent to both affected decisions, and the earlier one keeps everything about it that still holds -- what the oracle admits, the acceptance half, and the failure mode that looks like success. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- PLANS.md | 2 +- crates/windows-platform-probes/CHECKLIST.md | 129 +++++++++--- .../windows-platform-probes/DESIGN-NOTES.md | 154 ++++++++++++++- crates/windows-platform-probes/PLANS.md | 2 +- ...-2026-09-12-what-the-oracle-should-read.md | 187 ++++++++++++++++++ 5 files changed, 447 insertions(+), 27 deletions(-) create mode 100644 crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md diff --git a/PLANS.md b/PLANS.md index 8eb8844b4..bacac2dd0 100644 --- a/PLANS.md +++ b/PLANS.md @@ -20,7 +20,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) | in progress | M22-M23: extract the captured-context composite into `windows-thread-ambient-sys`, a standalone platform layer that captures a thread's ambient state and applies it on another thread. M24-M26: `windows-namespace-request-sys`, marshalable Win32 namespace call parameter sets, over a round-one entry list audited from three real consumers (this repository's watcher and enumeration crates, and `MikeGrier/Globazog-rs`) rather than guessed. M27: `windows-platform-probes`, a durable home for the measurements this workspace's designs rest on, under a three-tier scheme (asserted / ignored / binary-only) where every tier is compiled by an ordinary build. Feature-scoped and deleted when complete; it is the whole of the `mikegrier/thread-ambient` branch's work, and is deliberately separate from the deferred namespace-facility items in [CHECKLIST.md](CHECKLIST.md). | [crates/windows-thread-ambient-sys/DESIGN-NOTES.md](crates/windows-thread-ambient-sys/DESIGN-NOTES.md) | | [crates/windows-overlapped-io-sys/CHECKLIST.md](crates/windows-overlapped-io-sys/CHECKLIST.md) | not started | M14: finish the contract audit -- categories 1, 2, 6, 8, 9 were not examined -- and sweep `outstanding()` for the advisory-predicate hazard. | [crates/windows-overlapped-io-sys/DESIGN-NOTES.md](crates/windows-overlapped-io-sys/DESIGN-NOTES.md) | | [crates/windows-ioring-sys/CHECKLIST.md](crates/windows-ioring-sys/CHECKLIST.md) | in progress | Memory-safe Rust over the Windows `IoRing` submission/completion ring, as a new crate. M1-M7 (ring lifecycle through the `ring-copy` topology-aligned sample) are complete and archived. The parked, pinned-thread `M6+` work and the new M10 contract audit remain. | [crates/windows-ioring-sys/DESIGN-NOTES.md](crates/windows-ioring-sys/DESIGN-NOTES.md) | -| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 remains: check correspondence *between* a report's parts, which is the defect class no per-part instrument in this crate can see. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report) | +| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes; nine probe-side and bookkeeping items remain. M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row still publishes its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` cannot tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | Add a row here when new work is planned, against [CHECKLIST.md](CHECKLIST.md) or any crate's. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 830acc3af..1dd122b2a 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -217,27 +217,108 @@ three correlations below" while the enum already had four.) them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly the imagination-driven process M2.12 exists to replace. -- [ ] **M2.18** -- Decide whether a banner should be a TYPE rather than a `&str`. - - **This is a design decision for the engineer, not a defect to fix in passing.** A review observed - that `is_attribution_shaped` recognises a SHAPE, not a provenance: any two `host:` lines followed - by the exact disclaimer pass through `preamble` verbatim. Since `report` and `report_unmeasured` - both take `&str`, there is a public path where caller text decides a renderer-owned question. - Measured: a hand-built banner of `host: 999p/1c` / `host: 1p/1c` / the disclaimer - renders verbatim, and the oracle's exemption for unestablished attribution then skips the - banner-against-body processor-count check -- so the banner suppressed a correspondence. - - **The honest scope of it.** The suppression is not silent: the report visibly states that its two - readings disagree, which is exactly the condition under which declining to compare counts is - CORRECT. The oracle reads the artifact, and the artifact says so. Every production caller composes - its banner with `attribution()`, so nothing reaches this by accident today. Validating the - per-line shape more strictly does not close it either -- `attribution` legitimately emits - `host: UNKNOWN -- topology discovery failed: {error}` with arbitrary error text, so arbitrary - text can always ride inside a well-formed banner line. - - The fix that would actually close it is a typed banner with a private constructor, so only - `attribution()` can produce one and the renderer's signature carries the guarantee. The cost is - every fixture and corpus shape that builds a banner by hand, plus a test-only escape hatch that - partially reopens the hole for the tests that need odd banners. Worth doing if the renderer's - input contract is meant to be enforced rather than documented; not worth doing if `&str` in, and - containment on the way out, is the intended boundary. Raise before implementing. + +## M3 -- Make the encoded row the contract, and stop checking the prose against it + +Decided in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is +not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), from the session in +[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). + +The row is a machine contract mined across a fleet; the prose is for a reader. They carry different +obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and +readable**, enforced by review. Nothing is required to hold *between* them. + +Re-checked against the code rather than against M2's account of it: the original defect is fixed, and +what it left behind is larger. The row publishes `not_compared`, `parse_incomplete` and +`enumeration_anomalies` as **counts**, where the prose prints each entry's text. A survey reading +`"parse_incomplete":1` cannot tell *the probe detected a bug in itself* from *a core record +contradicted itself* from *this topology was not measured from a running machine*. **The row is +impoverished relative to the prose** -- the artifact that gets mined carries less than the artifact +that gets read. + +What this milestone changes in M2: + +- **M2.18 (typed banner) is dissolved into M3.3.** It was the smallest instance of "should a report + be a value a writer renders, or a string the renderer concatenates", and answering it alone would + have typed one parameter while leaving the shape everywhere else. +- **M2.17 (cross the corpus dimensions) is re-scoped by M3.5** and should be done after it, not + before: the dimensions worth crossing are the row's, and crossing prose shapes that are about to + stop being checked would be work aimed at the retiring half. +- M2.4, M2.5, M2.7, M2.8, M2.9, M2.13, M2.14, M2.15 and M2.16 are unaffected. + +- [ ] **M3.1** -- Publish each diagnostic as itself, not as a count. + + `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as + `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- + *which* condition occurred -- exists only in prose. Publish the entries, and give each a stable + machine-readable discriminant rather than the human sentence, so a survey can group by condition + without matching on English that is free to be reworded. The sentences stay in the prose, where + rewording them is harmless. + + **The rule this establishes, which is the durable half:** a renderer may not tell a reader + something the row cannot tell a survey. A cardinality is not a statement of the fact. + +- [ ] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before + rendering. + + Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three + oracle rules that survive the decision. They stop being comparisons of two rendered texts and + become predicates over `Observation` and `CrossCheck` -- `SummaryMissing` implies the verdict is not + `agree`, a non-empty `parse_incomplete` implies the verdict is not `agree`, and so on. No parser is + involved, and the check runs whether or not anything was rendered. + + Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, + restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. + +- [ ] **M3.3** -- Emit the row from a typed value through one writer. + + The row is built today by interpolating eighteen values positionally into a `concat!` template. + Two defect classes follow from that construction and both are closed by replacing it, not by + checking it: + + **Injection.** Measured on PR #88: an `io::Error` containing `{` was selected as the report's + machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text + reaching the mined artifact is contamination of the contract. + + **Field order and labelling.** A reordered value or a miscounted `{}` yields mislabelled data that + still parses, and nothing downstream can detect it. + + A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the + writer here rather than adding a serialization dependency -- this crate has none and the row is + one flat object. This subsumes M2.18: the banner becomes a typed field like any other, and the + question of who may construct one is answered by the row's constructor rather than separately. + +- [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. + + Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence + rules, four are comparison helpers, and **twenty-three exist only to extract values back out of + rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. + + What stays is a thin check that the row is **well-formed** -- it parses, it carries the expected + key set, and it is the only such line in the report. That is not a correspondence; it is the + writer's own output being checked, and the writer is the one place structure cannot check itself. + + Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for + a format that no longer has two readers. + +- [ ] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. + + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates + the facts a report publishes and measures, by mutation, which are read. The instrument is sound and + the target changes: enumerate the row's fields, and require each to be read by an invariant or + explicitly classified as unread. Its corpus of shapes keeps its purpose -- it exists to defeat the + imagination-driven fixture, which the decision does not change. + + The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on + its own terms rather than against the row. + +- [ ] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. + + Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is + to split unless the module is indivisible. It is not -- it carries current decisions and a large + volume of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture the repository + instructions describe. + + Move the rationale to `DESIGN-RATIONALE.md`, cross-referenced by decision anchor, leaving Tier 1 + stating what was decided and what forced it. The decision added by this milestone is written to be + split that way already, so it is the worked example rather than the hard case. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 8020fa745..93134363f 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1029,6 +1029,12 @@ scheduled by this decision. +**The diagnosis here is refined by [The encoded row is the contract; the prose is +not](#d-encoded-row-is-the-contract).** What each instrument could not see is +unchanged and is still the reason this component has an oracle at all. What this +section got wrong is WHERE the two defects lived: both were defects in the +ENCODED ROW, not in the relation between two renderings of a consistent state. + This probe was reviewed twenty-eight times before it opened as a pull request, by two independent readers per round on different models, with `cargo-mutants` reporting **zero surviving mutants** on both of its modules. A review on the @@ -1177,6 +1183,13 @@ section schedules nothing on its own. ## The oracle exists, and what it deliberately refuses to know + + +**The artifact-reading rule below is superseded by [The encoded row is the +contract; the prose is not](#d-encoded-row-is-the-contract).** The rest of this +section -- what the oracle admits, the acceptance half, the failure mode that +looks like success -- still describes what is in the tree and still holds. + M2.1 built it: [src/report_oracle.rs](src/report_oracle.rs), admitting only correlations the report already renders twice. The defect that forced it is the section above. @@ -1185,6 +1198,13 @@ it is the section above. would miss precisely this defect class -- in the original finding the state was consistent and the two *renderings* of it were not. +That last sentence is the superseded one, and it is wrong about its own +evidence. Re-checked against the code: the alarm has no NDJSON key, and +`cross_check` does -- so the original finding was a run whose ENCODED ROW said +`agree` while the probe had detected its own bug, and published nothing about +that bug. The state was not consistent; the row was wrong. See +[#d-encoded-row-is-the-contract](#d-encoded-row-is-the-contract). + **It relates two things already visible in the report, and re-derives nothing.** A second implementation of the rendering rules would be a check of the copy rather than of the contract, and would drift the moment either moved. So the @@ -1436,4 +1456,136 @@ live in `tests/`, so the tool validates the code they check and says nothing about THEM. The instruments remain exactly as good as the hand-sabotage that built them -- which is where several of this branch's defects were found, and where the next one will be. A clean sweep is evidence about the oracle, not about -the things measuring it. \ No newline at end of file +the things measuring it. +## The encoded row is the contract; the prose is not + + + +A probe is a data pipeline that renders, at its tail, to two artifacts: an NDJSON +row and prose. **They are not peers.** The row is a machine contract -- mined +across a fleet, joined against other runs, and the thing this workspace's designs +end up resting on. The prose is for a reader. + +So they carry different obligations: + +- **The row must be CORRECT**, and that is machine-enforced. Its values, its + invariants and its shape are asserted. +- **The prose must be ACCURATE AND READABLE**, and that is enforced by review. + It is not required to be programmatically comparable against the row, and + nothing here checks that it is. + +This supersedes the rule in +[#d-oracle-refuses-to-know](#d-oracle-refuses-to-know), which said the oracle +must read the rendered artifact rather than the state behind it. + +### What forced it: both originating defects were defects in the row + +The reason the earlier rule looked right was a misreading of its own evidence. +Re-checked against the code, for the two defects in +[#d-correspondence-failures](#d-correspondence-failures): + +**The alarm beside the agreeing verdict.** `report` emits +`BUG IN THIS PROBE: ...` with a `writeln!` into the prose, and the NDJSON row has +**no key for it** -- while `cross_check` IS a key, and read `agree` on the +defective run. So the row certified a clean agreeing measurement on a host where +the probe had detected its own bug, and said nothing about the bug. A survey +mining that row would have been wrong and had no way to know. The prose alarm was +not the defect; it was the only trace that the row was wrong, which is why a +human found it and no instrument did. + +**That defect is fixed, and what it left behind is the live gap.** Checked +rather than assumed, because the paragraph above describes the code as it was: +`Observation::cross_check` now pushes `PartitioningCache::SummaryMissing` onto +`parse_incomplete`, which forces the verdict away from `agree`, so the row can no +longer certify that run. But the row publishes `parse_incomplete` as a **count** +-- as it does `not_compared` and `enumeration_anomalies` -- where the prose +publishes each entry's text. A survey reading `"parse_incomplete":1` cannot tell +*the probe detected a bug in itself* from *a core record contradicted itself* +from *this topology was not measured from a running machine*. Those are +categorically different facts, and only the prose distinguishes them. + +So the shape of the problem is not that the row is out of step with the prose. It +is that **the row is impoverished relative to the prose** -- the artifact that +gets mined carries less than the artifact that gets read -- which is backwards +given which of the two the designs rest on. + +**`efficiency classes: [0]` against `"efficiency_classes":1`.** Both halves were +correct derivations of one consistent value -- the prose rendered the set, the row +rendered the cardinality -- so no invariant was violated. Note how it was +repaired: the row now publishes `"efficiency_classes":[...]`, the set. **The fix +was to change what the row publishes.** The prose comparison was how a reviewer +noticed, not the repair. + +Neither defect needed a prose-against-row oracle to fix. Both needed the +structured output to be made right. + +### The rule that falls out, and it is the load-bearing one + +**A renderer may not tell a reader something the row cannot tell a survey.** A +state worth naming to a human is a state worth publishing to a mining pass; if +only the prose can say it, the fact exists solely in the artifact nothing +queries, and the only detector is a person reading. A cardinality is not a +statement of the fact -- `"parse_incomplete":1` names no condition -- so a count +beside a prose list is an instance of this rule being broken, not an exception +to it. + +With that rule in place the surviving correspondences stop being text +comparisons and become **invariants on the observation, checked before +rendering** -- `summary_missing` implies the verdict is not `agree`, and likewise +for the other diagnostics and the counters. No parser is involved. + +### What the text-reading design cost + +Counted in [src/report_oracle.rs](src/report_oracle.rs): of 38 top-level +functions, ten are correspondence rules and four are comparison helpers. +**Twenty-three exist only to extract values back out of rendered text.** + +That is a parser for a format this crate itself writes, and it behaved like one. +A large share of PR #88's review rounds were defects in the READER rather than in +the thing read: a multi-byte panic in `processors_in_banner`, a `p/` substring +matching inside an opaque `io::Error`, `trim_matches` collapsing `[[0]]` and +`[0]`, a prose lookup selecting the wrong line when two began alike. None of +those is a defect in a probe. They are a defect source the design created for +itself. + +### Where structure replaces checking, prefer structure + +Three of the four hazards this component has actually met are made +*unrepresentable* by construction rather than detected after the fact, and that +is the stronger move: + +- **Injection.** Caller text reaching the row is contamination of the mined + artifact. Measured on PR #88: an `io::Error` containing `{` was selected as the + report's machine-readable row. A typed row emitted by one writer cannot have + this. +- **Field order and labelling.** The row is built today by interpolating + eighteen values positionally through a `concat!` template. A reordered field or + a miscounted `{}` yields mislabelled data that still parses. A typed row with + one writer cannot have this either. +- **Value divergence.** Two renderings of one field cannot disagree about its + value when both read the field. + +What structure does NOT cover, and so still needs something reading bytes: **the +writer itself.** Several of PR #88's defects lived there -- a disclaimer matched +as a suffix so it could be welded onto the line above, a flattening that ate the +disclaimer, a containment that produced `host: host: ...`. The residual text +check is therefore small and about well-formedness, not about correspondence. + +### What this does not say + +It does not say the prose does not matter. An overstated finding in prose +propagates into the design notes that cite it, which is a live concern in this +crate rather than a hypothetical -- M2.9 in [CHECKLIST.md](CHECKLIST.md) is an +open item about exactly that. What changes is that prose accuracy is a **review** +obligation, discharged by a person reading the report, rather than a +correspondence a machine asserts. + +It also does not delete the correspondence rules. They relocate onto the +observation, losing the parser in front of them. The containment work in +[src/topology_report.rs](src/topology_report.rs) matters MORE under this +decision, not less, because what it keeps out is now keeping it out of the +contract artifact. + +The work this implies is queued as M3 in [CHECKLIST.md](CHECKLIST.md). The +session that produced it is +[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index 00a4f7252..17c739edc 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; the oracle, the derived fact set and the shape corpus landed, and nine probe-side and bookkeeping items remain. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row publishes its three diagnostic lists as bare counts, so a survey cannot tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md new file mode 100644 index 000000000..a64ba47f9 --- /dev/null +++ b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md @@ -0,0 +1,187 @@ +# Design session 2026-09-12: what the oracle should read + +Decisions resulting from this session: + +- [DESIGN-NOTES.md](../DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is + not](../DESIGN-NOTES.md#d-encoded-row-is-the-contract) (new, and supersedes the + artifact-reading rule recorded in [The oracle exists, and what it deliberately refuses to + know](../DESIGN-NOTES.md#d-oracle-refuses-to-know)). + +Work queued from it: [CHECKLIST.md](../CHECKLIST.md) milestone M3. + +## The question + +Held immediately after PR #88 merged. The engineer asked, of the oracle that PR had just +built: **why does it have to be based on the formatted string?** + +The recorded answer at that moment was the one in the module doc of +[src/report_oracle.rs](../src/report_oracle.rs) and in Tier 1: it reads the rendered +artifact, never the state behind it, because "in the original finding the state was +consistent and the two renderings of it were not." + +The session set out to test that sentence against its own evidence rather than to defend +it. + +## What the evidence turned out to be + +The oracle's charter names two originating defects, both found by a pull-request review +after twenty-eight rounds of per-artifact review and a zero-surviving-mutant `cargo-mutants` +result had passed over them. They are recorded in [DESIGN-NOTES.md](../DESIGN-NOTES.md) -> +[The defects that survived were correspondence +failures](../DESIGN-NOTES.md#d-correspondence-failures). + +### Defect 1 -- the alarm beside the agreeing verdict + +The renderer printed `BUG IN THIS PROBE: the topology crate named L3 as the outermost +partitioning cache and this survey carries no summary for it. Nothing below about cache +partitioning can be trusted.` while `cross_check` had no branch for that state, so +`verdict()` could return `Agree` and print `=> agree` two paragraphs below. + +Checked during the session, in the code as it stands on `main`: + +- The alarm is emitted by a `writeln!` into the prose, in `report`'s + `PartitioningCache::Level` arm of [src/topology_report.rs](../src/topology_report.rs). +- The NDJSON row carries eighteen keys: `reason`, `arch`, `processors`, `groups`, + `packages`, `numa_domains`, `numa_domains_without_processors`, `cores`, + `efficiency_classes`, `caches`, `outermost_partitioning_cache_level`, + `outermost_partitioning_cache`, `policies`, `cross_check`, `not_compared`, + `parse_incomplete`, `enumeration_anomalies`, `numa_domains_only_in_cpu_sets`. +- **There is no key for the alarm.** `cross_check` is there, and on the defective run it + read `agree`. + +That changes the diagnosis. The defect was not a disagreement between two renderings of a +consistent state. **The encoded row was wrong**: it certified a run as `agree` on which the +probe had detected its own bug, and it published nothing at all about that bug. A +fleet-survey pass mining the NDJSON would have counted the host as a clean agreeing +measurement and never known otherwise. + +The prose alarm was not the defect. It was the only visible trace that the encoded row was +wrong -- which is exactly why a human reviewer found it and no instrument did. + +So the repair that actually addresses defect 1 is not "compare the prose against the +NDJSON". It is **publish the alarming condition structurally, and assert the invariant on +the data**: a run that detected a missing summary cannot also be `agree`. That is +checkable on the observation, before any rendering, with no parser. + +### Defect 2 -- `efficiency classes: [0]` against `"efficiency_classes":1` + +One fact rendered twice, in two shapes a consumer cannot reconcile: the prose printed the +set of class labels, the NDJSON printed the cardinality, and the numeral `1` reads as a +plausible class *label*. + +Both halves were correct derivations from one consistent value, so no struct-level +predicate was violated. At first reading this looks like the strongest case for an oracle +that reads text -- a contradiction that exists only in the representation. + +But look at how it was actually repaired. The row now emits +`"efficiency_classes":[{}]` -- a list. **The fix was to change what the row publishes.** +The prose comparison was the route by which a reviewer noticed, not the repair. + +### A correction made while writing this up, and it strengthened the case + +The first draft of the Tier 1 decision said the alarm's condition is unpublished and +the verdict can therefore still read `agree`. Checked before committing, and the second +half is **no longer true**: `Observation::cross_check` now pushes +`PartitioningCache::SummaryMissing` onto `parse_incomplete`, which forces the verdict away +from `agree`. That was the original repair, and it worked. + +Writing the superseding decision on a description of the code as it used to be would have +been the exact defect class this branch has spent fifteen review rounds on. What the check +found instead is a gap that is live, and larger: + +The row publishes `not_compared`, `parse_incomplete` and `enumeration_anomalies` as +**counts** -- `check.parse_incomplete.len()` -- where the prose prints each entry's text. A +survey reading `"parse_incomplete":1` cannot tell *the probe detected a bug in itself* from +*a core record contradicted itself* from *cache levels numbered 0* from *this topology was +not measured from a running machine*. Four categorically different facts, one cardinality, +and only the prose separates them. + +So the accurate statement of the problem is not that the row disagrees with the prose. It +is that **the row is impoverished relative to the prose**: the artifact that gets mined +carries strictly less than the artifact that gets read. Which is the engineer's point, +sharpened -- the effort went into comparing the two halves when the encoded half was the +one missing content. + +### Both defects were defects in the encoded data + +That is the session's central finding, and it was not what either the module doc or Tier 1 +said. Defect 1 was a wrong value in the row plus a missing field. Defect 2 was the row +publishing a cardinality where the useful fact was the set. Neither required a +prose-against-NDJSON oracle to *fix*; both required the structured output to be made +right. + +## The cost that was being paid for the other reading + +Counted during the session, in [src/report_oracle.rs](../src/report_oracle.rs): of 38 +top-level functions, ten are correspondence rules and four are comparison helpers -- +and **twenty-three exist purely to extract values back out of rendered text** +(`fingerprint_tokens`, `ndjson_field`, `ndjson_raw_field`, `balanced_end`, `claim_block`, +`prose_field`, `cache_object`, `cache_rows`, `policy_rows`, `leading_digits`, +`has_line_beginning`, `object_keys`, and the rest). + +That ratio is the indictment, and the branch's own review history is the evidence for it. A +large share of PR #88's review rounds were defects in **the reader, not in the thing read**: +a multi-byte panic in `processors_in_banner`, a `p/` substring matching inside an opaque +`io::Error`, `trim_matches` collapsing `[[0]]` and `[0]` to the same value, +`prose_field(" (")` selecting the wrong line when two lines began the same way. + +Those are not defects in the probe. They are defects in a hand-written parser of output +this crate had just finished writing -- a defect source the design invented for itself. + +## The engineer's framing, which the evidence supports + +> There is a data pipeline, and at some point it renders either to a structured format or +> to prose. The prose has to be accurate and readable, but I really do not see value in +> ensuring it is programmatically comparable. The structured format? Yes, absolutely. +> +> Yes, humans read the prose. But we should present prose which you and then optionally I +> will inspect for correspondence, and then once that's done, we'll move along. And there +> may be defects there. But it's the defects in the encoded data that really matter. + +This is the decision recorded in Tier 1. The session's contribution is that it is not +merely a preference about where to spend effort -- the two defects that motivated the +oracle in the first place were both defects in the encoded data, so the evidence that was +taken to argue for reading prose argues for the opposite. + +## Alternatives considered + +**Keep the oracle as it is.** Rejected. It is not wrong, and it does catch real things -- +but it aims the expensive machinery at the artifact with the lower stakes, and pays for it +with twenty-three parser functions that are themselves a defect source. The prose is +reviewable by a human; the row is mined by a machine across a fleet and is what this +workspace's designs rest on. + +**A typed banner (the previous M2.18).** Dissolved into this decision rather than answered +on its own terms. It was the smallest instance of a general question -- should a report be a +value that a writer renders, or a string the renderer concatenates -- and answering it in +isolation would have fixed one parameter while leaving the same shape everywhere else. + +**A structured report checked as a struct, with the oracle moved wholesale onto it.** +Tempting and half right. It closes value-divergence and injection by construction, which is +better than detecting them. But it must not be mistaken for a complete answer: a check on +the struct says nothing about the writer, and the writer is where several of PR #88's +defects lived (`is_attribution_shaped` matching a suffix, the flattening that ate the +disclaimer, the `host: host:` doubling). The conclusion taken was the narrower one -- assert +the invariants on the data, emit the row through one typed writer so injection and +field-order defects are unrepresentable, and keep only a thin check that the row is +well-formed. + +**Rejected framing: "the prose does not matter".** Not what was decided, and worth stating +because it is the easy misreading. The prose must be accurate; an overstated finding in +prose propagates into design notes that cite it, which is a live concern in this crate -- +M2.9 is an open item about exactly that. What was decided is that prose accuracy is a +**human review** obligation rather than a machine-checked correspondence. + +## What survives from PR #88 + +Worth recording, because the decision reads as a larger reversal than it is: + +- The correspondence *rules* survive, relocated. Alarm-against-verdict, + diagnostics-against-verdict and counters-against-verdict all become invariants on the + observation. What they lose is the parser in front of them. +- The containment work in [src/topology_report.rs](../src/topology_report.rs) survives and + matters more under this decision, not less: it stops caller text reaching the row. +- The shape corpus and the fact-accounting instrument survive in shape, re-aimed at the + row's fields rather than at prose labels. +- What retires is the prose-against-NDJSON family and the extraction helpers that serve + only it. From 76896801e52469eda58d59d9130b7df0b6f7ef6f Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 18:39:28 -0400 Subject: [PATCH 02/46] docs(platform-probes): correct two M2 items wrongly called unaffected by M3 --- crates/windows-platform-probes/CHECKLIST.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 1dd122b2a..535ec7078 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -244,7 +244,23 @@ What this milestone changes in M2: - **M2.17 (cross the corpus dimensions) is re-scoped by M3.5** and should be done after it, not before: the dimensions worth crossing are the row's, and crossing prose shapes that are about to stop being checked would be work aimed at the retiring half. -- M2.4, M2.5, M2.7, M2.8, M2.9, M2.13, M2.14, M2.15 and M2.16 are unaffected. +- **M2.4 (explore for further correspondences) is re-scoped by M3.2** and should follow it. The + exploration is still worth doing and its instrument is unchanged, but what it hunts for changes: + invariants over `Coherence`, `BracketOutcome` and `Verdict` as VALUES, and facts the row fails to + publish -- not correspondences between two renderings. Its closing sentence, "promote only what + proves meaningful into the oracle from M2.1", now means the invariant set from M3.2. The open + question attached to it -- whether this generalises past this crate -- survives unchanged and is + arguably sharpened, since a data-level invariant is easier to share than a text reader. +- **M2.15 (second architecture in CI) keeps its conclusion but loses its evidence.** The five + failures it cites were all `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the + correspondence M3.4 retires, so after M3.4 they would not occur and re-running it would look + clean. The underlying point stands on its own and does not need them: CI builds `aarch64` and + never tests it, and architecture is the one shape dimension a corpus cannot vary because it is + fixed at compile time. Restate the item on that basis when it is picked up. +- M2.5, M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are unaffected. M2.9 (a cross-host ratio called + "the finding") and M2.14 (two authoring rules) are if anything reinforced: under this decision + prose accuracy is a review obligation rather than a machine-checked one, which puts more weight on + both. - [ ] **M3.1** -- Publish each diagnostic as itself, not as a count. From 01161974a62dd9fb28f11465a13172d802a2f85a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 18:46:58 -0400 Subject: [PATCH 03/46] docs(platform-probes): close M2 and re-sequence its leftovers into M4 and M5 --- PLANS.md | 2 +- crates/windows-platform-probes/CHECKLIST.md | 356 +++++++++--------- .../COMPLETED-CHECKLIST.md | 29 +- crates/windows-platform-probes/PLANS.md | 2 +- 4 files changed, 205 insertions(+), 184 deletions(-) diff --git a/PLANS.md b/PLANS.md index bacac2dd0..eb5f2c19e 100644 --- a/PLANS.md +++ b/PLANS.md @@ -20,7 +20,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) | in progress | M22-M23: extract the captured-context composite into `windows-thread-ambient-sys`, a standalone platform layer that captures a thread's ambient state and applies it on another thread. M24-M26: `windows-namespace-request-sys`, marshalable Win32 namespace call parameter sets, over a round-one entry list audited from three real consumers (this repository's watcher and enumeration crates, and `MikeGrier/Globazog-rs`) rather than guessed. M27: `windows-platform-probes`, a durable home for the measurements this workspace's designs rest on, under a three-tier scheme (asserted / ignored / binary-only) where every tier is compiled by an ordinary build. Feature-scoped and deleted when complete; it is the whole of the `mikegrier/thread-ambient` branch's work, and is deliberately separate from the deferred namespace-facility items in [CHECKLIST.md](CHECKLIST.md). | [crates/windows-thread-ambient-sys/DESIGN-NOTES.md](crates/windows-thread-ambient-sys/DESIGN-NOTES.md) | | [crates/windows-overlapped-io-sys/CHECKLIST.md](crates/windows-overlapped-io-sys/CHECKLIST.md) | not started | M14: finish the contract audit -- categories 1, 2, 6, 8, 9 were not examined -- and sweep `outstanding()` for the advisory-predicate hazard. | [crates/windows-overlapped-io-sys/DESIGN-NOTES.md](crates/windows-overlapped-io-sys/DESIGN-NOTES.md) | | [crates/windows-ioring-sys/CHECKLIST.md](crates/windows-ioring-sys/CHECKLIST.md) | in progress | Memory-safe Rust over the Windows `IoRing` submission/completion ring, as a new crate. M1-M7 (ring lifecycle through the `ring-copy` topology-aligned sample) are complete and archived. The parked, pinned-thread `M6+` work and the new M10 contract audit remain. | [crates/windows-ioring-sys/DESIGN-NOTES.md](crates/windows-ioring-sys/DESIGN-NOTES.md) | -| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes; nine probe-side and bookkeeping items remain. M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row still publishes its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` cannot tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row still publishes its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` cannot tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | Add a row here when new work is planned, against [CHECKLIST.md](CHECKLIST.md) or any crate's. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 535ec7078..024a87fb7 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -5,31 +5,143 @@ separately, in the workspace [CHECKLIST-thread-ambient.md](../../CHECKLIST-threa M27; that file is feature-scoped and is deleted when its feature completes, so durable follow-up work for the crate belongs here instead. -## M2 -- Check correspondence between the report's parts, not just each part +## M3 -- Make the encoded row the contract, and stop checking the prose against it + +Decided in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is +not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), from the session in +[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). + +The row is a machine contract mined across a fleet; the prose is for a reader. They carry different +obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and +readable**, enforced by review. Nothing is required to hold *between* them. + +Re-checked against the code rather than against M2's account of it: the original defect is fixed, and +what it left behind is larger. The row publishes `not_compared`, `parse_incomplete` and +`enumeration_anomalies` as **counts**, where the prose prints each entry's text. A survey reading +`"parse_incomplete":1` cannot tell *the probe detected a bug in itself* from *a core record +contradicted itself* from *this topology was not measured from a running machine*. **The row is +impoverished relative to the prose** -- the artifact that gets mined carries less than the artifact +that gets read. + +**M2 completed with this decision**, and its ten open items were re-sequenced rather than reworked. +The milestone's own work -- the oracle, the binding, the real-host test, the derived fact set, the +partitioning discriminator and the shape corpus -- is done and archived. The leftovers had +accumulated under a heading none of them fit, and they split by whether M3 gates them: four are in +M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. + +- **M2.18 (typed banner) is dissolved into M3.3**, not carried over. It was the smallest instance + of "should a report be a value a writer renders, or a string the renderer concatenates", and + answering it alone would have typed one parameter while leaving the shape everywhere else. +- **M2.4** is re-scoped by M3.2. The exploration is still worth doing and its instrument is + unchanged, but what it hunts for changes: invariants over `Coherence`, `BracketOutcome` and + `Verdict` as VALUES, and facts the row fails to publish -- not correspondences between two + renderings. Its closing sentence, "promote only what proves meaningful into the oracle from M2.1", + now means the invariant set from M3.2. The open question attached to it -- whether this generalises + past this crate -- survives unchanged and is arguably sharpened, since a data-level invariant is + easier to share than a text reader. +- **M2.5** is gated by M3.1 and M3.3. Establishing that the middle of three discoveries agreed + produces a new FACT, which M3.1 says must reach the row rather than only the banner; and M3.3 + changes how the banner is built. Written first, it would be written into machinery about to move. +- **M2.15** keeps its conclusion but loses its evidence. The five failures it cites were all + `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the correspondence M3.4 retires, + so afterwards they would not occur and a re-run would look clean. The underlying point stands + without them: CI builds `aarch64` and never tests it, and architecture is the one shape dimension + a corpus cannot vary because it is fixed at compile time. Restate it on that basis when picked up. +- **M2.17** is re-scoped by M3.5: the dimensions worth crossing become the row's, and crossing prose + shapes that are about to stop being checked would aim at the retiring half. +- **M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are gated by nothing** and are in M5. M2.9 (a + cross-host ratio called "the finding") and M2.14 (two authoring rules) are if anything reinforced: + under this decision prose accuracy is a review obligation rather than a machine-checked one, which + puts more weight on both. + +- [ ] **M3.1** -- Publish each diagnostic as itself, not as a count. + + `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as + `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- + *which* condition occurred -- exists only in prose. Publish the entries, and give each a stable + machine-readable discriminant rather than the human sentence, so a survey can group by condition + without matching on English that is free to be reworded. The sentences stay in the prose, where + rewording them is harmless. + + **The rule this establishes, which is the durable half:** a renderer may not tell a reader + something the row cannot tell a survey. A cardinality is not a statement of the fact. + +- [ ] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before + rendering. + + Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three + oracle rules that survive the decision. They stop being comparisons of two rendered texts and + become predicates over `Observation` and `CrossCheck` -- `SummaryMissing` implies the verdict is not + `agree`, a non-empty `parse_incomplete` implies the verdict is not `agree`, and so on. No parser is + involved, and the check runs whether or not anything was rendered. + + Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, + restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. + +- [ ] **M3.3** -- Emit the row from a typed value through one writer. + + The row is built today by interpolating eighteen values positionally into a `concat!` template. + Two defect classes follow from that construction and both are closed by replacing it, not by + checking it: + + **Injection.** Measured on PR #88: an `io::Error` containing `{` was selected as the report's + machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text + reaching the mined artifact is contamination of the contract. + + **Field order and labelling.** A reordered value or a miscounted `{}` yields mislabelled data that + still parses, and nothing downstream can detect it. + + A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the + writer here rather than adding a serialization dependency -- this crate has none and the row is + one flat object. This subsumes M2.18: the banner becomes a typed field like any other, and the + question of who may construct one is answered by the row's constructor rather than separately. + +- [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. + + Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence + rules, four are comparison helpers, and **twenty-three exist only to extract values back out of + rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. -A pull-request review found a state where [src/topology_report.rs](src/topology_report.rs) printed -`BUG IN THIS PROBE ... Nothing below about cache partitioning can be trusted` while `cross_check` had -no branch for that state, so the verdict could print `=> agree` two paragraphs below. Twenty-eight -rounds of per-artifact review and a zero-surviving-mutant `cargo-mutants` result had both passed over -it, because every function involved was correct on its own terms and the defect lived in the relation -between two of them. + What stays is a thin check that the row is **well-formed** -- it parses, it carries the expected + key set, and it is the only such line in the report. That is not a correspondence; it is the + writer's own output being checked, and the writer is the one place structure cannot check itself. -See [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The defects that survived were correspondence -failures](DESIGN-NOTES.md#d-correspondence-failures) for why each instrument was structurally -incapable of finding it, and for the matrix-as-exploration / oracle-as-durable split this milestone -implements. + Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for + a format that no longer has two readers. -Every correlation the oracle admits is one the report already renders twice, found either by -catching a contradiction or by walking the artifact field by field. It is not a -speculative list to extend by imagination -- one is added when a contradiction is found, and the -authoritative set is the `Correspondence` enum rather than any count written here. (This said "the -three correlations below" while the enum already had four.) +- [ ] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. -- [x] **M2.1** -- Add a report oracle to this crate: one shared executable definition of the correlations that must hold between the parts of a rendered report. -> [completed 2026-09-10](COMPLETED-CHECKLIST.md#m21) + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates + the facts a report publishes and measures, by mutation, which are read. The instrument is sound and + the target changes: enumerate the row's fields, and require each to be read by an invariant or + explicitly classified as unread. Its corpus of shapes keeps its purpose -- it exists to defeat the + imagination-driven fixture, which the decision does not change. -- [x] **M2.2** -- Route every test that renders a report through the oracle, by binding it in the renderer. -> [completed 2026-09-10](COMPLETED-CHECKLIST.md#m22) + The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on + its own terms rather than against the row. -- [x] **M2.3** -- Run `measure()` against the real host, render the report, and apply the oracle. -> [completed 2026-09-10](COMPLETED-CHECKLIST.md#m23) +- [ ] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. + + Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is + to split unless the module is indivisible. It is not -- it carries current decisions and a large + volume of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture the repository + instructions describe. + + Move the rationale to `DESIGN-RATIONALE.md`, cross-referenced by decision anchor, leaving Tier 1 + stating what was decided and what forced it. The decision added by this milestone is written to be + split that way already, so it is the worked example rather than the hard case. + +## M4 -- Carried over from M2: the items M3 gates + +These were written under M2 and are blocked on M3 above: each one targets the prose-against-row +machinery that M3 retires or relocates, so doing them first means doing them twice. M3's preamble +says which M3 item gates each of them. + +**The IDs keep their M2 numbers deliberately.** [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md) is +append-only and its entries are immutable, and two archived entries already cite M2.4 and M2.14 -- +so renumbering would leave dangling references in a file that may not be edited to repair them. +Stable IDs cost a mismatch between an item number and its milestone; renumbering would cost +correctness in the archive. - [ ] **M2.4** -- Explore, with the sparse matrix as the instrument, whether the same correspondence failures exist for `Coherence`, `BracketOutcome` and `Verdict`, and in the sibling probes' renderers. @@ -74,7 +186,49 @@ three correlations below" while the enum already had four.) explicitly after being pointed at the question. Nothing in the suite decides it either way, which is itself the argument for the oracle. -- [x] **M2.6** -- Say what `GetFullPathNameW` does, in the crate that owns it, and whether it stays. -> [completed 2026-09-09](COMPLETED-CHECKLIST.md#m26) +- [ ] **M2.15** -- Run the probe suite on a second architecture in CI. + + A reviewer asked whether the suite was portable and it was not: three renderer fixtures and the + shape corpus' banner builder each hard-coded `x86_64` while the row they are compared against + publishes `std::env::consts::ARCH`. Measured on `i686-pc-windows-msvc`: five failures, every one + `prose: "x86_64"` against `ndjson: "x86"`. CI BUILDS `aarch64` and never TESTS it, so a + build-and-clippy matrix cannot see this class at all. + + Architecture is the one shape dimension the M2.12 corpus cannot vary, because it is fixed at + compile time rather than chosen per report -- so the corpus that exists precisely to defeat shape + blindness is blind here by construction, and only a second test target can close it. Add one + (`i686-pc-windows-msvc` runs natively on the existing runners; `aarch64` would need its own). + +- [ ] **M2.17** -- Cross the corpus dimensions instead of varying one at a time. + + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs)'s `shapes()` + builds each shape by taking `base()` and changing ONE thing. That makes every shape easy to read and + is why the corpus found what it found -- but it means any renderer branch selected by TWO + dimensions at once is unreachable by construction, and the corpus cannot report the gap because it + does not know the branch exists. + + Measured: `CrossCheck` tags a `parse_incomplete` entry `- {caveat}` under `INCOMPLETE` but + `(parse incomplete) {caveat}` under `DISAGREE`. Anomalies appeared only in an agreeing-counter + shape and disagreements only in a zero-anomaly shape, so the second spelling was never rendered, + and the anomaly-count rule was silently unread on every disagreeing report -- while the comment + above it said it was read for every verdict. One hand-written crossed shape closed it, and the + accounting test went red under sabotage only once that shape existed. + + Enumerate the dimensions the renderer actually branches on (verdict, bracket, coherence, the + partitioning arm, presence of each diagnostic list) and generate the cross product, or a pairwise + covering set if the full product is too slow. The corpus already asserts self-consistency and runs + the fact accounting per shape, so nothing new has to be written to check them -- only to produce + them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly + the imagination-driven process M2.12 exists to replace. + + +## M5 -- Carried over from M2: unblocked hygiene + +**Nothing gates these.** They are grouped last by priority, not by dependency -- none of them touches +the report pipeline, so any of them may be pulled forward ahead of M3 or M4 at any time. They were +discovered during M2 and parked there under a heading none of them fit. + +IDs keep their M2 numbers, for the reason given under M4. - [ ] **M2.7** -- Decide whether the other nine probe steps in CI should carry `if: '!cancelled()'`, and apply or record the decision. @@ -136,10 +290,6 @@ three correlations below" while the enum already had four.) and cheap. Same defect class as PR #86's subject -- a claim stated more strongly than the evidence supports -- so whichever is chosen, the wording has to end up matching what the numbers can carry. -- [x] **M2.10** -- Derive the oracle's set of checked facts from the renderer instead of extending it by hand. -> [completed 2026-09-10](COMPLETED-CHECKLIST.md#m210) -- [x] **M2.11** -- Compare the `outermost_partitioning_cache` discriminator against the prose conclusion. -> [completed 2026-09-10](COMPLETED-CHECKLIST.md#m211) - -- [x] **M2.12** -- Validate the oracle and its instruments against a corpus of report SHAPES generated from the renderer. -> [completed 2026-09-11](COMPLETED-CHECKLIST.md#m212) - [ ] **M2.13** -- Lint the completed-checklist archive mechanically in CI. Three bookkeeping defects reached review on this branch, and all three are decidable by a script: a @@ -151,6 +301,7 @@ three correlations below" while the enum already had four.) `- [ ]` remains; and the file has ZERO deleted lines against the merge base. The last one is the append-only invariant, and it is the one a human reviewer is least likely to notice. + - [ ] **M2.14** -- Write two authoring rules into the repository instructions, both earned on this branch. @@ -165,19 +316,6 @@ three correlations below" while the enum already had four.) `[1]` matched nothing because the report rendered `[0]`. Sabotage belongs at authoring time, not at review time. -- [ ] **M2.15** -- Run the probe suite on a second architecture in CI. - - A reviewer asked whether the suite was portable and it was not: three renderer fixtures and the - shape corpus' banner builder each hard-coded `x86_64` while the row they are compared against - publishes `std::env::consts::ARCH`. Measured on `i686-pc-windows-msvc`: five failures, every one - `prose: "x86_64"` against `ndjson: "x86"`. CI BUILDS `aarch64` and never TESTS it, so a - build-and-clippy matrix cannot see this class at all. - - Architecture is the one shape dimension the M2.12 corpus cannot vary, because it is fixed at - compile time rather than chosen per report -- so the corpus that exists precisely to defeat shape - blindness is blind here by construction, and only a second test target can close it. Add one - (`i686-pc-windows-msvc` runs natively on the existing runners; `aarch64` would need its own). - - [ ] **M2.16** -- Repair the garbled `Report` doc comment, and drop the two counts that have already rotted beside it. @@ -194,147 +332,3 @@ three correlations below" while the enum already had four.) passage is actually arguing -- that `String` already implements `fmt::Write`, so every existing write site stands untouched and only the renderer signatures move -- which is what makes the point and cannot rot. This is the same defect class as M2.14's first authoring rule. - -- [ ] **M2.17** -- Cross the corpus dimensions instead of varying one at a time. - - [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs)'s `shapes()` - builds each shape by taking `base()` and changing ONE thing. That makes every shape easy to read and - is why the corpus found what it found -- but it means any renderer branch selected by TWO - dimensions at once is unreachable by construction, and the corpus cannot report the gap because it - does not know the branch exists. - - Measured: `CrossCheck` tags a `parse_incomplete` entry `- {caveat}` under `INCOMPLETE` but - `(parse incomplete) {caveat}` under `DISAGREE`. Anomalies appeared only in an agreeing-counter - shape and disagreements only in a zero-anomaly shape, so the second spelling was never rendered, - and the anomaly-count rule was silently unread on every disagreeing report -- while the comment - above it said it was read for every verdict. One hand-written crossed shape closed it, and the - accounting test went red under sabotage only once that shape existed. - - Enumerate the dimensions the renderer actually branches on (verdict, bracket, coherence, the - partitioning arm, presence of each diagnostic list) and generate the cross product, or a pairwise - covering set if the full product is too slow. The corpus already asserts self-consistency and runs - the fact accounting per shape, so nothing new has to be written to check them -- only to produce - them. Until this lands, a shape that needs two dimensions must be added by hand, which is exactly - the imagination-driven process M2.12 exists to replace. - - -## M3 -- Make the encoded row the contract, and stop checking the prose against it - -Decided in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is -not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), from the session in -[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). - -The row is a machine contract mined across a fleet; the prose is for a reader. They carry different -obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and -readable**, enforced by review. Nothing is required to hold *between* them. - -Re-checked against the code rather than against M2's account of it: the original defect is fixed, and -what it left behind is larger. The row publishes `not_compared`, `parse_incomplete` and -`enumeration_anomalies` as **counts**, where the prose prints each entry's text. A survey reading -`"parse_incomplete":1` cannot tell *the probe detected a bug in itself* from *a core record -contradicted itself* from *this topology was not measured from a running machine*. **The row is -impoverished relative to the prose** -- the artifact that gets mined carries less than the artifact -that gets read. - -What this milestone changes in M2: - -- **M2.18 (typed banner) is dissolved into M3.3.** It was the smallest instance of "should a report - be a value a writer renders, or a string the renderer concatenates", and answering it alone would - have typed one parameter while leaving the shape everywhere else. -- **M2.17 (cross the corpus dimensions) is re-scoped by M3.5** and should be done after it, not - before: the dimensions worth crossing are the row's, and crossing prose shapes that are about to - stop being checked would be work aimed at the retiring half. -- **M2.4 (explore for further correspondences) is re-scoped by M3.2** and should follow it. The - exploration is still worth doing and its instrument is unchanged, but what it hunts for changes: - invariants over `Coherence`, `BracketOutcome` and `Verdict` as VALUES, and facts the row fails to - publish -- not correspondences between two renderings. Its closing sentence, "promote only what - proves meaningful into the oracle from M2.1", now means the invariant set from M3.2. The open - question attached to it -- whether this generalises past this crate -- survives unchanged and is - arguably sharpened, since a data-level invariant is easier to share than a text reader. -- **M2.15 (second architecture in CI) keeps its conclusion but loses its evidence.** The five - failures it cites were all `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the - correspondence M3.4 retires, so after M3.4 they would not occur and re-running it would look - clean. The underlying point stands on its own and does not need them: CI builds `aarch64` and - never tests it, and architecture is the one shape dimension a corpus cannot vary because it is - fixed at compile time. Restate the item on that basis when it is picked up. -- M2.5, M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are unaffected. M2.9 (a cross-host ratio called - "the finding") and M2.14 (two authoring rules) are if anything reinforced: under this decision - prose accuracy is a review obligation rather than a machine-checked one, which puts more weight on - both. - -- [ ] **M3.1** -- Publish each diagnostic as itself, not as a count. - - `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as - `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- - *which* condition occurred -- exists only in prose. Publish the entries, and give each a stable - machine-readable discriminant rather than the human sentence, so a survey can group by condition - without matching on English that is free to be reworded. The sentences stay in the prose, where - rewording them is harmless. - - **The rule this establishes, which is the durable half:** a renderer may not tell a reader - something the row cannot tell a survey. A cardinality is not a statement of the fact. - -- [ ] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before - rendering. - - Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three - oracle rules that survive the decision. They stop being comparisons of two rendered texts and - become predicates over `Observation` and `CrossCheck` -- `SummaryMissing` implies the verdict is not - `agree`, a non-empty `parse_incomplete` implies the verdict is not `agree`, and so on. No parser is - involved, and the check runs whether or not anything was rendered. - - Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, - restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. - -- [ ] **M3.3** -- Emit the row from a typed value through one writer. - - The row is built today by interpolating eighteen values positionally into a `concat!` template. - Two defect classes follow from that construction and both are closed by replacing it, not by - checking it: - - **Injection.** Measured on PR #88: an `io::Error` containing `{` was selected as the report's - machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text - reaching the mined artifact is contamination of the contract. - - **Field order and labelling.** A reordered value or a miscounted `{}` yields mislabelled data that - still parses, and nothing downstream can detect it. - - A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the - writer here rather than adding a serialization dependency -- this crate has none and the row is - one flat object. This subsumes M2.18: the banner becomes a typed field like any other, and the - question of who may construct one is answered by the row's constructor rather than separately. - -- [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. - - Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence - rules, four are comparison helpers, and **twenty-three exist only to extract values back out of - rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. - - What stays is a thin check that the row is **well-formed** -- it parses, it carries the expected - key set, and it is the only such line in the report. That is not a correspondence; it is the - writer's own output being checked, and the writer is the one place structure cannot check itself. - - Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for - a format that no longer has two readers. - -- [ ] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. - - [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates - the facts a report publishes and measures, by mutation, which are read. The instrument is sound and - the target changes: enumerate the row's fields, and require each to be read by an invariant or - explicitly classified as unread. Its corpus of shapes keeps its purpose -- it exists to defeat the - imagination-driven fixture, which the decision does not change. - - The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on - its own terms rather than against the row. - -- [ ] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. - - Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is - to split unless the module is indivisible. It is not -- it carries current decisions and a large - volume of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture the repository - instructions describe. - - Move the rationale to `DESIGN-RATIONALE.md`, cross-referenced by decision anchor, leaving Tier 1 - stating what was decided and what forced it. The decision added by this milestone is written to be - split that way already, so it is the worked example rather than the hard case. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 820fe6d2f..f3c4c5b8c 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -495,4 +495,31 @@ request as it was written, and quotes the module doc as it read before the corre is now `every_fact_is_accounted_for_on_every_representative_shape`. Both said "every" while `shapes()` is a sample, so a green run read as proof of coverage it does not have. The old names are left standing above because this file is history; this line is how a reader following them - finds where they went.)* \ No newline at end of file + finds where they went.)* + +## Moved 2026-09-12 -- M2 completes: the report oracle, its fact set and its shape corpus + +M2's own work is done. Its completed items -- M2.1 (the oracle), M2.2 (the renderer binding), +M2.3 (the real-host test), M2.6 (`GetFullPathNameW`), M2.10 (the derived fact set), M2.11 (the +partitioning discriminator) and M2.12 (the shape corpus) -- were each archived above as they landed, +so their stubs in [CHECKLIST.md](CHECKLIST.md) carried nothing this file does not already hold and +were deleted with the milestone. + +**The ten open items were re-sequenced, not reworked.** They had accumulated under a heading none of +them fit -- a CI `if:` condition and a doc-comment repair are not correspondence work -- and they +split by whether [DESIGN-NOTES.md](DESIGN-NOTES.md) -> +[#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) gates them: + +- **M4** (gated on M3): M2.4, M2.5, M2.15, M2.17. +- **M5** (gated on nothing): M2.7, M2.8, M2.9, M2.13, M2.14, M2.16. +- **M2.18 is dissolved** into M3.3 rather than moved. It asked whether a banner should be a type; + M3.3 answers the general form of that question, and answering the banner alone would have typed + one parameter while leaving the shape everywhere else. + +**Their IDs deliberately keep the `M2.` prefix.** This file is append-only and its entries are +immutable, and two entries above already cite M2.4 and M2.14 -- so renumbering would leave dangling +references here that may not be edited to repair them. A stable ID costs a mismatch between an item +number and its milestone heading; renumbering would cost correctness in the archive. + +*(Recorded 2026-09-12 18:45:27 -04:00. This entry closes a milestone rather than completing an item, +so it carries no `###` item heading and nothing links to it by anchor.)* diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index 17c739edc..d5fe44d06 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; the oracle, the derived fact set and the shape corpus landed, and nine probe-side and bookkeeping items remain. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row publishes its three diagnostic lists as bare counts, so a survey cannot tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row publishes its three diagnostic lists as bare counts, so a survey cannot tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | From 658a6317cdfa37dd915160368177bcbaf10947ee Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 19:29:31 -0400 Subject: [PATCH 04/46] feat(platform-probes)!: publish each diagnostic as itself, not as a count Completed item: M3.1: Publish each diagnostic as itself, not as a count. The row published `not_compared`, `parse_incomplete` and `enumeration_anomalies` as `.len()`, so it said a run was in doubt without saying why. A survey reading `"parse_incomplete":1` could not tell `partitioning_summary_missing` -- this probe detecting a bug in ITSELF -- from `contradictory_cores` or `not_measured`. Four categorically different facts, one cardinality, and only the prose separated them. That is the gap recorded in DESIGN-NOTES.md -> #d-encoded-row-is-the-contract: the row was impoverished relative to the prose, which is backwards given that the row is what a fleet survey mines and what the designs rest on. **The vocabulary is now three enums** in `topology::diagnostic` -- 21 `ParseIncomplete`, 6 `NotCompared`, 3 `Disagreement` variants, one per condition that `cross_check` can report. Each carries its own data, renders its own sentence through `Display`, and names itself through `code()`. `CrossCheck`'s three `Vec` became `Vec`, and all 30 push sites construct a variant rather than formatting a string. One definition, so the code and the sentence cannot drift apart -- where a parallel `(code, String)` pair could be updated on one side only. **The code is the contract; the sentence is not.** Changing a `code` is a breaking change to the row, exactly as renaming a field would be. The `Display` text reaches only the prose, where a reader is the consumer and rewording is harmless. That is what lets the prose be written for a human: before, a test or a survey wanting to know which condition fired had to match on English, which is a reason not to improve it. 34 assertions did exactly that and now match on the variant. **The wire format changed, deliberately and not additively.** `"parse_incomplete":1` is now `"parse_incomplete":["partitioning_summary_missing"]`. The count is still available as the list's length, so nothing is lost, and publishing both would be a restatement that can drift. Same correction, and the same argument, as the `efficiency_classes` count-to-list change that preceded it. Marked `!` for that reason: any consumer reading those three fields as numbers must change. The prose is byte-identical. The renderer's loops write `{entry}` and `Display` emits the sentences that were there before. **Which conditions are listed is deliberately not compared against the prose, and needs no rule.** The code and the sentence come from one variant, so there is no second implementation for them to disagree through: the correspondence holds by construction, which is stronger than a check. What remains checkable, and is checked, is that both renderings list the same NUMBER. That was found rather than assumed. Converting the accounting instrument, a mutation that swapped one code for another went unnoticed on the `verdict incomplete` corpus shape -- because the oracle reads the length, and a one-element list rewritten to a different one-element list has the same length. `corruptions` now APPENDS a code instead of substituting one, so the length differs by one whatever the host rendered. `ndjson_count` was retired rather than left unused: with the three fields now lists, it had no remaining consumer, and a dead reader is a second grammar for a format that no longer has two. Verified. 249 lib tests plus 14 ignored, 9 integration tests and doctests, on both `x86_64-pc-windows-msvc` and `i686-pc-windows-msvc`. Workspace `cargo check --all-targets` clean in debug and release, `cargo doc` clean under all three rustdoc deny lints, clippy --all-targets --all-features clean, fmt clean, encoding clean over 635 files. A real `probe-topology` run emits `"not_compared":[],"parse_incomplete":[],"enumeration_anomalies":[]`. The four new tests were each observed to fail, with the mutation injected on its own line and reverted: renaming `PartitioningSummaryMissing`'s code reddens only `the_row_names_the_probes_own_bug_when_it_detects_one`; making the row keep only the first condition reddens both list tests, through the bound oracle's count rule; mislabelling `TrailingBytes` reddens only `an_anomaly_reaches_the_row_as_its_kind`. Swept the claim this change falsified: DESIGN-NOTES.md stated in two places that the row publishes `parse_incomplete` as a count, and M3's checklist preamble stated it as a present gap. All three corrected in this commit rather than left to be found later. The design-session record keeps its present tense, being a dated account of that session. `AnomalyKind` is `#[non_exhaustive]`, so `anomaly_code` needs a catch-all and a variant added upstream lands in it. That is stated rather than hidden: `unclassified` means "this probe's vocabulary is older than the crate's", which is more useful to a survey than a code invented here that pretends to name the new kind. Deliberately not a compile error -- owning the row's vocabulary means this crate decides when a new upstream kind earns a code. M3.3 picks up what this left: the row carries a condition's code but not the values its variant holds, because the row is still a positional `concat!` template where a nested per-entry object has to be hand-assembled. Queued there rather than dropped, and the blocker is the row's shape, not the want of a consumer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 52 +- .../windows-platform-probes/DESIGN-NOTES.md | 50 +- .../src/report_oracle.rs | 62 ++- .../src/report_oracle/tests.rs | 61 +-- crates/windows-platform-probes/src/tests.rs | 329 +++++++++--- .../windows-platform-probes/src/topology.rs | 281 +++++------ .../src/topology/diagnostic.rs | 474 ++++++++++++++++++ .../src/topology_report.rs | 49 +- .../tests/a_real_report_agrees_with_itself.rs | 65 ++- 9 files changed, 1104 insertions(+), 319 deletions(-) create mode 100644 crates/windows-platform-probes/src/topology/diagnostic.rs diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 024a87fb7..976a58c3d 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -15,13 +15,14 @@ The row is a machine contract mined across a fleet; the prose is for a reader. T obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and readable**, enforced by review. Nothing is required to hold *between* them. -Re-checked against the code rather than against M2's account of it: the original defect is fixed, and -what it left behind is larger. The row publishes `not_compared`, `parse_incomplete` and -`enumeration_anomalies` as **counts**, where the prose prints each entry's text. A survey reading -`"parse_incomplete":1` cannot tell *the probe detected a bug in itself* from *a core record -contradicted itself* from *this topology was not measured from a running machine*. **The row is -impoverished relative to the prose** -- the artifact that gets mined carries less than the artifact -that gets read. +Re-checked against the code rather than against M2's account of it: the original defect was fixed, +and what it left behind was larger. The row published `not_compared`, `parse_incomplete` and +`enumeration_anomalies` as **counts**, where the prose printed each entry's text. A survey reading +`"parse_incomplete":1` could not tell *the probe detected a bug in itself* from *a core record +contradicted itself* from *this topology was not measured from a running machine*. **The row was +impoverished relative to the prose** -- the artifact that gets mined carried less than the artifact +that gets read. M3.1 has since closed that particular gap; the rest of the milestone is about which +artifact carries the contract, and stands whole. **M2 completed with this decision**, and its ten open items were re-sequenced rather than reworked. The milestone's own work -- the oracle, the binding, the real-host test, the derived fact set, the @@ -54,7 +55,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. under this decision prose accuracy is a review obligation rather than a machine-checked one, which puts more weight on both. -- [ ] **M3.1** -- Publish each diagnostic as itself, not as a count. +- [x] **M3.1** -- Publish each diagnostic as itself, not as a count. `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- @@ -66,6 +67,31 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. **The rule this establishes, which is the durable half:** a renderer may not tell a reader something the row cannot tell a survey. A cardinality is not a statement of the fact. + **Done.** Three enums in `topology::diagnostic` -- 21 + 6 + 3 variants, one per condition -- each + carrying its data, rendering its sentence through `Display`, and naming itself through `code()`. + `CrossCheck`'s three `Vec` became `Vec`, and the row publishes arrays of codes + where it published `.len()`. The prose is byte-identical: the loops write `{entry}` and `Display` + emits the same sentences. + + **The wire format changed**, deliberately and not additively: `"parse_incomplete":1` is now + `"parse_incomplete":["partitioning_summary_missing"]`. The count is still available as the list's + length, so nothing is lost, and publishing both would be a restatement that can drift. Same shape + as the `efficiency_classes` correction that preceded it. + + Sabotage-verified, each mutation injected on its own line and reverted: renaming + `PartitioningSummaryMissing`'s code reddens only + `the_row_names_the_probes_own_bug_when_it_detects_one`; making the row keep only the first + condition reddens the two list tests, through the bound oracle's count rule; mislabelling + `TrailingBytes` reddens only `an_anomaly_reaches_the_row_as_its_kind`. + + **Which conditions are listed is deliberately not compared against the prose.** The code and the + sentence come from one variant, so there is no second implementation to disagree through -- the + correspondence holds by construction, which is stronger than a check. What remains checkable, and + is checked, is that both renderings list the same NUMBER. Found while converting the accounting + instrument: a mutation that swapped one code for another went unnoticed on the + `verdict incomplete` shape, because the oracle reads the length. `corruptions` now APPENDS a code + rather than substituting one, so the length always differs. + - [ ] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before rendering. @@ -93,7 +119,15 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the writer here rather than adding a serialization dependency -- this crate has none and the row is - one flat object. This subsumes M2.18: the banner becomes a typed field like any other, and the + one flat object. + + **Carry each diagnostic's DATA, which M3.1 left behind.** M3.1 publishes a condition's code but + not the values its variant holds -- a survey learns `contradictory_cores` without learning that + three cores contradicted themselves. The variants already carry those values, for `Display`; what + stopped M3.1 publishing them is that the row is still a positional `concat!` template, where a + nested per-entry object has to be hand-assembled. Once the row is typed this is a field like any + other. Not deferred for want of a consumer -- the shape of the row is the blocker, and it is this + item. This subsumes M2.18: the banner becomes a typed field like any other, and the question of who may construct one is answered by the row's constructor rather than separately. - [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 93134363f..03487d422 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1214,10 +1214,17 @@ the report's own published evidence of doubt. That last one is the interesting boundary. `CrossCheck::parse_in_doubt` is `!disagreements.is_empty() || !parse_incomplete.is_empty()`, and the NDJSON -publishes `parse_incomplete` as a **count** rather than the predicate -- so the -oracle reads the count and the `disagree` verdict, which are the two visible -shadows of that definition. The coupling is deliberate, and confirming it still -holds is what M2.2's sabotage check is for when the call sites are bound. +publishes `parse_incomplete` as a **list of conditions** rather than the +predicate -- so the oracle reads whether that list is empty, together with the +`disagree` verdict, and those are the two visible shadows of that definition. The +coupling is deliberate, and confirming it still holds is what M2.2's sabotage +check is for when the call sites are bound. + +(This said "as a **count**", which M3.1 made false when the three diagnostic +fields began publishing their conditions. The shape of the argument is +unchanged -- the row still renders a shadow of the predicate rather than the +predicate -- but the shadow is now a list, and an emptiness test rather than a +comparison against `0`.) **Half the tests assert acceptance**, following [../windows-file-watcher/src/contract.rs](../windows-file-watcher/src/contract.rs)'s @@ -1493,21 +1500,28 @@ mining that row would have been wrong and had no way to know. The prose alarm wa not the defect; it was the only trace that the row was wrong, which is why a human found it and no instrument did. -**That defect is fixed, and what it left behind is the live gap.** Checked +**That defect is fixed, and what it left behind was the live gap.** Checked rather than assumed, because the paragraph above describes the code as it was: -`Observation::cross_check` now pushes `PartitioningCache::SummaryMissing` onto -`parse_incomplete`, which forces the verdict away from `agree`, so the row can no -longer certify that run. But the row publishes `parse_incomplete` as a **count** --- as it does `not_compared` and `enumeration_anomalies` -- where the prose -publishes each entry's text. A survey reading `"parse_incomplete":1` cannot tell -*the probe detected a bug in itself* from *a core record contradicted itself* -from *this topology was not measured from a running machine*. Those are -categorically different facts, and only the prose distinguishes them. - -So the shape of the problem is not that the row is out of step with the prose. It -is that **the row is impoverished relative to the prose** -- the artifact that -gets mined carries less than the artifact that gets read -- which is backwards -given which of the two the designs rest on. +`Observation::cross_check` pushes `PartitioningCache::SummaryMissing` onto +`parse_incomplete`, which forces the verdict away from `agree`, so the row could +not certify that run. But the row published `parse_incomplete` as a **count** -- +as it did `not_compared` and `enumeration_anomalies` -- where the prose published +each entry's text. A survey reading `"parse_incomplete":1` could not tell *the +probe detected a bug in itself* from *a core record contradicted itself* from +*this topology was not measured from a running machine*. Those are categorically +different facts, and only the prose distinguished them. + +So the shape of the problem was not that the row is out of step with the prose. +It is that **the row was impoverished relative to the prose** -- the artifact +that gets mined carried less than the artifact that gets read -- which is +backwards given which of the two the designs rest on. + +**M3.1 closed this**, and the past tense above is deliberate: the three fields +now publish arrays of condition codes minted by `topology::diagnostic`, so +`"parse_incomplete":["partitioning_summary_missing"]` tells a survey which +condition fired. The count remains available as the list's length. The rest of +this decision is unaffected -- it is about which artifact carries the contract, +not about these three fields. **`efficiency classes: [0]` against `"efficiency_classes":1`.** Both halves were correct derivations of one consistent value -- the prose rendered the set, the row diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 1b9b3a082..6e53a58ef 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -431,11 +431,15 @@ fn check_diagnostics_against_verdict( // does. Measured, before this: `"cross_check":"agree"` beside // `"not_compared":3` was accepted with no violation. Found by a review. for key in ["parse_incomplete", "enumeration_anomalies", "not_compared"] { - if let Some(count) = ndjson_field(ndjson, key) - && count != "0" + // The alarm now NAMES the conditions rather than counting them, so + // a violation reads `"parse_incomplete":["partitioning_summary_missing"]` + // instead of `"parse_incomplete":1` -- which is the whole point of + // the change, applied to the oracle's own output. + if let Some(raw) = ndjson_raw_field(ndjson, key) + && ndjson_list_len(ndjson, key).is_some_and(|entries| entries > 0) { found.push(Correspondence::AlarmWithAgreeingVerdict { - alarm: format!("\"{key}\":{count}"), + alarm: format!("\"{key}\":{raw}"), verdict_source: "ndjson", }); } @@ -455,11 +459,17 @@ fn check_diagnostics_against_verdict( // contradiction with the verdict; this is about the two renderings of one // number, which must agree whatever the verdict says. if let Some(prose_anomalies) = anomaly_count_in_prose(report) { + // The row lists each anomaly's code now, so the count it is compared + // against is the list's LENGTH. Rendered back to a string rather than + // compared as a number, because `compare` reports absence too and the + // prose count arrives as text. compare( found, "enumeration anomaly count", prose_anomalies, - ndjson_field(ndjson, "enumeration_anomalies"), + ndjson_list_len(ndjson, "enumeration_anomalies") + .map(|entries| entries.to_string()) + .as_deref(), ); } @@ -485,8 +495,8 @@ fn check_diagnostics_against_verdict( ), ] { let listed = prose_lines_beginning(report, label); - match ndjson_field(ndjson, key) { - Some(json) => compare_counts(found, fact, listed, json.parse().unwrap_or_default()), + match ndjson_list_len(ndjson, key) { + Some(entries) => compare_counts(found, fact, listed, entries), None if listed > 0 => found.push(Correspondence::RenderedOnlyInProse { fact, prose: listed.to_string(), @@ -499,8 +509,8 @@ fn check_diagnostics_against_verdict( if has_line_beginning(report, "=> INCOMPLETE") { let listed = prose_lines_beginning(report, " - "); let (Some(skipped), Some(caveats)) = ( - ndjson_count(ndjson, "not_compared"), - ndjson_count(ndjson, "parse_incomplete"), + ndjson_list_len(ndjson, "not_compared"), + ndjson_list_len(ndjson, "parse_incomplete"), ) else { // **The prose has already listed the entries here.** This arm sums // two fields, so it was written to return unless BOTH are present -- @@ -544,8 +554,29 @@ fn prose_lines_beginning(report: &str, prefix: &str) -> usize { } /// An NDJSON field read as a count, or `None` when it renders no number. -fn ndjson_count(ndjson: &str, key: &str) -> Option { - ndjson_field(ndjson, key)?.parse().ok() +/// How many entries a list-valued field carries. +/// +/// The three diagnostic fields were scalars and are now arrays of condition +/// codes, so every rule that compared a count against them asks for the length +/// here instead of parsing a number. The count is still the fact those rules +/// check; it is simply derived from the list rather than restated beside it. +/// +/// Splitting on `,` is safe for these fields and only these: a code is an +/// identifier this crate mints in `topology::diagnostic`, so it carries no +/// comma and no nesting. Pointing this at `caches`, whose entries are objects, +/// would count members rather than entries -- `cache_rows` exists for that. +fn ndjson_list_len(ndjson: &str, key: &str) -> Option { + let raw = ndjson_raw_field(ndjson, key)?; + if !raw.starts_with('[') { + return None; + } + + let inner = normalise_list(raw); + Some(if inner.is_empty() { + 0 + } else { + inner.split(',').count() + }) } /// Push a disagreement between two counts of the same thing. @@ -1613,10 +1644,15 @@ fn check_claims_against_doubt(report: &str, ndjson: Option<&str>, found: &mut Ve // rather than a coincidence: if the definition changes and this does not, // the sabotage check in M2.2 is what should notice. let mut evidence = Vec::new(); - if let Some(count) = ndjson_field(ndjson, "parse_incomplete") - && count != "0" + if let Some(entries) = ndjson_list_len(ndjson, "parse_incomplete") + && entries > 0 { - evidence.push(format!("parse_incomplete={count}")); + // Names the conditions rather than counting them, so the violation + // message says WHY the parse was in doubt. + evidence.push(format!( + "parse_incomplete={}", + ndjson_raw_field(ndjson, "parse_incomplete").unwrap_or_default() + )); } if ndjson_field(ndjson, "cross_check") == Some("disagree") { evidence.push("cross_check=disagree".to_owned()); diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index dd8c5b083..d56c1332f 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -80,7 +80,7 @@ fn clean_report() -> String { " GetActiveProcessorGroupCount: 1", " GetNumaHighestNodeNumber : 0", " => agree. Every check this probe could make was made and matched.", - r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16,"groups":1,"packages":1,"numa_domains":1,"numa_domains_without_processors":0,"cores":8,"efficiency_classes":[0],"caches":[{"level":1,"domains":8},{"level":3,"domains":1}],"outermost_partitioning_cache_level":1,"outermost_partitioning_cache":"level","policies":{"single":1,"by-core":8},"cross_check":"agree","parse_incomplete":0}"#, + r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16,"groups":1,"packages":1,"numa_domains":1,"numa_domains_without_processors":0,"cores":8,"efficiency_classes":[0],"caches":[{"level":1,"domains":8},{"level":3,"domains":1}],"outermost_partitioning_cache_level":1,"outermost_partitioning_cache":"level","policies":{"single":1,"by-core":8},"cross_check":"agree","parse_incomplete":[]}"#, ] .join("\n") } @@ -206,7 +206,7 @@ fn a_bare_hardware_claim_under_an_incomplete_parse_is_a_violation() { " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", ) .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#) - .replace(r#""parse_incomplete":0"#, r#""parse_incomplete":2"#); + .replace(r#""parse_incomplete":[]"#, r#""parse_incomplete":["no_cores","no_packages"]"#); let violations = check(&report); @@ -480,8 +480,8 @@ fn a_hardware_claim_with_its_caveat_under_doubt_is_accepted() { .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":2,"not_compared":0,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":["no_cores","no_packages"],"not_compared":[],"enumeration_anomalies":[]}"#, ); assert_eq!(check(&report), Vec::new()); @@ -937,12 +937,15 @@ fn an_agreeing_verdict_beside_a_nonzero_parse_incomplete_is_a_violation() { // `cross_check == "agree"` implies no record failed to decode. An `agree` // beside a nonzero count is therefore the report contradicting its own // published rule, in the field a mining pass trusts before any other. - let report = clean_report().replace(r#""parse_incomplete":0"#, r#""parse_incomplete":2"#); + let report = clean_report().replace( + r#""parse_incomplete":[]"#, + r#""parse_incomplete":["no_cores","no_packages"]"#, + ); assert_eq!( check(&report), vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""parse_incomplete":2"#.to_owned(), + alarm: r#""parse_incomplete":["no_cores","no_packages"]"#.to_owned(), verdict_source: "ndjson", }], "a parse that did not complete cannot sit beside a verdict saying every \ @@ -955,14 +958,14 @@ fn an_agreeing_verdict_beside_a_nonzero_anomaly_count_is_a_violation() { // The same rule reached through the other field it names. Anomalies populate // `parse_incomplete`, so an `agree` verdict rules both out. let report = clean_report().replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":0,"enumeration_anomalies":1}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":[],"enumeration_anomalies":["undersized"]}"#, ); assert_eq!( check(&report), vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""enumeration_anomalies":1"#.to_owned(), + alarm: r#""enumeration_anomalies":["undersized"]"#.to_owned(), verdict_source: "ndjson", }], "a dropped enumeration record cannot sit beside an agreeing verdict" @@ -987,8 +990,8 @@ fn a_not_compared_count_the_two_renderings_disagree_about_is_a_violation() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":0,"not_compared":4,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":[],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed","active_processor_count_failed"],"enumeration_anomalies":[]}"#, ); assert_eq!( @@ -1017,8 +1020,8 @@ fn an_incomplete_verdict_listing_fewer_entries_than_it_counts_is_a_violation() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":3,"not_compared":0,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":["no_cores","no_packages","not_measured"],"not_compared":[],"enumeration_anomalies":[]}"#, ); assert_eq!( @@ -1048,8 +1051,8 @@ fn a_nonzero_not_compared_beside_an_incomplete_verdict_is_accepted() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":0,"not_compared":1,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":[],"not_compared":["machine_changed"],"enumeration_anomalies":[]}"#, ); assert_eq!( @@ -1384,17 +1387,17 @@ fn an_anomaly_count_the_two_renderings_disagree_about_is_a_violation() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":1,"not_compared":0,"enumeration_anomalies":99}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer","trailing_bytes","truncated_array","undersized"]}"#, ); assert!( check(&report).contains(&Correspondence::ProseAndNdjsonDisagree { fact: "enumeration anomaly count", prose: "2".to_owned(), - ndjson: "99".to_owned(), + ndjson: "5".to_owned(), }), - "the prose says it recorded 2 and the field publishes 99: {:#?}", + "the prose says it recorded 2 and the field lists 5: {:#?}", check(&report) ); } @@ -1411,8 +1414,8 @@ fn an_anomaly_count_both_renderings_agree_about_is_accepted() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":1,"not_compared":0,"enumeration_anomalies":2}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer"]}"#, ); assert_eq!( @@ -1507,14 +1510,14 @@ fn an_agreeing_verdict_beside_skipped_work_is_a_violation() { // shape as a nonzero `parse_incomplete` beside `agree`, which this module // already read. Found by a review. let report = clean_report().replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":0,"not_compared":3,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":[],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed"],"enumeration_anomalies":[]}"#, ); assert_eq!( check(&report), vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""not_compared":3"#.to_owned(), + alarm: r#""not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed"]"#.to_owned(), verdict_source: "ndjson", }], "work the probe skipped cannot sit beside a verdict saying every check \ @@ -1794,8 +1797,8 @@ fn an_anomaly_sentence_with_no_count_names_no_count() { ) .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":1,"not_compared":0,"enumeration_anomalies":3}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer","trailing_bytes"]}"#, ); assert!( @@ -1870,8 +1873,8 @@ fn a_marker_in_the_banner_is_not_the_probe_speaking() { let anomalies = clean_report() .replace( - r#""parse_incomplete":0}"#, - r#""parse_incomplete":0,"not_compared":0,"enumeration_anomalies":0}"#, + r#""parse_incomplete":[]}"#, + r#""parse_incomplete":[],"not_compared":[],"enumeration_anomalies":[]}"#, ) .replace( "host: x86_64 16p/8c", @@ -2110,7 +2113,7 @@ fn a_caveat_in_the_banner_does_not_excuse_an_uncaveated_claim() { // caveat sentence to the banner made `UncaveatedClaimUnderDoubt` vanish from // a report that still carried the claim and still said `parse_incomplete=2`. let claimed = clean_report() - .replace(r#""parse_incomplete":0}"#, r#""parse_incomplete":2}"#) + .replace(r#""parse_incomplete":[]}"#, r#""parse_incomplete":["no_cores","no_packages"]}"#) .replace( " efficiency classes: [0]", " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index a53d9a232..304c3f607 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -850,7 +850,13 @@ fn a_numa_node_the_topology_crate_never_saw_is_still_reported() { "{check:?}" ); assert_eq!(check.disagreements.len(), 1, "{check:?}"); - assert!(check.disagreements[0].contains("NUMA nodes"), "{check:?}"); + assert!( + matches!( + check.disagreements[0], + crate::topology::Disagreement::HighestNumaNode { .. } + ), + "{check:?}" + ); assert!(check.not_compared.is_empty(), "{check:?}"); } @@ -869,7 +875,13 @@ fn a_topology_reporting_no_numa_node_at_all_disagrees_with_a_raw_one() { "{check:?}" ); assert_eq!(check.disagreements.len(), 1, "{check:?}"); - assert!(check.disagreements[0].contains("none"), "{check:?}"); + assert!( + matches!( + check.disagreements[0], + crate::topology::Disagreement::HighestNumaNode { parsed: None, .. } + ), + "{check:?}" + ); } // A counter that could not be read must never read as agreement. Each of these @@ -895,7 +907,7 @@ fn a_failed_numa_read_is_incomplete_rather_than_agreement() { assert!(check.disagreements.is_empty(), "{check:?}"); assert_eq!(check.not_compared.len(), 1, "{check:?}"); assert!( - check.not_compared[0].contains("GetNumaHighestNodeNumber"), + check.not_compared[0] == crate::topology::NotCompared::HighestNumaNodeFailed, "{check:?}" ); } @@ -917,7 +929,7 @@ fn a_failed_processor_count_is_incomplete_rather_than_a_parse_disagreement() { ); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check.not_compared[0].contains("GetActiveProcessorCount"), + check.not_compared[0] == crate::topology::NotCompared::ActiveProcessorCountFailed, "{check:?}" ); } @@ -935,7 +947,7 @@ fn a_failed_group_count_is_incomplete_rather_than_a_parse_disagreement() { ); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check.not_compared[0].contains("GetActiveProcessorGroupCount"), + check.not_compared[0] == crate::topology::NotCompared::ActiveProcessorGroupCountFailed, "{check:?}" ); } @@ -1003,7 +1015,10 @@ fn disagreeing_source_enumerations_block_agreement_even_when_every_counter_match assert!(check.not_compared.is_empty(), "{check:?}"); assert_eq!(check.parse_incomplete.len(), 1, "{check:?}"); assert!( - check.parse_incomplete[0].contains("never agreed"), + matches!( + check.parse_incomplete[0], + crate::topology::ParseIncomplete::EnumerationsDisagreed { .. } + ), "{check:?}" ); assert_eq!( @@ -1374,9 +1389,11 @@ fn a_topology_with_no_processors_at_all_is_not_accused_of_hiding_packages() { let check = observation.cross_check(); assert!( - !check.parse_incomplete.iter().any( - |c| c.contains("no packages were reported") || c.contains("no cores were reported") - ), + !check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::NoPackages + | crate::topology::ParseIncomplete::NoCores + )), "with no processors reported, absent packages and cores are not a separate finding: \ {check:?}" ); @@ -1466,10 +1483,10 @@ fn observe_reports_a_numa_domain_whose_sources_number_it_differently() { // so the empty-survey entry fires alongside; asserting a total would couple // this test to causes it is not about. assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("more than one distinct node number")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::NumaDomainsWithConflictingLabels { .. } + )), "{check:?}" ); assert_eq!( @@ -1536,10 +1553,10 @@ fn a_machine_that_changed_still_reports_what_the_parse_itself_lost() { "the timing skew is still recorded: {check:?}" ); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("enumeration anomal")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::EnumerationAnomalies { .. } + )), "and the dropped record is NOT suppressed by it: {check:?}" ); assert!( @@ -1556,12 +1573,12 @@ fn a_core_or_attribute_the_sources_disagree_about_blocks_agreement() { // cores, or give the same processor different efficiency classes. for (label, mutate) in [ ( - "core(s) were reported only by CPU Sets", + "cores_only_in_cpu_sets", Box::new(|o: &mut crate::topology::Observation| o.cores_only_in_cpu_sets = 1) as Box, ), ( - "attribute(s) carry more than one distinct value", + "processor_attribute_conflicts", Box::new(|o: &mut crate::topology::Observation| o.processor_attribute_conflicts = 1), ), ] { @@ -1575,7 +1592,7 @@ fn a_core_or_attribute_the_sources_disagree_about_blocks_agreement() { counter: {check:?}" ); assert!( - check.parse_incomplete.iter().any(|c| c.contains(label)), + check.parse_incomplete.iter().any(|c| c.code() == label), "{label}: {check:?}" ); assert_eq!( @@ -1710,7 +1727,10 @@ fn observe_carries_the_crates_attribute_conflicts() { .cross_check() .parse_incomplete .iter() - .any(|c| c.contains("attribute(s) carry more than one distinct value")), + .any(|c| matches!( + c, + crate::topology::ParseIncomplete::ProcessorAttributeConflicts { .. } + )), ); } @@ -1773,7 +1793,7 @@ fn a_topology_nobody_measured_cannot_be_certified_against_this_machine() { .cross_check() .parse_incomplete .iter() - .any(|c| c.contains("not measured from a running machine")), + .any(|c| matches!(c, crate::topology::ParseIncomplete::NotMeasured)), "{:?}", synthetic.cross_check() ); @@ -1793,7 +1813,7 @@ fn a_topology_nobody_measured_cannot_be_certified_against_this_machine() { .cross_check() .parse_incomplete .iter() - .any(|c| c.contains("not measured from a running machine")), + .any(|c| matches!(c, crate::topology::ParseIncomplete::NotMeasured)), "{:?}", measured.cross_check() ); @@ -1856,12 +1876,12 @@ fn a_report_of_no_packages_or_no_cores_is_a_finding_not_a_machine() { // exactly as an empty cache survey is. for (label, mutate) in [ ( - "packages", + "no_packages", Box::new(|o: &mut crate::topology::Observation| o.packages = 0) as Box, ), ( - "cores", + "no_cores", Box::new(|o: &mut crate::topology::Observation| o.cores = Vec::new()), ), ] { @@ -1874,7 +1894,7 @@ fn a_report_of_no_packages_or_no_cores_is_a_finding_not_a_machine() { "{label}: an absent relationship is not the crate contradicting a counter: {check:?}" ); assert!( - check.parse_incomplete.iter().any(|c| c.contains(label)), + check.parse_incomplete.iter().any(|c| c.code() == label), "{label}: {check:?}" ); assert_eq!( @@ -1904,7 +1924,10 @@ fn a_numa_domain_no_source_reported_blocks_agreement_rather_than_accusing_the_pa assert!(check.disagreements.is_empty(), "{check:?}"); assert_eq!(check.parse_incomplete.len(), 1, "{check:?}"); assert!( - !check.parse_incomplete[0].contains("CPU Sets"), + !matches!( + check.parse_incomplete[0], + crate::topology::ParseIncomplete::NumaDomainsOnlyInCpuSets { .. } + ), "nobody reported it, so the message must not name a reporter: {check:?}" ); assert_eq!( @@ -2818,7 +2841,10 @@ fn an_empty_core_or_package_record_blocks_agreement() { let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{label}: {check:?}"); assert!( - check.parse_incomplete.iter().any(|c| c.contains(label)), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::RelationsWithoutProcessors { .. } + )), "{label}: {check:?}" ); assert_eq!( @@ -2949,7 +2975,10 @@ fn a_relation_a_caller_described_is_not_a_measurement() { .cross_check() .parse_incomplete .iter() - .any(|c| c.contains("described by a caller")), + .any(|c| matches!( + c, + crate::topology::ParseIncomplete::DescribedRelations { .. } + )), "a measured topology carrying a described relation is not all measured: {:?}", described.cross_check() ); @@ -2968,7 +2997,10 @@ fn a_relation_a_caller_described_is_not_a_measurement() { .cross_check() .parse_incomplete .iter() - .any(|c| c.contains("described by a caller")), + .any(|c| matches!( + c, + crate::topology::ParseIncomplete::DescribedRelations { .. } + )), "{:?}", walked.cross_check() ); @@ -3006,7 +3038,7 @@ fn a_bracket_left_open_is_not_the_same_as_a_machine_that_held_still() { check .not_compared .iter() - .any(|c| c.contains("bracket around the parse was not closed")), + .any(|c| matches!(c, crate::topology::NotCompared::BracketNotEstablished)), "{check:?}" ); assert!( @@ -3076,7 +3108,7 @@ fn observe_will_not_claim_a_bracket_it_was_not_given() { check .not_compared .iter() - .any(|c| c.contains("bracket around the parse was not closed")), + .any(|c| matches!(c, crate::topology::NotCompared::BracketNotEstablished)), "{check:?}" ); assert_ne!( @@ -3156,10 +3188,10 @@ fn a_relation_no_source_reported_is_counted_whatever_its_kind() { let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("carry no observation from any source")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::UnreportedRelations { .. } + )), "{check:?}" ); assert_eq!( @@ -3203,7 +3235,10 @@ fn a_measured_topology_reporting_no_processors_or_groups_blocks_agreement() { {check:?}" ); assert!( - check.parse_incomplete.iter().any(|c| c.contains(label)), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::MeasuredButCountsAbsent { .. } + )), "{label}: {check:?}" ); assert_eq!( @@ -3231,10 +3266,10 @@ fn both_absent_counts_are_named_together_rather_than_one_standing_for_the_pair() let check = observation.cross_check(); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("no online processors and processor groups")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::MeasuredButCountsAbsent { .. } + )), "{check:?}" ); } @@ -3254,17 +3289,17 @@ fn a_topology_nobody_measured_is_not_accused_of_describing_no_machine() { let check = observation.cross_check(); assert!( - !check - .parse_incomplete - .iter() - .any(|c| c.contains("cannot have none")), + !check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::MeasuredButCountsAbsent { .. } + )), "an unmeasured topology is not held to what a running machine must have: {check:?}" ); assert!( check .parse_incomplete .iter() - .any(|c| c.contains("was not measured from a running machine")), + .any(|c| matches!(c, crate::topology::ParseIncomplete::NotMeasured)), "and the reason it is exempt is itself reported: {check:?}" ); } @@ -3283,10 +3318,10 @@ fn two_walk_records_of_one_kind_claiming_a_processor_block_agreement() { let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("share a processor with another")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::OverlappingWalkRelations { .. } + )), "{check:?}" ); assert_eq!( @@ -3637,10 +3672,10 @@ fn a_count_holding_relations_no_platform_reported_cannot_contradict_a_counter() counter: {check:?}" ); assert!( - check - .not_compared - .iter() - .any(|c| c.contains("could not be attributed to the parse")), + check.not_compared.iter().any(|c| matches!( + c, + crate::topology::NotCompared::CountsIncludeUnparsedRelations + )), "{label}: and the reason no comparison was made is reported: {check:?}" ); assert_eq!( @@ -3661,7 +3696,10 @@ fn a_wholly_parsed_topology_is_still_compared_against_its_counters() { assert!(!observation.counts_include_unparsed_relations()); let check = observation.cross_check(); assert!( - check.disagreements.iter().any(|c| c.contains("groups:")), + check + .disagreements + .iter() + .any(|c| matches!(c, crate::topology::Disagreement::ProcessorGroups { .. })), "{check:?}" ); assert_eq!(check.verdict(), crate::topology::Verdict::Disagree); @@ -3707,10 +3745,10 @@ fn a_core_whose_smt_flag_contradicts_its_own_processor_count_blocks_agreement() let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{label}: {check:?}"); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("disagrees with the number of processors")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::ContradictoryCores { .. } + )), "{label}: {check:?}" ); assert_eq!( @@ -3763,10 +3801,10 @@ fn a_cache_level_numbered_zero_blocks_agreement() { let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("numbered 0")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::UnnumberedCacheLevels { .. } + )), "{check:?}" ); assert_eq!(check.verdict(), crate::topology::Verdict::Incomplete); @@ -4060,10 +4098,10 @@ fn observe_counts_walk_numa_nodes_that_claim_the_same_processor() { "the counter agreed, so nothing is filed against the parse: {check:?}" ); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("share a processor with another")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::OverlappingWalkRelations { .. } + )), "{check:?}" ); assert!(check.parse_in_doubt(), "{check:?}"); @@ -4093,7 +4131,12 @@ fn a_conflict_count_says_what_it_counted_rather_than_which_source_said_it() { mutate(&mut observation); let check = observation.cross_check(); - let entries = check.parse_incomplete.join(" "); + let entries = check + .parse_incomplete + .iter() + .map(ToString::to_string) + .collect::>() + .join(" "); assert!( entries.contains("more than one distinct"), "{label}: states what it counted: {check:?}" @@ -4215,10 +4258,10 @@ fn a_named_level_with_no_summary_blocks_agreement_and_sizes_to_one_domain() { let check = observation.cross_check(); assert!(check.disagreements.is_empty(), "{check:?}"); assert!( - check - .parse_incomplete - .iter() - .any(|c| c.contains("carries no summary for it")), + check.parse_incomplete.iter().any(|c| matches!( + c, + crate::topology::ParseIncomplete::PartitioningSummaryMissing { .. } + )), "{check:?}" ); assert_eq!( @@ -4448,3 +4491,145 @@ fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { `request_cost` depends on for its hard-coded long-path sample" ); } + +// --- M3.1: the row names each condition, rather than counting them ----------- + +/// The codes the row publishes for `key`, in order. +/// +/// Reads the rendered artifact rather than the `CrossCheck` behind it, because +/// what a survey receives is the point: an assertion against the struct would +/// hold even if the writer published nothing at all. +fn row_codes(text: &str, key: &str) -> Vec { + let row = text + .lines() + .find(|line| line.starts_with('{')) + .unwrap_or_else(|| panic!("no machine-readable row in:\n{text}")); + let needle = format!("\"{key}\":["); + let start = row + .find(&needle) + .unwrap_or_else(|| panic!("no `{key}` list in the row:\n{row}")) + + needle.len(); + let end = start + + row[start..] + .find(']') + .unwrap_or_else(|| panic!("unterminated `{key}` list in the row:\n{row}")); + + row[start..end] + .split(',') + .map(|piece| piece.trim().trim_matches('"').to_owned()) + .filter(|piece| !piece.is_empty()) + .collect() +} + +#[test] +fn the_row_names_the_probes_own_bug_when_it_detects_one() { + // **The defect this whole milestone came from, stated as a test.** The + // renderer prints `BUG IN THIS PROBE ...` for a named partitioning level + // carrying no summary. The verdict has been forced away from `agree` since + // that defect was fixed -- but the row published only + // `"parse_incomplete":1`, so a survey could tell the run was in doubt and + // NOT that the doubt was this probe contradicting itself, which is a + // categorically different fact from a flaky host. + let mut observation = clean_observation(); + observation.partitioning_cache_level = Some(9); + + let text = crate::topology_report::report(BANNER, &observation); + + assert!( + text.contains("BUG IN THIS PROBE"), + "the prose still alarms: {text}" + ); + assert!( + row_codes(&text, "parse_incomplete").contains(&"partitioning_summary_missing".to_owned()), + "the row must name the condition the prose alarms about, not merely \ + count it: {text}" + ); +} + +#[test] +fn the_row_lists_exactly_the_codes_of_the_conditions_the_check_found() { + // **The row against the vocabulary, on a report carrying several + // conditions at once.** A single-condition fixture cannot show that the + // codes travel in order, or that one is not dropped. + let mut observation = clean_observation(); + observation.caches = Vec::new(); + observation.cores_only_in_cpu_sets = 1; + observation.numa_domains_unreported = 2; + + let check = observation.cross_check(); + let expected: Vec = check + .parse_incomplete + .iter() + .map(|entry| entry.code().to_owned()) + .collect(); + + assert!( + expected.len() >= 3, + "the fixture must carry several conditions or it shows nothing: {check:?}" + ); + + let text = crate::topology_report::report(BANNER, &observation); + + assert_eq!( + row_codes(&text, "parse_incomplete"), + expected, + "every condition the check found reaches the row, in order: {text}" + ); +} + +#[test] +fn the_row_lists_one_code_per_line_the_prose_lists() { + // **The rule M3.1 establishes: a renderer may not tell a reader something + // the row cannot tell a survey.** Asserted as a count rather than a pairing + // on purpose -- the code and the sentence come from one `diagnostic` + // variant, so they cannot name different conditions, and what is left to + // check is that neither rendering drops an entry the other kept. + let mut observation = clean_observation(); + observation.caches = Vec::new(); + observation.numa_domains_unreported = 2; + + let text = crate::topology_report::report(BANNER, &observation); + let listed = text + .lines() + .filter(|line| line.starts_with(" - ") || line.starts_with(" (parse incomplete) ")) + .count(); + + assert_eq!( + row_codes(&text, "parse_incomplete").len(), + listed, + "the prose lists {listed} conditions, so the row must publish that \ + many: {text}" + ); +} + +#[test] +fn an_anomaly_reaches_the_row_as_its_kind() { + // Anomalies are published per-anomaly, so a survey can group by WHAT failed + // to decode. `AnomalyKind` is `#[non_exhaustive]`, so a kind this crate has + // no code for lands in `unclassified` -- visible in the row rather than + // silently mislabelled as a kind it is not. + let mut observation = clean_observation(); + observation.enumeration_anomalies = vec![ + windows_topology_sys::EnumerationAnomaly { + source: windows_topology_sys::Source::RelationshipWalk, + offset: 0, + kind: windows_topology_sys::AnomalyKind::TrailingBytes { remaining: 3 }, + }, + windows_topology_sys::EnumerationAnomaly { + source: windows_topology_sys::Source::CpuSets, + offset: 8, + kind: windows_topology_sys::AnomalyKind::Undersized { + declared: 8, + minimum: 48, + }, + }, + ]; + + let text = crate::topology_report::report(BANNER, &observation); + + assert_eq!( + row_codes(&text, "enumeration_anomalies"), + vec!["trailing_bytes".to_owned(), "undersized".to_owned()], + "each anomaly reaches the row as its own kind: {text}" + ); +} diff --git a/crates/windows-platform-probes/src/topology.rs b/crates/windows-platform-probes/src/topology.rs index 3a9353ca3..bc7085aa7 100644 --- a/crates/windows-platform-probes/src/topology.rs +++ b/crates/windows-platform-probes/src/topology.rs @@ -43,6 +43,10 @@ use windows_topology_sys::{ Source, }; +pub mod diagnostic; + +pub use diagnostic::{Disagreement, NotCompared, ParseIncomplete}; + /// One cache level, summarised across the machine. #[derive(Debug, Clone, PartialEq, Eq)] pub struct CacheLevel { @@ -536,36 +540,24 @@ impl Observation { let mut check = CrossCheck::default(); if !self.enumeration_anomalies.is_empty() { - check.parse_incomplete.push(format!( - // States the CONDITION, not a direction. "Records were dropped - // and the counts are short" was true of the two anomaly kinds - // that decode to nothing and false of `TruncatedArray`, which - // keeps the record with the entries that fit -- and a cache - // record kept with a partial affinity mask can INFLATE a - // partition count rather than shorten it, so the old message - // pointed a reader the wrong way. `AnomalyKind` is - // `#[non_exhaustive]`, so classifying here would need a - // catch-all arm that a future variant falls into silently; - // saying only what is true of all of them cannot rot that way. - "windows-topology-sys recorded {} enumeration anomal{}, so what Windows returned \ - was not fully decoded and the counts above may be short, or overstated where a \ - record was kept with an incomplete processor set", - self.enumeration_anomalies.len(), - if self.enumeration_anomalies.len() == 1 { - "y" - } else { - "ies" - }, - )); + // The wording's reasoning lives on the variant now, with the rest of + // this vocabulary. `AnomalyKind` is `#[non_exhaustive]`, so this + // entry deliberately says only what is true of every kind; the row + // publishes each anomaly's own code separately, where the catch-all + // is visible as `unclassified` rather than hidden in a sentence. + check + .parse_incomplete + .push(ParseIncomplete::EnumerationAnomalies { + count: self.enumeration_anomalies.len(), + }); } if self.numa_domains_only_in_cpu_sets > 0 { - check.parse_incomplete.push(format!( - "{} NUMA domain(s) were reported only by CPU Sets and never by the relationship \ - walk, so the two sources group nodes differently -- which no counter and no \ - coherence check reaches, since coherence compares processor sets", - self.numa_domains_only_in_cpu_sets, - )); + check + .parse_incomplete + .push(ParseIncomplete::NumaDomainsOnlyInCpuSets { + count: self.numa_domains_only_in_cpu_sets, + }); } // No cache levels at all. Distinct from the per-level case below, and @@ -576,11 +568,7 @@ impl Observation { // conclusion the report draws about cache structure would be drawn from // nothing. if self.caches.is_empty() { - check.parse_incomplete.push( - "no cache levels were reported at all, so what divides this machine by cache \ - was not established in either direction" - .to_string(), - ); + check.parse_incomplete.push(ParseIncomplete::NoCacheLevels); } // A level the survey DOES carry, with no partitions at all. @@ -599,10 +587,11 @@ impl Observation { .map(|c| c.level) .collect(); if !empty_levels.is_empty() { - check.parse_incomplete.push(format!( - "cache level(s) {empty_levels:?} decoded to no partitions at all, so what \ - divides this machine at those levels was not established" - )); + check + .parse_incomplete + .push(ParseIncomplete::CacheLevelsWithoutPartitions { + levels: empty_levels, + }); } // A running machine has processors and groups whatever the enumeration @@ -623,11 +612,9 @@ impl Observation { .map(|(name, _)| name) .collect(); if self.topology_was_measured && !absent.is_empty() { - check.parse_incomplete.push(format!( - "this topology was measured from a running machine, which cannot have none, but \ - it reported no {}", - absent.join(" and "), - )); + check + .parse_incomplete + .push(ParseIncomplete::MeasuredButCountsAbsent { absent }); } // The machine has packages and cores whatever the enumeration said, so @@ -637,14 +624,10 @@ impl Observation { // simply not report the relationship, in which case no anomaly fires // and nothing else here notices. if self.online_processors > 0 && self.packages == 0 { - check - .parse_incomplete - .push("no packages were reported at all, though the machine has one".to_string()); + check.parse_incomplete.push(ParseIncomplete::NoPackages); } if self.online_processors > 0 && self.cores.is_empty() { - check - .parse_incomplete - .push("no cores were reported at all, though the machine has one".to_string()); + check.parse_incomplete.push(ParseIncomplete::NoCores); } // A record that contradicts ITSELF, which no counter reaches: nothing @@ -660,20 +643,22 @@ impl Observation { .filter(|core| core.contradicts_itself()) .count(); if contradictory_cores > 0 { - check.parse_incomplete.push(format!( - "{contradictory_cores} core(s) report an SMT flag that disagrees with the number \ - of processors recorded beside it, so the record contradicts itself" - )); + check + .parse_incomplete + .push(ParseIncomplete::ContradictoryCores { + count: contradictory_cores, + }); } // Windows numbers cache levels from 1, so a level of 0 is a level the // parse did not read rather than one the machine has. let unnumbered_levels = self.caches.iter().filter(|c| c.level == 0).count(); if unnumbered_levels > 0 { - check.parse_incomplete.push(format!( - "{unnumbered_levels} cache level(s) are numbered 0, which is not a level Windows \ - reports, so what they describe was not established" - )); + check + .parse_incomplete + .push(ParseIncomplete::UnnumberedCacheLevels { + count: unnumbered_levels, + }); } // The renderer prints this state as "BUG IN THIS PROBE ... Nothing @@ -687,83 +672,77 @@ impl Observation { // `partitioning_cache` are both public, which is the same reason the // clamps in `domain_counts` exist. if let PartitioningCache::SummaryMissing(level) = self.partitioning_cache() { - check.parse_incomplete.push(format!( - "L{level} was named as the outermost partitioning cache and this survey carries \ - no summary for it, so what it divides was not established" - )); + check + .parse_incomplete + .push(ParseIncomplete::PartitioningSummaryMissing { level }); } if !self.topology_was_measured { - check.parse_incomplete.push( - "this topology was not measured from a running machine, so nothing here \ - describes the host it is reported on" - .to_string(), - ); + check.parse_incomplete.push(ParseIncomplete::NotMeasured); } if self.cores_without_processors > 0 || self.packages_without_processors > 0 { - check.parse_incomplete.push(format!( - "{} core(s) and {} package(s) cover no processors, so they raise those counts \ - and the policies derived from them without describing any part of the machine", - self.cores_without_processors, self.packages_without_processors, - )); + check + .parse_incomplete + .push(ParseIncomplete::RelationsWithoutProcessors { + cores: self.cores_without_processors, + packages: self.packages_without_processors, + }); } if self.unreported_relations > 0 { - check.parse_incomplete.push(format!( - "{} relation(s) carry no observation from any source, so they are counted here \ - without any platform API having described them", - self.unreported_relations, - )); + check + .parse_incomplete + .push(ParseIncomplete::UnreportedRelations { + count: self.unreported_relations, + }); } if self.described_relations > 0 { - check.parse_incomplete.push(format!( - "{} relation(s) were described by a caller rather than reported by any platform \ - API, so the counts above are not all of them measured", - self.described_relations, - )); + check + .parse_incomplete + .push(ParseIncomplete::DescribedRelations { + count: self.described_relations, + }); } if self.cores_only_in_cpu_sets > 0 { - check.parse_incomplete.push(format!( - "{} core(s) were reported only by CPU Sets and never by the relationship walk, so \ - the two group processors into cores differently and the core count above holds \ - both groupings", - self.cores_only_in_cpu_sets, - )); + check + .parse_incomplete + .push(ParseIncomplete::CoresOnlyInCpuSets { + count: self.cores_only_in_cpu_sets, + }); } if self.overlapping_walk_relations > 0 { - check.parse_incomplete.push(format!( - "{} relation(s) reported by the relationship walk share a processor with another \ - of the same kind, so one processor is claimed by two packages, two cores or two \ - NUMA nodes and the counts above hold both", - self.overlapping_walk_relations, - )); + check + .parse_incomplete + .push(ParseIncomplete::OverlappingWalkRelations { + count: self.overlapping_walk_relations, + }); } if self.processor_attribute_conflicts > 0 { - check.parse_incomplete.push(format!( - "{} per-processor attribute(s) carry more than one distinct value, so the \ - efficiency classes above are one claim rather than an agreed one", - self.processor_attribute_conflicts, - )); + check + .parse_incomplete + .push(ParseIncomplete::ProcessorAttributeConflicts { + count: self.processor_attribute_conflicts, + }); } if self.numa_domains_with_conflicting_labels > 0 { - check.parse_incomplete.push(format!( - "{} NUMA domain(s) carry more than one distinct node number, so what node they \ - are was not established and the highest below takes the larger", - self.numa_domains_with_conflicting_labels, - )); + check + .parse_incomplete + .push(ParseIncomplete::NumaDomainsWithConflictingLabels { + count: self.numa_domains_with_conflicting_labels, + }); } if self.numa_domains_unreported > 0 { - check.parse_incomplete.push(format!( - "{} NUMA domain(s) carry no observation from either source, so they raise the \ - domain count while contributing no node number to compare", - self.numa_domains_unreported, - )); + check + .parse_incomplete + .push(ParseIncomplete::NumaDomainsUnreported { + count: self.numa_domains_unreported, + }); } match &self.coherence { @@ -772,22 +751,20 @@ impl Observation { walk_only, cpu_sets_only, attempts, - } => check.parse_incomplete.push(format!( - "windows-topology-sys reports its two enumerations never agreed within {attempts} \ - attempt(s): {} processor(s) seen only by the relationship walk, {} seen only by \ - CPU Sets and so absent from the parsed list entirely", - walk_only.len(), - cpu_sets_only.len(), - )), + } => check + .parse_incomplete + .push(ParseIncomplete::EnumerationsDisagreed { + attempts: *attempts, + walk_only: walk_only.len(), + cpu_sets_only: cpu_sets_only.len(), + }), // Unreachable from `discover`, which returns `Agreed` or // `Disagreed`. Reported rather than ignored because reaching it // would mean the parse came from somewhere that read nothing // twice, and a cross-check cannot certify that either. - Coherence::NotCollected => check.parse_incomplete.push( - "windows-topology-sys reports its coherence was never collected, so nothing \ - established that its two enumerations describe the same machine" - .to_string(), - ), + Coherence::NotCollected => check + .parse_incomplete + .push(ParseIncomplete::CoherenceNotCollected), } // Everything above is about the PARSE and is evaluated unconditionally. @@ -806,19 +783,11 @@ impl Observation { match self.bracket { BracketOutcome::HeldStill => {} BracketOutcome::Changed => { - check.not_compared.push( - "the machine changed while this ran -- the counters moved across the parse, \ - so the two readings describe different instants" - .to_string(), - ); + check.not_compared.push(NotCompared::MachineChanged); return check; } BracketOutcome::NotEstablished => { - check.not_compared.push( - "the bracket around the parse was not closed -- a counter failed one of its \ - two reads, so nothing established that the machine held still" - .to_string(), - ); + check.not_compared.push(NotCompared::BracketNotEstablished); return check; } } @@ -835,11 +804,9 @@ impl Observation { // Filed as `not_compared` rather than skipped silently: this is a // reading that could not be trusted, which is exactly what that list is. if self.counts_include_unparsed_relations() { - check.not_compared.push( - "these counts include relations no platform API reported, so a counter mismatch \ - could not be attributed to the parse" - .to_string(), - ); + check + .not_compared + .push(NotCompared::CountsIncludeUnparsedRelations); return check; } @@ -848,31 +815,29 @@ impl Observation { // count to compare against. Treating it as a count reported a failed // measurement as though the crate's parse were wrong. if self.raw_active_processors == 0 { - check.not_compared.push( - "GetActiveProcessorCount returned 0, which is its failure report".to_string(), - ); + check + .not_compared + .push(NotCompared::ActiveProcessorCountFailed); } else if self.online_processors != self.raw_active_processors as usize { - check.disagreements.push(format!( - "online processors: topology crate says {}, GetActiveProcessorCount says {}", - self.online_processors, self.raw_active_processors - )); + check.disagreements.push(Disagreement::OnlineProcessors { + parsed: self.online_processors, + counter: self.raw_active_processors, + }); } if self.raw_group_count == 0 { - check.not_compared.push( - "GetActiveProcessorGroupCount returned 0, which is its failure report".to_string(), - ); + check + .not_compared + .push(NotCompared::ActiveProcessorGroupCountFailed); } else if self.groups != self.raw_group_count as usize { - check.disagreements.push(format!( - "groups: topology crate says {}, GetActiveProcessorGroupCount says {}", - self.groups, self.raw_group_count - )); + check.disagreements.push(Disagreement::ProcessorGroups { + parsed: self.groups, + counter: self.raw_group_count, + }); } let Some(highest) = self.raw_highest_numa_node else { - check.not_compared.push( - "GetNumaHighestNodeNumber failed, so no NUMA comparison was made".to_string(), - ); + check.not_compared.push(NotCompared::HighestNumaNodeFailed); return check; }; @@ -883,12 +848,10 @@ impl Observation { // nodes 0 and 2 are a valid sparse topology, and the count form // would report a regression on hardware that is reporting itself // correctly. - check.disagreements.push(format!( - "NUMA nodes: topology crate's highest node is {}, GetNumaHighestNodeNumber says {}", - self.highest_numa_node - .map_or_else(|| "none".to_string(), |n| n.to_string()), - highest - )); + check.disagreements.push(Disagreement::HighestNumaNode { + parsed: self.highest_numa_node, + counter: highest, + }); } check } @@ -902,7 +865,7 @@ impl Observation { pub struct CrossCheck { /// Counters that were compared and did not match. Each is a finding about /// the shipping crate's parse. - pub disagreements: Vec, + pub disagreements: Vec, /// Comparisons this run could not make, or could not trust. Each is a gap /// in this measurement, not a finding about the parse. /// @@ -913,7 +876,7 @@ pub struct CrossCheck { /// relations no platform API reported. Both are comparisons that were not /// made; neither is a counter that failed. The causes are enumerated in /// exactly one place, [`Observation::cross_check`]'s body. - pub not_compared: Vec, + pub not_compared: Vec, /// Ways the parse is short, or its claims mutually inconsistent, such that /// agreeing counters cannot certify it. /// @@ -926,7 +889,7 @@ pub struct CrossCheck { /// while every entry happened to be a crate self-assessment. One is not: /// a NUMA domain only CPU Sets described is derived here, from provenance /// the crate carries but draws no conclusion about. - pub parse_incomplete: Vec, + pub parse_incomplete: Vec, } impl CrossCheck { diff --git a/crates/windows-platform-probes/src/topology/diagnostic.rs b/crates/windows-platform-probes/src/topology/diagnostic.rs new file mode 100644 index 000000000..5990429e7 --- /dev/null +++ b/crates/windows-platform-probes/src/topology/diagnostic.rs @@ -0,0 +1,474 @@ +// Copyright (c) Mike Grier. + +//! The vocabulary of what a cross-check found, as values rather than sentences. +//! +//! # Why these are types and not `String`s +//! +//! Each of [`CrossCheck`](super::CrossCheck)'s three lists used to hold the +//! human sentence and nothing else, and the NDJSON row published each list's +//! **length**. So the fact a mining pass most needs -- *which* condition +//! occurred -- existed only in the prose: a survey reading +//! `"parse_incomplete":1` could not tell *the probe detected a bug in itself* +//! from *a core record contradicted itself* from *this topology was not +//! measured from a running machine*. +//! +//! That is the gap recorded in +//! [DESIGN-NOTES.md](../../DESIGN-NOTES.md#d-encoded-row-is-the-contract): the +//! row was impoverished relative to the prose, which is backwards given that the +//! row is the artifact a fleet survey mines and the designs rest on. +//! +//! A variant carries its own data, renders its own sentence through +//! [`fmt::Display`], and names itself through `code`. One definition, so the +//! sentence and the discriminant cannot drift apart -- where a parallel +//! `(code, String)` pair could be updated on one side only. +//! +//! # The code is the contract; the sentence is not +//! +//! `code` is a **stable machine discriminant** and changing one is a breaking +//! change to the row, exactly as renaming a field would be. The `Display` text +//! is free to be reworded at any time: it reaches only the prose, where a reader +//! is the consumer and rewording is harmless. +//! +//! This is what lets the prose stay written for a human. Before, a test or a +//! survey wanting to know which condition fired had to match on English, so +//! improving a sentence risked breaking a consumer -- which is a reason not to +//! improve it. + +use std::fmt; + +use windows_topology_sys::{AnomalyKind, EnumerationAnomaly}; + +/// A counter comparison that was made and did not match. +/// +/// Each is a finding about the shipping crate's parse, and is the only list +/// whose entries produce [`Verdict::Disagree`](super::Verdict::Disagree). +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Disagreement { + /// The parsed processor count and `GetActiveProcessorCount` differ. + OnlineProcessors { + /// What `windows-topology-sys` parsed. + parsed: usize, + /// What the Win32 counter reported. + counter: u32, + }, + /// The parsed group count and `GetActiveProcessorGroupCount` differ. + ProcessorGroups { + /// What `windows-topology-sys` parsed. + parsed: usize, + /// What the Win32 counter reported. + counter: u16, + }, + /// The parsed highest NUMA node and `GetNumaHighestNodeNumber` differ. + /// + /// Highest against highest, never a count against `highest + 1`: Windows + /// does not promise the largest node *number* equals the node count, and + /// nodes 0 and 2 are a valid sparse topology. + HighestNumaNode { + /// The largest node number the parse carries, if it carries any. + parsed: Option, + /// What the Win32 counter reported. + counter: u32, + }, +} + +impl Disagreement { + /// The stable discriminant a survey groups by. + /// + /// Changing one of these is a breaking change to the NDJSON row. + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + Self::OnlineProcessors { .. } => "online_processors", + Self::ProcessorGroups { .. } => "processor_groups", + Self::HighestNumaNode { .. } => "highest_numa_node", + } + } +} + +impl fmt::Display for Disagreement { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::OnlineProcessors { parsed, counter } => write!( + f, + "online processors: topology crate says {parsed}, GetActiveProcessorCount says \ + {counter}" + ), + Self::ProcessorGroups { parsed, counter } => write!( + f, + "groups: topology crate says {parsed}, GetActiveProcessorGroupCount says {counter}" + ), + Self::HighestNumaNode { parsed, counter } => write!( + f, + "NUMA nodes: topology crate's highest node is {}, GetNumaHighestNodeNumber says \ + {counter}", + parsed.map_or_else(|| "none".to_string(), |n| n.to_string()), + ), + } + } +} + +/// A comparison this run could not make, or could not trust. +/// +/// Each is a gap in this measurement, not a finding about the parse -- which is +/// why [`parse_in_doubt`](super::CrossCheck::parse_in_doubt) excludes this list. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NotCompared { + /// The counters moved across the parse, so the two readings describe + /// different instants. + MachineChanged, + /// A counter failed one of its two reads, so nothing established that the + /// machine held still. + BracketNotEstablished, + /// The counts include relations no platform API reported, so a mismatch + /// could not be attributed to the parse. + CountsIncludeUnparsedRelations, + /// `GetActiveProcessorCount` reported failure. + ActiveProcessorCountFailed, + /// `GetActiveProcessorGroupCount` reported failure. + ActiveProcessorGroupCountFailed, + /// `GetNumaHighestNodeNumber` reported failure. + HighestNumaNodeFailed, +} + +impl NotCompared { + /// The stable discriminant a survey groups by. + /// + /// Changing one of these is a breaking change to the NDJSON row. + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + Self::MachineChanged => "machine_changed", + Self::BracketNotEstablished => "bracket_not_established", + Self::CountsIncludeUnparsedRelations => "counts_include_unparsed_relations", + Self::ActiveProcessorCountFailed => "active_processor_count_failed", + Self::ActiveProcessorGroupCountFailed => "active_processor_group_count_failed", + Self::HighestNumaNodeFailed => "highest_numa_node_failed", + } + } +} + +impl fmt::Display for NotCompared { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let text = match self { + Self::MachineChanged => { + "the machine changed while this ran -- the counters moved across the parse, so \ + the two readings describe different instants" + } + Self::BracketNotEstablished => { + "the bracket around the parse was not closed -- a counter failed one of its two \ + reads, so nothing established that the machine held still" + } + Self::CountsIncludeUnparsedRelations => { + "these counts include relations no platform API reported, so a counter mismatch \ + could not be attributed to the parse" + } + Self::ActiveProcessorCountFailed => { + "GetActiveProcessorCount returned 0, which is its failure report" + } + Self::ActiveProcessorGroupCountFailed => { + "GetActiveProcessorGroupCount returned 0, which is its failure report" + } + Self::HighestNumaNodeFailed => { + "GetNumaHighestNodeNumber failed, so no NUMA comparison was made" + } + }; + + f.write_str(text) + } +} + +/// A way the parse is short, or its claims mutually inconsistent, such that +/// agreeing counters cannot certify it. +/// +/// Established from the PARSE rather than from any counter, which is why no +/// counter agreeing can retire an entry here. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ParseIncomplete { + /// `windows-topology-sys` recorded records it could not decode. + /// + /// States the CONDITION, not a direction. "Records were dropped and the + /// counts are short" was true of the two anomaly kinds that decode to + /// nothing and false of `TruncatedArray`, which keeps the record with the + /// entries that fit -- and a cache record kept with a partial affinity mask + /// can INFLATE a partition count rather than shorten it, so the old message + /// pointed a reader the wrong way. + EnumerationAnomalies { + /// How many anomalies were recorded. + count: usize, + }, + /// NUMA domains only CPU Sets reported, so the two sources group nodes + /// differently. + NumaDomainsOnlyInCpuSets { + /// How many such domains. + count: usize, + }, + /// The survey reported no cache relationships whatsoever. + NoCacheLevels, + /// Levels the survey carries that decoded to no partitions at all. + CacheLevelsWithoutPartitions { + /// Which levels. + levels: Vec, + }, + /// A measured topology reported none of a count a running machine must have. + MeasuredButCountsAbsent { + /// Which counts were zero, in the order the report names them. + absent: Vec<&'static str>, + }, + /// No packages were reported, though the machine has one. + NoPackages, + /// No cores were reported, though the machine has one. + NoCores, + /// Cores whose SMT flag disagrees with the processors recorded beside it. + ContradictoryCores { + /// How many such cores. + count: usize, + }, + /// Cache levels numbered 0, which is not a level Windows reports. + UnnumberedCacheLevels { + /// How many such levels. + count: usize, + }, + /// A level was named as the outermost partitioning cache with no summary + /// for it. + /// + /// This is the probe detecting a bug in itself, and is the condition the + /// renderer prints as `BUG IN THIS PROBE`. + PartitioningSummaryMissing { + /// The level that was named. + level: u8, + }, + /// The topology was not measured from a running machine. + NotMeasured, + /// Cores and packages that cover no processors. + RelationsWithoutProcessors { + /// How many cores. + cores: usize, + /// How many packages. + packages: usize, + }, + /// Relations carrying no observation from any source. + UnreportedRelations { + /// How many relations. + count: usize, + }, + /// Relations a caller described rather than any platform API reporting them. + DescribedRelations { + /// How many relations. + count: usize, + }, + /// Cores only CPU Sets reported, so the two sources group processors into + /// cores differently. + CoresOnlyInCpuSets { + /// How many such cores. + count: usize, + }, + /// Walk relations sharing a processor with another of the same kind. + OverlappingWalkRelations { + /// How many relations. + count: usize, + }, + /// Per-processor attributes carrying more than one distinct value. + ProcessorAttributeConflicts { + /// How many attributes. + count: usize, + }, + /// NUMA domains carrying more than one distinct node number. + NumaDomainsWithConflictingLabels { + /// How many domains. + count: usize, + }, + /// NUMA domains carrying no observation from either source. + NumaDomainsUnreported { + /// How many domains. + count: usize, + }, + /// The crate's two enumerations never agreed. + EnumerationsDisagreed { + /// How many attempts were made. + /// + /// `u32` because that is what `Coherence::Disagreed` carries; taking the + /// upstream type rather than casting keeps this a copy of the value and + /// not a conversion of it. + attempts: u32, + /// Processors seen only by the relationship walk. + walk_only: usize, + /// Processors seen only by CPU Sets. + cpu_sets_only: usize, + }, + /// The crate reports its coherence was never collected. + CoherenceNotCollected, +} + +impl ParseIncomplete { + /// The stable discriminant a survey groups by. + /// + /// Changing one of these is a breaking change to the NDJSON row. + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + Self::EnumerationAnomalies { .. } => "enumeration_anomalies", + Self::NumaDomainsOnlyInCpuSets { .. } => "numa_domains_only_in_cpu_sets", + Self::NoCacheLevels => "no_cache_levels", + Self::CacheLevelsWithoutPartitions { .. } => "cache_levels_without_partitions", + Self::MeasuredButCountsAbsent { .. } => "measured_but_counts_absent", + Self::NoPackages => "no_packages", + Self::NoCores => "no_cores", + Self::ContradictoryCores { .. } => "contradictory_cores", + Self::UnnumberedCacheLevels { .. } => "unnumbered_cache_levels", + Self::PartitioningSummaryMissing { .. } => "partitioning_summary_missing", + Self::NotMeasured => "not_measured", + Self::RelationsWithoutProcessors { .. } => "relations_without_processors", + Self::UnreportedRelations { .. } => "unreported_relations", + Self::DescribedRelations { .. } => "described_relations", + Self::CoresOnlyInCpuSets { .. } => "cores_only_in_cpu_sets", + Self::OverlappingWalkRelations { .. } => "overlapping_walk_relations", + Self::ProcessorAttributeConflicts { .. } => "processor_attribute_conflicts", + Self::NumaDomainsWithConflictingLabels { .. } => "numa_domains_with_conflicting_labels", + Self::NumaDomainsUnreported { .. } => "numa_domains_unreported", + Self::EnumerationsDisagreed { .. } => "enumerations_disagreed", + Self::CoherenceNotCollected => "coherence_not_collected", + } + } +} + +impl fmt::Display for ParseIncomplete { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EnumerationAnomalies { count } => write!( + f, + "windows-topology-sys recorded {count} enumeration anomal{}, so what Windows \ + returned was not fully decoded and the counts above may be short, or overstated \ + where a record was kept with an incomplete processor set", + if *count == 1 { "y" } else { "ies" }, + ), + Self::NumaDomainsOnlyInCpuSets { count } => write!( + f, + "{count} NUMA domain(s) were reported only by CPU Sets and never by the \ + relationship walk, so the two sources group nodes differently -- which no \ + counter and no coherence check reaches, since coherence compares processor sets" + ), + Self::NoCacheLevels => f.write_str( + "no cache levels were reported at all, so what divides this machine by cache was \ + not established in either direction", + ), + Self::CacheLevelsWithoutPartitions { levels } => write!( + f, + "cache level(s) {levels:?} decoded to no partitions at all, so what divides this \ + machine at those levels was not established" + ), + Self::MeasuredButCountsAbsent { absent } => write!( + f, + "this topology was measured from a running machine, which cannot have none, but \ + it reported no {}", + absent.join(" and "), + ), + Self::NoPackages => { + f.write_str("no packages were reported at all, though the machine has one") + } + Self::NoCores => { + f.write_str("no cores were reported at all, though the machine has one") + } + Self::ContradictoryCores { count } => write!( + f, + "{count} core(s) report an SMT flag that disagrees with the number of processors \ + recorded beside it, so the record contradicts itself" + ), + Self::UnnumberedCacheLevels { count } => write!( + f, + "{count} cache level(s) are numbered 0, which is not a level Windows reports, so \ + what they describe was not established" + ), + Self::PartitioningSummaryMissing { level } => write!( + f, + "L{level} was named as the outermost partitioning cache and this survey carries \ + no summary for it, so what it divides was not established" + ), + Self::NotMeasured => f.write_str( + "this topology was not measured from a running machine, so nothing here describes \ + the host it is reported on", + ), + Self::RelationsWithoutProcessors { cores, packages } => write!( + f, + "{cores} core(s) and {packages} package(s) cover no processors, so they raise \ + those counts and the policies derived from them without describing any part of \ + the machine" + ), + Self::UnreportedRelations { count } => write!( + f, + "{count} relation(s) carry no observation from any source, so they are counted \ + here without any platform API having described them" + ), + Self::DescribedRelations { count } => write!( + f, + "{count} relation(s) were described by a caller rather than reported by any \ + platform API, so the counts above are not all of them measured" + ), + Self::CoresOnlyInCpuSets { count } => write!( + f, + "{count} core(s) were reported only by CPU Sets and never by the relationship \ + walk, so the two group processors into cores differently and the core count \ + above holds both groupings" + ), + Self::OverlappingWalkRelations { count } => write!( + f, + "{count} relation(s) reported by the relationship walk share a processor with \ + another of the same kind, so one processor is claimed by two packages, two cores \ + or two NUMA nodes and the counts above hold both" + ), + Self::ProcessorAttributeConflicts { count } => write!( + f, + "{count} per-processor attribute(s) carry more than one distinct value, so the \ + efficiency classes above are one claim rather than an agreed one" + ), + Self::NumaDomainsWithConflictingLabels { count } => write!( + f, + "{count} NUMA domain(s) carry more than one distinct node number, so what node \ + they are was not established and the highest below takes the larger" + ), + Self::NumaDomainsUnreported { count } => write!( + f, + "{count} NUMA domain(s) carry no observation from either source, so they raise \ + the domain count while contributing no node number to compare" + ), + Self::EnumerationsDisagreed { + attempts, + walk_only, + cpu_sets_only, + } => write!( + f, + "windows-topology-sys reports its two enumerations never agreed within \ + {attempts} attempt(s): {walk_only} processor(s) seen only by the relationship \ + walk, {cpu_sets_only} seen only by CPU Sets and so absent from the parsed list \ + entirely" + ), + Self::CoherenceNotCollected => f.write_str( + "windows-topology-sys reports its coherence was never collected, so nothing \ + established that its two enumerations describe the same machine", + ), + } + } +} + +/// The stable discriminant for an anomaly the enumeration recorded. +/// +/// **`AnomalyKind` is `#[non_exhaustive]`, so this match needs a catch-all and +/// a variant added upstream lands in it.** That is stated rather than hidden: +/// `unclassified` is a real answer meaning "this probe's vocabulary is older +/// than the crate's", which is more useful to a survey than a code invented +/// here that pretends to name the new kind. The row publishes it, so a sweep +/// over accumulated output finds the day the vocabularies parted rather than +/// silently mislabelling the anomaly. +/// +/// It is deliberately NOT a compile error. Owning the row's vocabulary means +/// this crate decides when a new upstream kind earns a code, and a build that +/// breaks on a dependency bump would force that decision at the worst moment. +#[must_use] +pub fn anomaly_code(anomaly: &EnumerationAnomaly) -> &'static str { + match anomaly.kind { + AnomalyKind::Undersized { .. } => "undersized", + AnomalyKind::OverrunsBuffer { .. } => "overruns_buffer", + AnomalyKind::TrailingBytes { .. } => "trailing_bytes", + AnomalyKind::TruncatedArray { .. } => "truncated_array", + _ => "unclassified", + } +} diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index 869664bba..43fb9ee50 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -19,6 +19,7 @@ use std::io; use windows_placement_probe::fingerprint::{Fingerprint, banner_line_for}; +use crate::topology::diagnostic::anomaly_code; use crate::topology::{Observation, PartitioningCache, Verdict}; /// The banner and title both reports open with. @@ -665,6 +666,38 @@ pub fn report(banner: &str, observation: &Observation) -> String { .into_iter() .map(|(name, count)| format!(r#""{name}":{count}"#)) .collect(); + // **The conditions, not how many there were.** These three published + // `.len()`, so the row said a run was in doubt without saying why: a survey + // reading `"parse_incomplete":1` could not tell `partitioning_summary_missing` + // -- this probe detecting a bug in ITSELF -- from `contradictory_cores` or + // `not_measured`. Four categorically different facts, one cardinality, and + // only the prose separated them. + // + // Exactly the correction `efficiency_classes` already carries a few lines + // below, for exactly the reason given there: the list is what the name + // promises, and a count is still available from its length. The + // `windows-topology-sys recorded N enumeration anomal...` sentence the prose + // prints keeps that count; it does not need the row to restate it. + // + // The codes are the contract and the sentences are not -- see + // `topology::diagnostic`. That is what lets the prose be reworded for a + // reader without breaking a mining pass. + let quoted = |codes: Vec<&'static str>| { + codes + .into_iter() + .map(|code| format!(r#""{code}""#)) + .collect::>() + .join(",") + }; + let not_compared_json = quoted(check.not_compared.iter().map(|e| e.code()).collect()); + let parse_incomplete_json = quoted(check.parse_incomplete.iter().map(|e| e.code()).collect()); + let anomalies_json = quoted( + observation + .enumeration_anomalies + .iter() + .map(anomaly_code) + .collect(), + ); let _ = writeln!( out, concat!( @@ -672,8 +705,8 @@ pub fn report(banner: &str, observation: &Observation) -> String { r#""packages":{},"numa_domains":{},"numa_domains_without_processors":{},"cores":{},"#, r#""efficiency_classes":[{}],"caches":[{}],"outermost_partitioning_cache_level":{},"#, r#""outermost_partitioning_cache":"{}","#, - r#""policies":{{{}}},"cross_check":"{}","not_compared":{},"parse_incomplete":{},"#, - r#""enumeration_anomalies":{},"numa_domains_only_in_cpu_sets":{}}}"# + r#""policies":{{{}}},"cross_check":"{}","not_compared":[{}],"parse_incomplete":[{}],"#, + r#""enumeration_anomalies":[{}],"numa_domains_only_in_cpu_sets":{}}}"# ), std::env::consts::ARCH, observation.online_processors, @@ -737,16 +770,16 @@ pub fn report(banner: &str, observation: &Observation) -> String { Verdict::Disagree => "disagree", Verdict::Incomplete => "incomplete", }, - check.not_compared.len(), + not_compared_json, // Separate from `not_compared`, because a mining pass that finds // `"cross_check":"incomplete"` needs to know whether this probe failed // to read a counter or the parse itself was short or disputed -- the // first is a gap in the measurement, the second a fact about the - // machine worth going and looking at. The two counts beside it say - // which kind, without a consumer having to know what `cross_check` - // currently pushes for. - check.parse_incomplete.len(), - observation.enumeration_anomalies.len(), + // machine worth going and looking at. The two lists beside it say which + // kind, without a consumer having to know what `cross_check` currently + // pushes for -- and now say which CONDITION, not merely how many. + parse_incomplete_json, + anomalies_json, observation.numa_domains_only_in_cpu_sets, ); diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index e06022e5c..f4c5472aa 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -518,19 +518,38 @@ const FACTS: &[Fact] = &[ // a non-zero count beside an agreeing verdict is a violation in its own // right. `Silence::AtValue("0")` would have excused both mutations and // discarded that second guarantee. + // **These three publish their CONDITIONS now, not a count of them**, so the + // value at which the prose says nothing is the empty list rather than `0`, + // and the mutation that changes them is a different code rather than a + // different digit. The guarantees are unchanged: an empty list still renders + // no prose entry, so deleting the key contradicts nothing, while making it + // non-empty is still caught by the rule that an agreeing verdict cannot sit + // beside published doubt. + // + // `corruptions` mutates them by APPENDING a code, so the list's length + // differs by one whatever the host rendered -- and the length is what the + // prose comparison reads. + // + // **Which conditions are listed is deliberately not compared against the + // prose, and needs no rule.** The code and the sentence come from one + // `topology::diagnostic` variant, so there is no second implementation for + // them to disagree through: the correspondence is guaranteed by + // construction rather than checked after the fact, which is the stronger + // form. What remains checkable -- and checked -- is that the two renderings + // list the same NUMBER of conditions. Fact { - absent_is_silent_at: Some("0"), - why: "at zero the renderer emits no `(not compared)` entry, so the \ - absence of the field contradicts nothing", + absent_is_silent_at: Some("[]"), + why: "with no entries the renderer emits no `(not compared)` line, so \ + the absence of the field contradicts nothing", ..fact( "not_compared", &["not compared count", "incomplete-verdict listing count"], ) }, Fact { - absent_is_silent_at: Some("0"), - why: "at zero the renderer emits no `(parse incomplete)` entry, so the \ - absence of the field contradicts nothing", + absent_is_silent_at: Some("[]"), + why: "with no entries the renderer emits no `(parse incomplete)` line, \ + so the absence of the field contradicts nothing", ..fact( "parse_incomplete", &["parse incomplete count", "incomplete-verdict listing count"], @@ -548,8 +567,8 @@ const FACTS: &[Fact] = &[ // The narrower declaration is the same one `not_compared` and // `parse_incomplete` needed, and this key should have been swept with them. Fact { - silence: Silence::AtValueUnlessAgreeing("0"), - absent_is_silent_at: Some("0"), + silence: Silence::AtValueUnlessAgreeing("[]"), + absent_is_silent_at: Some("[]"), why: "the count reaches the prose only inside the `windows-topology-sys \ recorded N enumeration anomal...` sentence, which the renderer \ emits only when there are anomalies. At zero that sentence is \ @@ -847,9 +866,33 @@ fn corruptions(report: &str, key: &str, empty_replacement: Option<&str>) -> Vec< if sites.is_empty() { // An empty container still has prose beside it, so substitute the // smallest value that disagrees with an empty one. - let mut mutations = empty_replacement - .map(|replacement| vec![rewrite(replacement)]) - .unwrap_or_default(); + // + // **A digit-free list is not necessarily an empty one.** The + // diagnostic fields publish arrays of condition CODES, which carry + // no digits at whatever length -- so this arm now serves two shapes, + // and a fixed replacement cannot serve the second: a host already + // listing one condition, rewritten to a one-element replacement, + // changes WHICH code is listed and not HOW MANY, and the length is + // what the prose comparison reads. That mutation went unnoticed on + // the `verdict incomplete` corpus shape, which is how this was + // found. + // + // Appending instead makes the length differ by one whatever the + // original was. Only where no replacement is declared: `caches` and + // `efficiency_classes` need their specific shapes, and an appended + // string would not carry the members their rules read. + let mut mutations = match empty_replacement { + Some(replacement) => vec![rewrite(replacement)], + None => { + let inner = original.trim_start_matches('[').trim_end_matches(']'); + let extended = if inner.is_empty() { + r#"["x-corrupted"]"#.to_owned() + } else { + format!(r#"[{inner},"x-corrupted"]"#) + }; + vec![rewrite(&extended)] + } + }; mutations.push(deleted); return mutations; } From 5765fff8e5bdcce5528b03cf4d9dd4d00fe1cce8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 21:14:52 -0400 Subject: [PATCH 05/46] fix(platform-probes): assert the payload a substring used to discriminate A review of the M3.1 change set found two tests that had stopped testing what they are named for, and three stale statements. **The substring-to-variant conversion dropped what the substring discriminated.** `both_absent_counts_are_named_together_rather_than_one_standing_for_the_pair` became `matches!(c, MeasuredButCountsAbsent { .. })`, which holds when `absent` names only ONE of the two counts -- exactly what the test forbids. Measured: with `absent` truncated to its first entry, the whole suite stayed green at 249 passed, so the test could not fail for its own property and was fully subsumed by the loop above it. The same conversion took the discrimination out of `a_measured_topology_reporting_no_processors_or_groups_blocks_agreement`: its loop labels had BEEN the assertion (`c.contains(label)`) and became captions for the failure message, so neither iteration checked that the correct count was named. Both now assert the variant's payload, and both were observed to fail: truncating `absent` reddens the both-absent test, and swapping the two names in `cross_check` reddens both. The general lesson, recorded in the checklist: converting an assertion from a substring to a variant DROPS whatever the substring discriminated inside the payload. The variant is the weaker claim unless the payload comes with it. **Swept the class rather than the reported instance**, which found one more -- `an_empty_core_or_package_record_blocks_agreement`, where both arms raise `RelationsWithoutProcessors` and the variant match cannot separate an empty CORE record from an empty PACKAGE one. That gap PREDATES this branch: `Display` renders `{cores} core(s) and {packages} package(s) cover no processors` as one sentence, so the old `"core(s) and"` and `"package(s) cover no processors"` substrings both matched whichever field was set. Fixed here because it is the same class as the two above, and verified the same way: swapping `cores` and `packages` at the construction site reddens it. Reviewed the other thirty-odd conversions against this: the rest map to distinct variants whose identity is the whole claim, and the one that did carry a payload distinction -- `contains("none")` for a missing NUMA node -- kept it as `HighestNumaNode { parsed: None, .. }`. **Three stale statements**, each made false by the commit before this one and each verified against the code: - Both `PLANS.md` trackers still said the row publishes its diagnostic lists as bare counts, in the present tense. They were written before M3.1 landed and were not swept with DESIGN-NOTES.md and CHECKLIST.md, so they contradicted the code and three sibling documents. - `report_oracle.rs` carried the deleted `ndjson_count`'s summary line orphaned above `ndjson_list_len`'s own, so rustdoc rendered a two-sentence summary whose first half described a function that no longer exists. - DESIGN-NOTES.md and CHECKLIST.md M3.3 both said the row interpolates "eighteen values" positionally. Counted: the template has seventeen `{}` placeholders and takes seventeen arguments -- `reason` is a literal. Since the sentence's point is the hazard of a miscounted placeholder, the placeholder count is the one it should quote. The session record's "eighteen keys" is correct and unchanged. Verified: 249 lib tests plus 14 ignored, 9 integration tests and doctests. clippy --all-targets --all-features clean, fmt clean, cargo doc clean under all three rustdoc deny lints, encoding clean over 636 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- PLANS.md | 2 +- crates/windows-platform-probes/CHECKLIST.md | 17 ++++- .../windows-platform-probes/DESIGN-NOTES.md | 9 ++- crates/windows-platform-probes/PLANS.md | 2 +- .../src/report_oracle.rs | 1 - crates/windows-platform-probes/src/tests.rs | 63 ++++++++++++++----- 6 files changed, 70 insertions(+), 24 deletions(-) diff --git a/PLANS.md b/PLANS.md index eb5f2c19e..dcd7e609b 100644 --- a/PLANS.md +++ b/PLANS.md @@ -20,7 +20,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) | in progress | M22-M23: extract the captured-context composite into `windows-thread-ambient-sys`, a standalone platform layer that captures a thread's ambient state and applies it on another thread. M24-M26: `windows-namespace-request-sys`, marshalable Win32 namespace call parameter sets, over a round-one entry list audited from three real consumers (this repository's watcher and enumeration crates, and `MikeGrier/Globazog-rs`) rather than guessed. M27: `windows-platform-probes`, a durable home for the measurements this workspace's designs rest on, under a three-tier scheme (asserted / ignored / binary-only) where every tier is compiled by an ordinary build. Feature-scoped and deleted when complete; it is the whole of the `mikegrier/thread-ambient` branch's work, and is deliberately separate from the deferred namespace-facility items in [CHECKLIST.md](CHECKLIST.md). | [crates/windows-thread-ambient-sys/DESIGN-NOTES.md](crates/windows-thread-ambient-sys/DESIGN-NOTES.md) | | [crates/windows-overlapped-io-sys/CHECKLIST.md](crates/windows-overlapped-io-sys/CHECKLIST.md) | not started | M14: finish the contract audit -- categories 1, 2, 6, 8, 9 were not examined -- and sweep `outstanding()` for the advisory-predicate hazard. | [crates/windows-overlapped-io-sys/DESIGN-NOTES.md](crates/windows-overlapped-io-sys/DESIGN-NOTES.md) | | [crates/windows-ioring-sys/CHECKLIST.md](crates/windows-ioring-sys/CHECKLIST.md) | in progress | Memory-safe Rust over the Windows `IoRing` submission/completion ring, as a new crate. M1-M7 (ring lifecycle through the `ring-copy` topology-aligned sample) are complete and archived. The parked, pinned-thread `M6+` work and the new M10 contract audit remain. | [crates/windows-ioring-sys/DESIGN-NOTES.md](crates/windows-ioring-sys/DESIGN-NOTES.md) | -| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row still publishes its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` cannot tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row published its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` could not tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | Add a row here when new work is planned, against [CHECKLIST.md](CHECKLIST.md) or any crate's. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 976a58c3d..41168397b 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -84,6 +84,16 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. condition reddens the two list tests, through the bound oracle's count rule; mislabelling `TrailingBytes` reddens only `an_anomaly_reaches_the_row_as_its_kind`. + **The substring-to-variant conversion cost two assertions their discrimination, found by review.** + `c.contains("no online processors and processor groups")` became + `matches!(c, MeasuredButCountsAbsent { .. })`, which holds when the entry names only ONE of the + two -- exactly what the test forbids -- and the loop's labels stopped being asserted at all. + Measured: with `absent` truncated to its first entry the whole suite stayed green at 249 passed. + Both now assert the variant's `absent` payload, and both were observed to fail -- the truncation + reddens the both-absent test, and swapping the two names reddens both. The general lesson is that + converting an assertion from a substring to a variant DROPS whatever the substring discriminated + inside the payload; the variant is the weaker claim unless the payload comes with it. + **Which conditions are listed is deliberately not compared against the prose.** The code and the sentence come from one variant, so there is no second implementation to disagree through -- the correspondence holds by construction, which is stronger than a check. What remains checkable, and @@ -106,7 +116,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - [ ] **M3.3** -- Emit the row from a typed value through one writer. - The row is built today by interpolating eighteen values positionally into a `concat!` template. + The row is built today by interpolating seventeen values positionally into a `concat!` template. Two defect classes follow from that construction and both are closed by replacing it, not by checking it: @@ -114,8 +124,9 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text reaching the mined artifact is contamination of the contract. - **Field order and labelling.** A reordered value or a miscounted `{}` yields mislabelled data that - still parses, and nothing downstream can detect it. + **Field order and labelling.** The template carries seventeen positional `{}` placeholders for + the row's eighteen keys -- `reason` is a literal -- and a reordered value or a miscounted + placeholder yields mislabelled data that still parses, which nothing downstream can detect. A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the writer here rather than adding a serialization dependency -- this crate has none and the row is diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 03487d422..d005fdc26 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1573,9 +1573,12 @@ is the stronger move: report's machine-readable row. A typed row emitted by one writer cannot have this. - **Field order and labelling.** The row is built today by interpolating - eighteen values positionally through a `concat!` template. A reordered field or - a miscounted `{}` yields mislabelled data that still parses. A typed row with - one writer cannot have this either. + seventeen values positionally through a `concat!` template. A reordered field + or a miscounted `{}` yields mislabelled data that still parses. A typed row + with one writer cannot have this either. (Seventeen placeholders, not the + eighteen keys the row carries: `reason` is a literal in the template and is + not interpolated. The placeholder count is the one that matters here, because + the hazard is a miscounted `{}`.) - **Value divergence.** Two renderings of one field cannot disagree about its value when both read the field. diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index d5fe44d06..8cb25d08e 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row publishes its three diagnostic lists as bare counts, so a survey cannot tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 6e53a58ef..0daf99ba3 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -553,7 +553,6 @@ fn prose_lines_beginning(report: &str, prefix: &str) -> usize { .count() } -/// An NDJSON field read as a count, or `None` when it renders no number. /// How many entries a list-valued field carries. /// /// The three diagnostic fields were scalars and are now arrays of condition diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 304c3f607..ef7606cb2 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -2824,15 +2824,27 @@ fn an_empty_core_or_package_record_blocks_agreement() { // count and the policy derived from it while nothing else notices. The // live-host test already calls an empty core a parse error; without this, // cross_check would still certify one. - for (label, mutate) in [ + // **The expected pair travels with each case, because the variant alone + // cannot tell them apart.** Both arms raise `RelationsWithoutProcessors`, so + // matching the variant would run this loop twice and establish one thing -- + // an empty CORE record and an empty PACKAGE record would be + // interchangeable, and a `cross_check` that counted one for the other would + // pass. The old substrings did not separate them either: `Display` renders + // `{cores} core(s) and {packages} package(s) cover no processors` in one + // sentence, so both `"core(s) and"` and `"package(s) cover no processors"` + // matched whichever field was set. That gap predates the move to variants; + // the payload is what closes it. + for (label, mutate, expected) in [ ( - "core(s) and", + "an empty core record", Box::new(|o: &mut crate::topology::Observation| o.cores_without_processors = 1) as Box, + (1, 0), ), ( - "package(s) cover no processors", + "an empty package record", Box::new(|o: &mut crate::topology::Observation| o.packages_without_processors = 1), + (0, 1), ), ] { let mut observation = agreeing_observation(); @@ -2843,9 +2855,11 @@ fn an_empty_core_or_package_record_blocks_agreement() { assert!( check.parse_incomplete.iter().any(|c| matches!( c, - crate::topology::ParseIncomplete::RelationsWithoutProcessors { .. } + crate::topology::ParseIncomplete::RelationsWithoutProcessors { cores, packages } + if (*cores, *packages) == expected )), - "{label}: {check:?}" + "{label}: the entry must count the record that was empty, and only \ + it -- expected {expected:?}: {check:?}" ); assert_eq!( check.verdict(), @@ -3209,16 +3223,20 @@ fn a_measured_topology_reporting_no_processors_or_groups_blocks_agreement() { // machine its own parse says has no processors at all. The guard on the // package and core checks reads `online_processors > 0`, so the impossible // case silently switched those off as well. + // The label is the name the variant must carry in `absent`, not merely a + // caption for the failure message: zeroing one count must name THAT count. + // Matching the variant alone would pass for either, so the loop would run + // twice and establish one thing. for (label, mutate) in [ ( - "no online processors", + "online processors", Box::new(|o: &mut crate::topology::Observation| { o.online_processors = 0; o.raw_active_processors = 0; }) as Box, ), ( - "no processor groups", + "processor groups", Box::new(|o: &mut crate::topology::Observation| { o.groups = 0; o.raw_group_count = 0; @@ -3237,9 +3255,10 @@ fn a_measured_topology_reporting_no_processors_or_groups_blocks_agreement() { assert!( check.parse_incomplete.iter().any(|c| matches!( c, - crate::topology::ParseIncomplete::MeasuredButCountsAbsent { .. } + crate::topology::ParseIncomplete::MeasuredButCountsAbsent { absent } + if absent.as_slice() == [label] )), - "{label}: {check:?}" + "{label}: the entry must name the count that was zero, and only it: {check:?}" ); assert_eq!( check.verdict(), @@ -3265,12 +3284,26 @@ fn both_absent_counts_are_named_together_rather_than_one_standing_for_the_pair() observation.raw_group_count = 0; let check = observation.cross_check(); - assert!( - check.parse_incomplete.iter().any(|c| matches!( - c, - crate::topology::ParseIncomplete::MeasuredButCountsAbsent { .. } - )), - "{check:?}" + let absent = check + .parse_incomplete + .iter() + .find_map(|entry| match entry { + crate::topology::ParseIncomplete::MeasuredButCountsAbsent { absent } => Some(absent), + _ => None, + }) + .unwrap_or_else(|| panic!("no entry for the impossible machine at all: {check:?}")); + + // **The CONTENTS, not merely the variant.** Matching + // `MeasuredButCountsAbsent { .. }` holds when `absent` names one of the two, + // which is exactly what this test exists to forbid -- so it passed while + // establishing nothing beyond what the loop above already establishes. + // Measured: with `absent` truncated to its first entry, the whole suite + // stayed green at 249 passed. Found by a review. + assert_eq!( + absent.as_slice(), + ["online processors", "processor groups"], + "a machine missing both must name both, in the order the report states \ + them: {check:?}" ); } From ec6af09140aec45f76ac929f0d283b4088698530 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 21:38:11 -0400 Subject: [PATCH 06/46] feat(platform-probes)!: publish which counter disagreed A review found the list M3.1 built a vocabulary for and never wired. `Disagreement::code()` had ZERO production callers: the row carried `cross_check`, `not_compared`, `parse_incomplete` and `enumeration_anomalies`, and no `disagreements` field at all -- while the prose listed each disagreement with both readings. So a survey could see `"cross_check":"disagree"` and could not tell WHICH counter contradicted the parse, or whether several did. That is the same shape as the defect this milestone came from, in the one list nobody had noticed was missing, and it breaks the rule M3.1 itself established: a renderer may not tell a reader something the row cannot tell a survey. The gap predates the milestone -- `disagreements` was never in the row, not even as a count -- but M3.1 is what made it a stated violation rather than an omission, and M3.1 converted all three lists while wiring two. **Why every instrument here missed it, which is the durable finding.** The fact accounting enumerates the ROW's keys and requires each to be classified, so it asks "does anything read this key?" and never "does the prose state a fact the row omits?". A fact with no key is outside the set of things it can have an opinion about. The mutation sweep has the same orientation: it perturbs code the row's construction reaches. Between them they survived 41 review rounds and a zero-survivor sweep on this exact omission. The reviewer found it by reading the enum and asking who called `code()`. Recorded in DESIGN-NOTES.md as a limit of what the instruments cover -- the rule is a convention, not a checked property, until the second enumeration exists -- and queued as M3.5, which owns the accounting. The row now publishes `"disagreements":[...]`, and two rules read it: - The DISAGREE arm counts them against the prose's bare `- ` lines, symmetric with the two tagged kinds it already counts. That arm labels each kind, which is why it can separate all three where the INCOMPLETE arm can only sum. - The agreeing-verdict alarm covers it, and more directly than the other three: a non-empty `disagreements` makes the verdict `Disagree` by construction, so an agreeing row publishing one contradicts `CrossCheck::verdict` itself rather than a rule about what `agree` implies. Marked `!` for the same reason as M3.1: the row gained a key, and a consumer asserting on its shape must change. Two existing tests went red on the way, both correctly. An oracle fixture listed a disagreement in prose with no `disagreements` in its row, which is now the violation it looks like; and the accounting had nothing reading the key on an INCOMPLETE shape, which the same `AtValueUnlessAgreeing("[]")` classification `enumeration_anomalies` already carries resolves -- at `[]` there is no second rendering, and under `agree` a non-empty list is a violation in its own right. Sabotage-verified: with the row publishing an empty list unconditionally, `the_row_names_which_counter_disagreed` fails through the bound oracle. **Swept the placeholder count this change falsified, and stopped quoting it.** DESIGN-NOTES.md and CHECKLIST.md said the template interpolates "eighteen values"; a review corrected that to seventeen this afternoon; adding `disagreements` made it eighteen again within the hour. Both now state the coupling instead -- a field's name and its value are related only by counting positions -- which is the hazard the sentence is actually about and cannot rot. That is M2.14's first authoring rule, learned for the third time on the same sentence. Verified: 251 lib tests plus 14 ignored, 9 integration tests and doctests, on both `x86_64-pc-windows-msvc` and `i686-pc-windows-msvc`. clippy --all-targets --all-features clean, fmt clean, cargo doc clean under all three rustdoc deny lints, encoding clean over 636 files. A real `probe-topology` run emits `"cross_check":"agree","disagreements":[],"not_compared":[]`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 26 +++++++++++-- .../windows-platform-probes/DESIGN-NOTES.md | 35 +++++++++++++---- .../src/report_oracle.rs | 17 ++++++++- .../src/report_oracle/tests.rs | 2 +- crates/windows-platform-probes/src/tests.rs | 38 +++++++++++++++++++ .../src/topology_report.rs | 16 +++++++- .../tests/a_real_report_agrees_with_itself.rs | 13 +++++++ 7 files changed, 133 insertions(+), 14 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 41168397b..af44af456 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -116,7 +116,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - [ ] **M3.3** -- Emit the row from a typed value through one writer. - The row is built today by interpolating seventeen values positionally into a `concat!` template. + The row is built today by interpolating every value positionally into a `concat!` template. Two defect classes follow from that construction and both are closed by replacing it, not by checking it: @@ -124,9 +124,10 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text reaching the mined artifact is contamination of the contract. - **Field order and labelling.** The template carries seventeen positional `{}` placeholders for - the row's eighteen keys -- `reason` is a literal -- and a reordered value or a miscounted - placeholder yields mislabelled data that still parses, which nothing downstream can detect. + **Field order and labelling.** A field's name and its value are related only by counting + positions, so a reordered argument or a miscounted placeholder yields mislabelled data that + still parses, which nothing downstream can detect. Stated as the coupling rather than as a + count of placeholders: that count was written twice and wrong twice within an hour. A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the writer here rather than adding a serialization dependency -- this crate has none and the row is @@ -156,6 +157,23 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - [ ] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. + **The instrument enumerates in one direction only, and the other direction is where M3.1's rule + lives.** `ndjson_keys` reads the ROW's keys and requires each to be classified, so it asks "does + anything read this key?" -- never "does the prose state a fact the row omits?". A fact with no key + is outside the set of things it can have an opinion about. + + Measured, and this is how it was found rather than reasoned: `CrossCheck::disagreements` reached + the prose as a listed entry per disagreement and reached the row as nothing at all. `cross_check` + said `disagree` without saying WHICH counter did, which is the same shape as the defect the + milestone came from. It survived 41 review rounds, a zero-survivor mutation sweep and the fact + accounting, because every one of those instruments starts from what the row publishes. A review + found it by reading the enum and asking who called `code()` -- the answer was nobody. + + So the accounting needs a second enumeration, from the PROSE's facts to the row's keys, or the + rule "a renderer may not tell a reader something the row cannot tell a survey" has no instrument + behind it and holds only as long as someone remembers it. + + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates the facts a report publishes and measures, by mutation, which are read. The instrument is sound and the target changes: enumerate the row's fields, and require each to be read by an invariant or diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index d005fdc26..7460d5343 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1548,6 +1548,22 @@ comparisons and become **invariants on the observation, checked before rendering** -- `summary_missing` implies the verdict is not `agree`, and likewise for the other diagnostics and the counters. No parser is involved. +**Nothing here enforces that rule yet, and the first thing it would have caught +was already broken.** Every instrument in this crate starts from what the row +publishes -- the fact accounting enumerates the row's keys, the mutation sweep +perturbs code the row's construction reaches -- so all of them ask "does +anything read this key?" and none asks "does the prose state a fact the row +omits?". Measured: `CrossCheck::disagreements` was rendered per-entry in the +prose and published in the row as nothing at all, so a survey could see +`"cross_check":"disagree"` and not which counter disagreed. It survived 41 +review rounds and a zero-survivor mutation sweep. A reviewer found it by reading +the enum and asking who called `code()`. + +The second enumeration -- prose facts to row keys -- is queued as M3.5 in +[CHECKLIST.md](CHECKLIST.md). Until it exists the rule is a convention rather +than a checked property, which is worth stating plainly rather than leaving a +reader to assume the instruments cover it. + ### What the text-reading design cost Counted in [src/report_oracle.rs](src/report_oracle.rs): of 38 top-level @@ -1572,13 +1588,18 @@ is the stronger move: artifact. Measured on PR #88: an `io::Error` containing `{` was selected as the report's machine-readable row. A typed row emitted by one writer cannot have this. -- **Field order and labelling.** The row is built today by interpolating - seventeen values positionally through a `concat!` template. A reordered field - or a miscounted `{}` yields mislabelled data that still parses. A typed row - with one writer cannot have this either. (Seventeen placeholders, not the - eighteen keys the row carries: `reason` is a literal in the template and is - not interpolated. The placeholder count is the one that matters here, because - the hazard is a miscounted `{}`.) +- **Field order and labelling.** The row is built today by interpolating every + value positionally through a `concat!` template, so a field's name and its + value are related only by counting -- and a reordered argument or a miscounted + `{}` yields mislabelled data that still parses. A typed row with one writer + cannot have this either. + + Stated as the coupling rather than as a count, deliberately, and the reason is + on the record: this said "eighteen values", was corrected to "seventeen" when + a review counted the placeholders, and was falsified again within the hour by + M3.1's follow-up adding `disagreements`. The hazard is that the correspondence + is positional at all; how many positions there are is exactly the sort of + census this component keeps having to re-correct. - **Value divergence.** Two renderings of one field cannot disagree about its value when both read the field. diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 0daf99ba3..3acf3d712 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -430,7 +430,17 @@ fn check_diagnostics_against_verdict( // work contradicts its own rule exactly as a nonzero `parse_incomplete` // does. Measured, before this: `"cross_check":"agree"` beside // `"not_compared":3` was accepted with no violation. Found by a review. - for key in ["parse_incomplete", "enumeration_anomalies", "not_compared"] { + // `disagreements` belongs here for the same reason the other three do, + // and more directly: a non-empty `disagreements` makes the verdict + // `Disagree` by construction, so an agreeing row publishing one + // contradicts `CrossCheck::verdict` itself rather than merely the rule + // about what `agree` implies. + for key in [ + "disagreements", + "parse_incomplete", + "enumeration_anomalies", + "not_compared", + ] { // The alarm now NAMES the conditions rather than counting them, so // a violation reads `"parse_incomplete":["partitioning_summary_missing"]` // instead of `"parse_incomplete":1` -- which is the whole point of @@ -487,6 +497,11 @@ fn check_diagnostics_against_verdict( // that was previously accepted. Absence matters only where the prose // actually listed entries. for (label, key, fact) in [ + // The DISAGREE arm renders the disagreements themselves as bare + // `- ` lines and tags the other two, so all three are separable + // here -- which is why this arm can count each of them and the + // INCOMPLETE arm below can only sum. + (" - ", "disagreements", "disagreement count"), (" (not compared) ", "not_compared", "not compared count"), ( " (parse incomplete) ", diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index d56c1332f..3340ba744 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -991,7 +991,7 @@ fn a_not_compared_count_the_two_renderings_disagree_about_is_a_violation() { .replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#) .replace( r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed","active_processor_count_failed"],"enumeration_anomalies":[]}"#, + r#""parse_incomplete":[],"disagreements":["processor_groups"],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed","active_processor_count_failed"],"enumeration_anomalies":[]}"#, ); assert_eq!( diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index ef7606cb2..25e78c34f 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4666,3 +4666,41 @@ fn an_anomaly_reaches_the_row_as_its_kind() { "each anomaly reaches the row as its own kind: {text}" ); } + +#[test] +fn the_row_names_which_counter_disagreed() { + // **The list M3.1 built a vocabulary for and did not wire.** `cross_check` + // said `disagree`, and the prose named the counter and both readings -- but + // the row carried no `disagreements` field at all, so a survey could tell + // that a host's parse was contradicted and not WHAT contradicted it. + // + // Same shape as the defect the milestone came from, and it survived every + // instrument here for a structural reason worth remembering: the fact + // accounting in `tests/a_real_report_agrees_with_itself.rs` enumerates the + // ROW's keys, so a fact the row omits entirely is outside the set of things + // it can ask about. + let mut observation = clean_observation(); + observation.raw_group_count = 2; + + let text = crate::topology_report::report(BANNER, &observation); + + assert!( + text.contains("=> DISAGREE"), + "the fixture must actually disagree or this shows nothing: {text}" + ); + assert_eq!( + row_codes(&text, "disagreements"), + vec!["processor_groups".to_owned()], + "the row must name the counter that disagreed: {text}" + ); +} + +#[test] +fn a_report_with_nothing_to_report_lists_no_disagreements() { + // The acceptance half: an agreeing host publishes the key with an empty + // list rather than omitting it, so a survey can tell "no disagreements" + // from "this probe is too old to say". + let text = crate::topology_report::report(BANNER, &clean_observation()); + + assert!(text.contains(r#""disagreements":[]"#), "{text}"); +} diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index 43fb9ee50..b6ac9db09 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -689,6 +689,7 @@ pub fn report(banner: &str, observation: &Observation) -> String { .collect::>() .join(",") }; + let disagreements_json = quoted(check.disagreements.iter().map(|e| e.code()).collect()); let not_compared_json = quoted(check.not_compared.iter().map(|e| e.code()).collect()); let parse_incomplete_json = quoted(check.parse_incomplete.iter().map(|e| e.code()).collect()); let anomalies_json = quoted( @@ -705,7 +706,8 @@ pub fn report(banner: &str, observation: &Observation) -> String { r#""packages":{},"numa_domains":{},"numa_domains_without_processors":{},"cores":{},"#, r#""efficiency_classes":[{}],"caches":[{}],"outermost_partitioning_cache_level":{},"#, r#""outermost_partitioning_cache":"{}","#, - r#""policies":{{{}}},"cross_check":"{}","not_compared":[{}],"parse_incomplete":[{}],"#, + r#""policies":{{{}}},"cross_check":"{}","disagreements":[{}],"not_compared":[{}],"#, + r#""parse_incomplete":[{}],"#, r#""enumeration_anomalies":[{}],"numa_domains_only_in_cpu_sets":{}}}"# ), std::env::consts::ARCH, @@ -770,6 +772,18 @@ pub fn report(banner: &str, observation: &Observation) -> String { Verdict::Disagree => "disagree", Verdict::Incomplete => "incomplete", }, + // **The disagreements, which the row did not carry at all.** Before + // this, a survey could tell a run had disagreed -- `cross_check` says + // so -- but not WHICH counter disagreed, while the prose listed each one + // with both readings. That is the same shape as the defect this + // milestone came from, in the list nobody had noticed was missing: the + // vocabulary was built for all three lists and wired for two. + // + // It survived because the fact-accounting instrument enumerates the + // ROW's keys, so a fact the row omits entirely is outside what it can + // ask about. It catches a key nothing reads; it cannot catch a prose + // fact with no key. + disagreements_json, not_compared_json, // Separate from `not_compared`, because a mining pass that finds // `"cross_check":"incomplete"` needs to know whether this probe failed diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index f4c5472aa..12f0604f6 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -537,6 +537,19 @@ const FACTS: &[Fact] = &[ // construction rather than checked after the fact, which is the stronger // form. What remains checkable -- and checked -- is that the two renderings // list the same NUMBER of conditions. + Fact { + silence: Silence::AtValueUnlessAgreeing("[]"), + absent_is_silent_at: Some("[]"), + why: "the prose renders these as bare `- ` lines and only under \ + DISAGREE, which is the one verdict where that prefix is \ + unambiguous -- so an empty list on an INCOMPLETE host has no \ + second rendering to compare against. Under `agree` a non-empty \ + list is a violation in its own right, and a stronger one than the \ + other three carry: a non-empty `disagreements` makes the verdict \ + `Disagree` by construction, so an agreeing row publishing one \ + contradicts `CrossCheck::verdict` itself", + ..fact("disagreements", &["disagreement count"]) + }, Fact { absent_is_silent_at: Some("[]"), why: "with no entries the renderer emits no `(not compared)` line, so \ From 45cf0b08f78cd939f365dbd0a9786fe66e762391 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 21:42:28 -0400 Subject: [PATCH 07/46] docs(platform-probes): order M3.4 before M3.3, and record the review pause --- crates/windows-platform-probes/CHECKLIST.md | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index af44af456..612e0ae50 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -116,6 +116,10 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - [ ] **M3.3** -- Emit the row from a typed value through one writer. + > **-> PREREQUISITE: M3.4 lands first.** The reason is on M3.4: this item's nested per-entry data + > makes `ndjson_list_len` silently miscount, so the parsers it would break should be gone before + > the row changes shape rather than taught a shape they are about to lose. + The row is built today by interpolating every value positionally into a `concat!` template. Two defect classes follow from that construction and both are closed by replacing it, not by checking it: @@ -144,6 +148,31 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. + > **-> DO THIS BEFORE M3.3, and leave both IDs where they are.** M3.3 carries each diagnostic's + > data, which turns the flat code arrays into arrays of OBJECTS -- and `ndjson_list_len` splits on + > `,`, documented as safe for flat code arrays and nothing else. Pointed at + > `[{"code":"contradictory_cores","cores":3}]` it counts members rather than entries and returns 2 + > for one entry. It does not fail; it silently answers wrong, and every prose-comparison rule then + > compares that against the prose. Running M3.3 first therefore means teaching parsers a nested + > shape and deleting them one item later, with a silent-wrong-answer window in between. The IDs + > stay put because renumbering costs more than the mismatch, the same trade as M4/M5. + > + > Intended order for the rest of M3: **M3.2 -> M3.4 -> M3.3 -> M3.5**. + + > **-> CODE REVIEW RESUMES HERE.** Reviews are paused by the engineer's decision of 2026-09-12 + > until this crate no longer depends on prose as the oracle's subject, and this is the item that + > ends that dependence. The reasoning: a large share of PR #88's fifteen fix commits were defects + > in the prose-reading machinery -- the multibyte panic in `processors_in_banner`, `trim_matches` + > collapsing `[[0]]` and `[0]`, `prose_field` selecting the wrong line -- and every one of them is + > code this item deletes. Reviewing it closely is polishing something already scheduled for + > demolition. + > + > Recorded with the honest counterweight, so the decision can be re-judged on evidence rather than + > re-argued: of the six findings across the two reviews run on 2026-09-12, none was a defect in + > the prose oracle. Two were documentation drift, one was a test that had lost its + > discrimination, and the most valuable -- `disagreements` reaching the prose and not the row at + > all -- was about the ROW being incomplete and survives this item untouched. + Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence rules, four are comparison helpers, and **twenty-three exist only to extract values back out of rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. From 5b1d6fac24d7b0ce0a73ce58b949d1d0ed270118 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 21:53:50 -0400 Subject: [PATCH 08/46] feat(platform-probes): assert the observation-to-verdict invariants off the text Completed item: M3.2: Assert the surviving correspondences as invariants on the observation, before rendering. The correspondences worth keeping were never really about rendering. They relate a STATE to the verdict, and the report is only where that relation first became visible. `topology::invariant` states them as predicates over `Observation`, so they run whether or not anything was rendered and no parser stands between the rule and the values it reads. **The first attempt was wrong, in a way worth recording because it is not obvious.** It had four rules, two of which read `CrossCheck`'s lists: "a non-empty `parse_incomplete` forbids `agree`", and the same for `disagreements`. `CrossCheck::verdict` is a pure function of those three lists, so both are restatements of its definition -- they cannot fail for any input, and three of the tests written for them asserted acceptance under violation-sounding names, which is the exact defect this branch has been fixing all day. Worse than useless: a list-reading rule **cannot catch the defect this component exists because of.** That defect was a state -- a named partitioning level with no summary -- that `cross_check` had no branch for. A missing branch leaves the list EMPTY, so a list-reading rule sees nothing and the verdict it produces is `Agree` legitimately. The evidence survives only in the observation. So every rule now reads the observation. `blocking_states` names ten states that forbid an agreeing verdict -- the summary-missing bug, recorded anomalies, an unmeasured topology, no caches, no packages, no cores, a self-contradicting core record, a cache level numbered 0, enumerations that did not agree, a bracket that did not hold -- each computed from the observation, each with a push site in `cross_check` that it does not consult. Plus the two counter rules: `agree` asserts every check was MADE, so a counter that reported failure or disagreed cannot sit beside it. `check` takes the VERDICT rather than deriving it. That is not a convenience: `cross_check` is what makes these hold, so no observation can violate them while it is correct, and a derived version would leave every branch reachable only by editing the source -- a green run carrying no information. Supplying the verdict lets a test hand over the answer a broken `cross_check` would give. Bound at two places, covering different things. `observe` covers every observation this crate MEASURES, rendered or not, which is what the item asked for. `report` covers every observation anyone RENDERS, which on the test side is most of them, since the suite builds observations by hand rather than measuring a machine. Never in `cross_check`: `assert_holds` asks it for the verdict, so the assertion would recurse. Sabotage-verified on the push site that matters. Deleting the `PartitioningSummaryMissing` push from `cross_check` -- the fix for the originating defect -- reddens four tests, two of them new: the invariant's own `every_blocking_state_is_one_the_real_cross_check_already_reports`, and `the_row_names_the_probes_own_bug_when_it_detects_one` through `assert_holds` at the renderer binding. The other two are pre-existing verdict tests, so the invariant is not the sole detector for THIS push site; its value is the nine others, several of which have no dedicated test. `every_blocking_state_is_one_the_real_cross_check_already_reports` is the test that keeps this a postcondition rather than a second opinion: for each state, the verdict the crate actually draws must ALREADY be something other than `agree`. If one ever fails, the answer is to fix `cross_check`, not to relax the invariant. Verified: 260 lib tests plus 14 ignored, 9 integration tests and doctests. clippy --all-targets --all-features clean, fmt clean, encoding clean over 636 files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 24 +- .../windows-platform-probes/src/topology.rs | 19 +- .../src/topology/invariant.rs | 258 ++++++++++++++ .../src/topology/invariant/tests.rs | 331 ++++++++++++++++++ .../src/topology_report.rs | 9 + 5 files changed, 638 insertions(+), 3 deletions(-) create mode 100644 crates/windows-platform-probes/src/topology/invariant.rs create mode 100644 crates/windows-platform-probes/src/topology/invariant/tests.rs diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 612e0ae50..80d9dbe3c 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -102,7 +102,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. `verdict incomplete` shape, because the oracle reads the length. `corruptions` now APPENDS a code rather than substituting one, so the length always differs. -- [ ] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before +- [x] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before rendering. Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three @@ -114,6 +114,28 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. + **Done, and the item's own framing was wrong in a way worth recording.** It named + "diagnostics-against-verdict" and "counters-against-verdict" as rules to move. Two of those read + `CrossCheck`'s lists -- and `verdict` is a pure function of those lists, so such a rule restates + the definition, cannot fail for any input, and CANNOT CATCH A DELETED PUSH SITE: the deletion + empties the list, the rule sees nothing, and the verdict is `agree` legitimately. Written that + way first, with three tests that asserted acceptance under violation-sounding names. + + Every rule now reads the OBSERVATION. `blocking_states` names ten states that forbid an agreeing + verdict, each with a push site in `cross_check` that it does not consult, plus the two counter + rules. `check` takes the verdict rather than deriving it, so a test can supply the answer a + broken `cross_check` would give -- otherwise every branch is reachable only by editing the source + and a green run says nothing. + + Bound at `observe` (every observation MEASURED, rendered or not -- what this item asked for) and + at `report` (every observation RENDERED, which on the test side is most of them, since the suite + builds observations by hand). Not in `cross_check`, which would recurse. + + Sabotage: deleting the `PartitioningSummaryMissing` push reddens four tests, two of them new -- + the invariant's own accounting test, and a render test through `assert_holds` at the renderer + binding. The invariant is not the sole detector for that push site; its value is the nine others, + several of which have no dedicated test. + - [ ] **M3.3** -- Emit the row from a typed value through one writer. > **-> PREREQUISITE: M3.4 lands first.** The reason is on M3.4: this item's nested per-entry data diff --git a/crates/windows-platform-probes/src/topology.rs b/crates/windows-platform-probes/src/topology.rs index bc7085aa7..039a55d15 100644 --- a/crates/windows-platform-probes/src/topology.rs +++ b/crates/windows-platform-probes/src/topology.rs @@ -44,6 +44,7 @@ use windows_topology_sys::{ }; pub mod diagnostic; +pub mod invariant; pub use diagnostic::{Disagreement, NotCompared, ParseIncomplete}; @@ -1392,7 +1393,7 @@ pub fn observe( .outermost_partitioning_cache() .map(|(level, _)| level); - Observation { + let observation = Observation { online_processors, groups, numa_domains, @@ -1419,5 +1420,19 @@ pub fn observe( raw_group_count, raw_highest_numa_node, bracket, - } + }; + + // **Bound here, so the invariants hold for every observation this crate + // MEASURES whether or not one is ever rendered.** That is the difference + // between these and the correspondences they came from: the oracle can only + // speak about a report, so a caller who measures and never renders got + // nothing. `report` binds them too, which is what covers the observations + // the tests build by hand. + // + // Never in `cross_check`: `assert_holds` asks it for the verdict, so the + // assertion would recurse. + #[cfg(any(test, feature = "oracle-in-renderer"))] + invariant::assert_holds(&observation); + + observation } diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs new file mode 100644 index 000000000..e1a70af5e --- /dev/null +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -0,0 +1,258 @@ +// Copyright (c) Mike Grier. + +//! States an observation can be in that forbid an agreeing verdict. +//! +//! # These are the correspondences that survived, moved off the text +//! +//! [`crate::report_oracle`] checks relations between a report's two rendered +//! halves. The rules worth keeping are not really about rendering -- they relate +//! a STATE to the verdict, and the report is only where that relation became +//! visible. They are here instead, as predicates over [`Observation`], per +//! [DESIGN-NOTES.md](../../DESIGN-NOTES.md#d-encoded-row-is-the-contract). +//! +//! Two things change by moving them. They run whether or not anything was +//! rendered, so a caller that measures and never builds a report still gets +//! them; and no parser stands between the rule and the values it reads, which is +//! where a large share of this crate's defects lived. +//! +//! # Why every rule here reads the OBSERVATION, and none reads the lists +//! +//! This was got wrong on the first attempt, and the reason is worth recording +//! because it is not obvious. +//! +//! [`CrossCheck::verdict`] is a pure function of the three lists: a non-empty +//! `disagreements` gives `Disagree`, two empty lists beside it give `Agree`, +//! anything else gives `Incomplete`. So a rule of the form "a non-empty +//! `parse_incomplete` forbids `agree`" is not an invariant at all -- it restates +//! the definition, cannot fail for any input, and three such rules were written +//! here before that was noticed. +//! +//! Worse than useless: such a rule **cannot catch the defect this component +//! exists because of.** That defect was a state -- a named partitioning level +//! with no summary -- that `cross_check` had no branch for. A missing branch +//! means the list stays EMPTY, so a list-reading rule sees nothing to complain +//! about and the verdict it produces is `Agree` legitimately. The evidence that +//! something is wrong survives only in the observation. +//! +//! So each rule below names a state, computes it from the observation, and +//! requires the verdict to have moved off `agree`. A push site deleted from +//! `cross_check` leaves the state visible here and fires the rule; that is the +//! whole design, and it is what the sabotage evidence in +//! [CHECKLIST.md](../../CHECKLIST.md) M3.2 demonstrates. +//! +//! # What that means for testing them +//! +//! No input can violate these while `cross_check` is correct, because +//! `cross_check` is what makes them hold. They are reachable by CODE CHANGE, not +//! by data -- so [`check`] takes the verdict alongside the observation, letting +//! a test supply the answer a broken `cross_check` would give, and the sabotage +//! loop confirms that a real deletion reddens a real test. + +use std::fmt; + +use super::{Coherence, CrossCheck, Observation, PartitioningCache, Verdict}; + +#[cfg(test)] +mod tests; + +/// A state that forbids an agreeing verdict, found beside one. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Violation { + /// The observation is in a state that must prevent `agree`, and did not. + StateWithAgreeingVerdict { + /// The state, as [`blocking_states`] names it. + state: &'static str, + }, + /// `agree` without the comparison it asserts having been made. + /// + /// `agree` is the claim that every check this probe could make WAS made and + /// matched, so a counter that reported failure cannot sit beside it. + AgreedWithoutComparingCounter { + /// The counter that was not compared. + counter: &'static str, + }, + /// `agree` beside a counter that does not equal what was enumerated. + AgreedDespiteCounterMismatch { + /// The counter. + counter: &'static str, + /// What the parse carried. + parsed: usize, + /// What the counter reported. + read: usize, + }, +} + +impl fmt::Display for Violation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::StateWithAgreeingVerdict { state } => write!( + f, + "the observation is in the `{state}` state, which forbids an \ + agreeing verdict, but the verdict is `agree` -- so whatever in \ + `cross_check` should have reported this state did not" + ), + Self::AgreedWithoutComparingCounter { counter } => write!( + f, + "the verdict is `agree`, which asserts every check was made, \ + but {counter} reported failure so its comparison was not made" + ), + Self::AgreedDespiteCounterMismatch { + counter, + parsed, + read, + } => write!( + f, + "the verdict is `agree` but the parse carries {parsed} where \ + {counter} read {read}" + ), + } + } +} + +/// Every state `observation` is in that forbids an agreeing verdict. +/// +/// **Computed from the observation, never from the cross-check's lists.** Each +/// of these has a push site in [`Observation::cross_check`], and the point of +/// enumerating them separately is that a deleted push site leaves the state +/// here and the list empty -- so this is what notices, and the list could not. +/// +/// Not every entry in `parse_incomplete` appears here, and that is deliberate +/// rather than an omission: some are derived counts whose only source IS the +/// cross-check's own arithmetic, so restating them would be the tautology this +/// module exists to avoid. What belongs here is a state readable from the +/// observation on its own terms. +#[must_use] +pub fn blocking_states(observation: &Observation) -> Vec<&'static str> { + let mut states = Vec::new(); + + // The defect this component exists because of: the renderer prints this + // state as `BUG IN THIS PROBE ... Nothing below about cache partitioning + // can be trusted`, and `cross_check` had no branch for it. + if matches!( + observation.partitioning_cache(), + PartitioningCache::SummaryMissing(_) + ) { + states.push("summary missing for the outermost partitioning cache"); + } + + if !observation.enumeration_anomalies.is_empty() { + states.push("the enumeration recorded anomalies"); + } + + if !observation.topology_was_measured { + states.push("the topology was not measured from a running machine"); + } + + if observation.caches.is_empty() { + states.push("no cache levels were reported"); + } + + if observation.online_processors > 0 && observation.packages == 0 { + states.push("no packages were reported"); + } + + if observation.online_processors > 0 && observation.cores.is_empty() { + states.push("no cores were reported"); + } + + if observation + .cores + .iter() + .any(super::CoreShape::contradicts_itself) + { + states.push("a core record contradicts itself"); + } + + if observation.caches.iter().any(|cache| cache.level == 0) { + states.push("a cache level is numbered 0"); + } + + if !matches!(observation.coherence, Coherence::Agreed) { + states.push("the crate's two enumerations did not agree"); + } + + if observation.bracket != super::BracketOutcome::HeldStill { + states.push("the bracket did not establish that the machine held still"); + } + + states +} + +/// Every invariant relating `observation` to `verdict` that does not hold. +/// +/// Empty is the answer for every pair this crate can produce, because +/// `cross_check` is what makes these hold. A non-empty result means a push site +/// or a guard in `cross_check` stopped reporting a state the observation still +/// shows. +/// +/// **Takes the verdict rather than deriving it**, so a test can supply the +/// answer a broken `cross_check` would give and see the rule fire. Deriving it +/// would leave every branch reachable only by editing the source, and a green +/// run would carry no information about whether the branch works. +#[must_use] +pub fn check(observation: &Observation, verdict: Verdict) -> Vec { + if verdict != Verdict::Agree { + return Vec::new(); + } + + let mut found: Vec = blocking_states(observation) + .into_iter() + .map(|state| Violation::StateWithAgreeingVerdict { state }) + .collect(); + + // Zero is how both counters report failure, so a zero beside `agree` is the + // verdict claiming a comparison that could not have happened. The NUMA + // counter is deliberately absent, for the reason the renderer gives: it + // reports the largest node NUMBER rather than a count, so there is no + // enumerated quantity to hold it to, and nodes 0 and 2 are a valid sparse + // topology. + for (counter, parsed, read) in [ + ( + "GetActiveProcessorCount", + observation.online_processors, + observation.raw_active_processors as usize, + ), + ( + "GetActiveProcessorGroupCount", + observation.groups, + observation.raw_group_count as usize, + ), + ] { + if read == 0 { + found.push(Violation::AgreedWithoutComparingCounter { counter }); + } else if parsed != read { + found.push(Violation::AgreedDespiteCounterMismatch { + counter, + parsed, + read, + }); + } + } + + found +} + +/// [`check`], as an assertion, for the call sites that are bound to it. +/// +/// # Panics +/// +/// Panics listing every invariant the observation violated. +pub fn assert_holds(observation: &Observation) { + let violations = check(observation, observation.cross_check().verdict()); + assert!( + violations.is_empty(), + "an observation violated {} invariant(s) relating it to its verdict:\n{}", + violations.len(), + violations + .iter() + .map(|violation| format!(" - {violation}")) + .collect::>() + .join("\n"), + ); +} + +/// Named so the module's own import of [`CrossCheck`] is not dead. +/// +/// `assert_holds` reaches the verdict through it, and a reader looking for the +/// relation between the two types should find it stated rather than inferred. +const _: fn(&CrossCheck) -> Verdict = CrossCheck::verdict; diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs new file mode 100644 index 000000000..b1fe28607 --- /dev/null +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -0,0 +1,331 @@ +// Copyright (c) Mike Grier. + +//! Tests for the observation-to-verdict invariants. +//! +//! Every violation branch is reachable here because [`super::check`] takes the +//! VERDICT rather than deriving it. That is not a convenience: `cross_check` is +//! what makes these invariants hold, so no observation can violate them while it +//! is correct, and a derived version would leave each branch reachable only by +//! editing the source. Supplying the verdict lets a test hand over the answer a +//! broken `cross_check` would give. +//! +//! Half of these assert ACCEPTANCE. An invariant that fires on a legal pair +//! costs a reader more than one that misses an illegal one, because noise trains +//! them to ignore the instrument -- the same rule the report oracle is built on. + +use super::{Violation, blocking_states, check}; +use crate::topology::{ + BracketOutcome, CacheLevel, CoreShape, Observation, PartitioningCache, Verdict, +}; + +/// An observation whose every check passes, to perturb one field at a time. +/// +/// Deliberately a copy of the shape `crate::tests::agreeing_observation` builds +/// rather than a call to it: that helper is private to a sibling test module, +/// and a fixture reaching across module boundaries couples two suites that +/// should be free to change apart. +fn agreeing() -> Observation { + Observation { + online_processors: 4, + groups: 1, + numa_domains: 1, + numa_domains_without_processors: 0, + numa_domains_only_in_cpu_sets: 0, + numa_domains_unreported: 0, + numa_domains_with_conflicting_labels: 0, + topology_was_measured: true, + cores_only_in_cpu_sets: 0, + cores_without_processors: 0, + packages_without_processors: 0, + overlapping_walk_relations: 0, + described_relations: 0, + unreported_relations: 0, + processor_attribute_conflicts: 0, + highest_numa_node: Some(0), + packages: 1, + cores: vec![CoreShape { + simultaneous_multithreading: true, + efficiency_class: 0, + processors: 4, + }], + caches: vec![CacheLevel { + level: 1, + processors_per_domain: vec![4], + }], + partitioning_cache_level: None, + enumeration_anomalies: Vec::new(), + coherence: windows_topology_sys::Coherence::Agreed, + raw_active_processors: 4, + raw_group_count: 1, + raw_highest_numa_node: Some(0), + bracket: BracketOutcome::HeldStill, + } +} + +#[test] +fn the_observation_this_crate_produces_is_in_no_blocking_state() { + // The acceptance half, and the premise every perturbation below rests on: + // if the fixture already blocked agreement, each test would be asserting + // against two states instead of the one it introduced. + assert_eq!(blocking_states(&agreeing()), Vec::<&str>::new()); + assert_eq!(check(&agreeing(), Verdict::Agree), Vec::new()); +} + +#[test] +fn the_verdict_the_crate_actually_draws_holds_every_invariant() { + // Against the REAL pair rather than a supplied verdict, so this would catch + // an invariant that is wrong about what `cross_check` does. + let observation = agreeing(); + let verdict = observation.cross_check().verdict(); + + assert_eq!(verdict, Verdict::Agree); + assert_eq!(check(&observation, verdict), Vec::new()); +} + +/// Each blocking state, the field that produces it, and the name it reports. +/// +/// A table rather than a test each, because the property is identical and the +/// interesting part is that NONE of them is missing -- which the count assertion +/// below states as a relation rather than as a census. +type Perturbation = (&'static str, Box); + +fn perturbations() -> Vec { + vec![ + ( + "summary missing for the outermost partitioning cache", + Box::new(|o: &mut Observation| o.partitioning_cache_level = Some(9)), + ), + ( + "the enumeration recorded anomalies", + Box::new(|o: &mut Observation| { + o.enumeration_anomalies = vec![windows_topology_sys::EnumerationAnomaly { + source: windows_topology_sys::Source::CpuSets, + offset: 0, + kind: windows_topology_sys::AnomalyKind::TrailingBytes { remaining: 3 }, + }]; + }), + ), + ( + "the topology was not measured from a running machine", + Box::new(|o: &mut Observation| o.topology_was_measured = false), + ), + ( + "no cache levels were reported", + Box::new(|o: &mut Observation| o.caches = Vec::new()), + ), + ( + "no packages were reported", + Box::new(|o: &mut Observation| o.packages = 0), + ), + ( + "no cores were reported", + Box::new(|o: &mut Observation| o.cores = Vec::new()), + ), + ( + "a core record contradicts itself", + Box::new(|o: &mut Observation| { + o.cores = vec![CoreShape { + simultaneous_multithreading: false, + efficiency_class: 0, + processors: 4, + }]; + }), + ), + ( + "a cache level is numbered 0", + Box::new(|o: &mut Observation| { + o.caches = vec![CacheLevel { + level: 0, + processors_per_domain: vec![4], + }]; + }), + ), + ( + "the crate's two enumerations did not agree", + Box::new(|o: &mut Observation| { + o.coherence = windows_topology_sys::Coherence::NotCollected; + }), + ), + ( + "the bracket did not establish that the machine held still", + Box::new(|o: &mut Observation| o.bracket = BracketOutcome::Changed), + ), + ] +} + +#[test] +fn every_blocking_state_forbids_an_agreeing_verdict() { + for (state, mutate) in perturbations() { + let mut observation = agreeing(); + mutate(&mut observation); + + assert!( + blocking_states(&observation).contains(&state), + "{state}: the observation is in this state and `blocking_states` \ + did not say so" + ); + assert!( + check(&observation, Verdict::Agree) + .contains(&Violation::StateWithAgreeingVerdict { state }), + "{state}: the state is present beside an agreeing verdict and the \ + invariant did not fire" + ); + } +} + +#[test] +fn every_blocking_state_is_one_the_real_cross_check_already_reports() { + // **The invariants must agree with `cross_check`, or they are a second + // opinion rather than a postcondition.** For each state, the verdict the + // crate actually draws must already be something other than `agree` -- so + // the invariant is pinning behaviour that exists rather than demanding + // behaviour that does not. + // + // This is what makes the whole module a postcondition. If one of these + // failed, the right response would be to fix `cross_check`, not to relax + // the invariant. + for (state, mutate) in perturbations() { + let mut observation = agreeing(); + mutate(&mut observation); + + let cross_check = observation.cross_check(); + assert_ne!( + cross_check.verdict(), + Verdict::Agree, + "{state}: the invariant forbids `agree` here, so `cross_check` must \ + already forbid it: {cross_check:?}" + ); + assert_eq!( + check(&observation, cross_check.verdict()), + Vec::new(), + "{state}: and against the real verdict there is nothing to report" + ); + } +} + +#[test] +fn a_blocking_state_is_silent_when_the_verdict_already_admits_it() { + // The acceptance half of the whole module: these rules constrain the + // AGREEING verdict and nothing else. A report that says it is incomplete is + // free to be in any of these states -- that is what incomplete means. + for verdict in [Verdict::Incomplete, Verdict::Disagree] { + for (state, mutate) in perturbations() { + let mut observation = agreeing(); + mutate(&mut observation); + + assert_eq!( + check(&observation, verdict), + Vec::new(), + "{state}: {verdict:?} admits the doubt, so there is nothing to \ + contradict" + ); + } + } +} + +#[test] +fn the_partitioning_state_is_read_from_the_observation_not_the_list() { + // **The distinction the module is built on, asserted rather than described.** + // A rule reading `parse_incomplete` cannot catch a DELETED push site: the + // deletion empties the list, so the rule sees nothing and the verdict is + // `agree` legitimately. Reading the observation is what survives that. + // + // Expressed here as: the state is visible with no reference to the + // cross-check at all. + let mut observation = agreeing(); + observation.partitioning_cache_level = Some(9); + + assert!(matches!( + observation.partitioning_cache(), + PartitioningCache::SummaryMissing(9) + )); + assert!( + blocking_states(&observation) + .contains(&"summary missing for the outermost partitioning cache"), + "read from the observation, with the cross-check never consulted" + ); +} + +#[test] +fn an_agreeing_verdict_requires_the_counters_to_have_been_read() { + // Zero is how both counters report failure, so `agree` beside a zero is the + // verdict claiming a comparison that could not have happened. + for (counter, mutate) in [ + ( + "GetActiveProcessorCount", + Box::new(|o: &mut Observation| o.raw_active_processors = 0) + as Box, + ), + ( + "GetActiveProcessorGroupCount", + Box::new(|o: &mut Observation| o.raw_group_count = 0), + ), + ] { + let mut observation = agreeing(); + mutate(&mut observation); + + assert_eq!( + check(&observation, Verdict::Agree), + vec![Violation::AgreedWithoutComparingCounter { counter }], + "{counter}: a failed read cannot sit beside a verdict claiming \ + every check was made" + ); + + let cross_check = observation.cross_check(); + assert_ne!( + cross_check.verdict(), + Verdict::Agree, + "{counter}: and the real cross-check already forbids it: \ + {cross_check:?}" + ); + } +} + +#[test] +fn an_agreeing_verdict_requires_the_counters_to_have_matched() { + for (counter, parsed, read, mutate) in [ + ( + "GetActiveProcessorCount", + 4, + 8, + Box::new(|o: &mut Observation| o.raw_active_processors = 8) + as Box, + ), + ( + "GetActiveProcessorGroupCount", + 1, + 2, + Box::new(|o: &mut Observation| o.raw_group_count = 2), + ), + ] { + let mut observation = agreeing(); + mutate(&mut observation); + + assert_eq!( + check(&observation, Verdict::Agree), + vec![Violation::AgreedDespiteCounterMismatch { + counter, + parsed, + read, + }], + "{counter}: the enumeration and the counter differ, so `agree` is \ + not available" + ); + } +} + +#[test] +fn the_numa_counter_is_deliberately_not_held_to_the_enumeration() { + // `GetNumaHighestNodeNumber` reports the largest node NUMBER, which Windows + // does not promise equals the node count -- nodes 0 and 2 are a valid + // sparse topology. Holding it to `numa_domains` would manufacture a + // violation on hardware reporting itself correctly, which is the same + // over-claim `cross_check` was corrected to stop making. + let mut observation = agreeing(); + observation.numa_domains = 2; + observation.highest_numa_node = Some(2); + observation.raw_highest_numa_node = Some(2); + + assert_eq!(observation.cross_check().verdict(), Verdict::Agree); + assert_eq!(check(&observation, Verdict::Agree), Vec::new()); +} diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index b6ac9db09..eea88af8a 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -280,6 +280,15 @@ pub fn report(banner: &str, observation: &Observation) -> String { // draws. A rule with one exception is not a rule, and the exception was the // conclusion drawn from the one field the two sources are known to // contradict each other about. + // **Bound here as well as in `observe`, and the two cover different + // things.** `observe` covers every observation this crate MEASURES; this + // covers every observation anyone RENDERS, which on the test side is most + // of them -- the suite builds observations by hand rather than measuring a + // machine, so binding only at `observe` would leave the invariants + // exercised on one host shape. + #[cfg(any(test, feature = "oracle-in-renderer"))] + crate::topology::invariant::assert_holds(observation); + let check = observation.cross_check(); let parse_in_doubt = check.parse_in_doubt(); From 3a451501aa385c57b525f41aa1be8e1173e91c3e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 22:02:26 -0400 Subject: [PATCH 09/46] feat(platform-probes)!: retire the prose oracle and re-aim the accounting at the row Completed items: M3.4, M3.5 Completed item: M3.4: Retire the prose-against-row correspondences and the parsers that serve only them. Completed item: M3.5: Re-aim the shape corpus and the fact accounting at the row. **Committed together because they cannot be separated.** The fact-accounting instrument is built entirely on `report_oracle::check` and the `Correspondence` variants, so deleting the correspondences leaves it measuring nothing and the suite red between the two items. Splitting would produce a commit that does not pass, which the checklist rules call the worse defect. `report_oracle` read the report's PROSE, extracted values back out of the rendered sentences, and compared them against the NDJSON row. DESIGN-NOTES.md -> #d-encoded-row-is-the-contract retired that: the row is a machine contract and the prose is for a reader, they carry different obligations, and nothing is required to hold between them. Measured. `report_oracle.rs` 79,394 -> 8,874 bytes; its tests 105,299 -> 5,598; the integration instrument 71,007 -> 25,689. All eight prose correspondences and all twenty-three extraction helpers are gone -- a parser for a format this crate itself writes, which behaved like one: a multi-byte panic, a substring matching inside an opaque `io::Error`, a `trim_matches` collapsing `[[0]]` and `[0]`. What survives is the writer's own output read back, which is the one thing no amount of typing upstream can do for itself: exactly one machine-readable line, brackets balanced, no repeated top-level key. The balance check is string-aware because a failed discovery's `io::Error` is interpolated into a string value and an OS message is free to contain a bracket -- a naive counter would report every such host as malformed. The repeated-key check is there because that malformation survives a consumer's parse and silently changes what it reads. **The key-set check is deliberately not here.** Asserting it needs a list of expected keys, and a list written here is a census -- this component re-corrected the same census three times in one day. M3.3 makes the row a typed value, at which point the set is derivable from the type. Moved there rather than approximated now. **The accounting now enumerates in the direction the rule lives in.** It no longer asks "which prose facts does the oracle read"; it asks, for every state `topology::invariant` knows forbids agreement, whether the row publishes a condition for it -- and holds the prose's diagnostic line count against the row's condition count across the corpus. That is the enumeration whose absence let `disagreements` reach the prose and not the row at all, surviving 41 review rounds and a zero-survivor mutation sweep. Sabotage-verified against exactly that: dropping `disagreements` from the row reddens `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists`. **One asymmetry, found by the instrument rather than reasoned.** Counting all four lists against prose lines failed on the anomaly shapes: the prose folds every anomaly into ONE sentence while the row lists one code per anomaly, so three anomalies read as two dropped entries. `enumeration_anomalies` counts on its own axis, checked against the OBSERVATION -- one published code per anomaly recorded -- which is the artifact the row owes fidelity to. Checking it against the number inside that sentence would be the prose-reading this milestone retired. Both publication rules carry a corpus guard, because both skip a shape in no blocking state and a drifted all-healthy corpus would leave them green while checking nothing. The corpus itself, its shape builders and the real-host render are unchanged. Marked `!`: `report_oracle`'s public surface is replaced. `Correspondence`, `check`'s return type, `architecture_in_banner` and the extraction helpers are gone, and `check` now returns `Vec`. Verified: 184 lib tests plus 14 ignored, 9 integration tests and doctests. clippy --all-targets --all-features clean, fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 45 +- .../src/report_oracle.rs | 1824 ++---------- .../src/report_oracle/tests.rs | 2567 +---------------- .../tests/a_real_report_agrees_with_itself.rs | 1398 ++------- 4 files changed, 554 insertions(+), 5280 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 80d9dbe3c..a97ce29b9 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -168,7 +168,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. item. This subsumes M2.18: the banner becomes a typed field like any other, and the question of who may construct one is answered by the row's constructor rather than separately. -- [ ] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. +- [x] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. > **-> DO THIS BEFORE M3.3, and leave both IDs where they are.** M3.3 carries each diagnostic's > data, which turns the flat code arrays into arrays of OBJECTS -- and `ndjson_list_len` splits on @@ -206,7 +206,28 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for a format that no longer has two readers. -- [ ] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. + **Done, together with M3.5, because they cannot be separated.** The fact-accounting instrument is + built entirely on `report_oracle::check` and the `Correspondence` variants, so deleting the + correspondences leaves it measuring nothing and the suite red between the two items. Committed as + one commit citing both IDs, per the checklist rule for coupled items, rather than split into a + commit that does not pass. + + Measured: `report_oracle.rs` 79,394 -> 8,874 bytes, its tests 105,299 -> 5,598, the integration + instrument 71,007 -> 25,689. All eight prose correspondences and all twenty-three extraction + helpers are gone. + + What survives is the row's well-formedness: exactly one machine-readable line, brackets balanced + (string-aware, because a failed discovery's `io::Error` is interpolated into a string value and an + OS message is free to contain a bracket), and no repeated top-level key. That last one is the + malformation that survives a consumer's parse and changes what it reads, since most JSON readers + take the last. + + **The key-set check is deliberately NOT here.** Asserting it needs a list of expected keys, and a + list written here is a census -- this component re-corrected the same census three times in one + day. M3.3 makes the row a typed value, at which point the key set is derivable from the type + rather than declared beside it. Moved there rather than approximated here. + +- [x] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. **The instrument enumerates in one direction only, and the other direction is where M3.1's rule lives.** `ndjson_keys` reads the ROW's keys and requires each to be classified, so it asks "does @@ -224,6 +245,26 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. rule "a renderer may not tell a reader something the row cannot tell a survey" has no instrument behind it and holds only as long as someone remembers it. + **Done, with M3.4, and the second enumeration exists.** The instrument no longer asks "which prose + facts does the oracle read" -- there are none. It asks, for every state `topology::invariant` knows + forbids agreement, whether the row publishes a condition for it; and it holds the prose's + diagnostic line count against the row's condition count across the corpus. + + Sabotage-verified against the defect that motivated it: dropping `disagreements` from the row -- + the omission that survived 41 review rounds, a zero-survivor mutation sweep and the old accounting + -- now reddens `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists`. + + **One asymmetry, found by the instrument rather than reasoned.** Counting all four lists against + prose lines failed: the prose folds every anomaly into ONE + `windows-topology-sys recorded N enumeration anomal...` sentence while the row lists one code per + anomaly, so three anomalies read as two dropped entries. `enumeration_anomalies` counts on its own + axis and is checked against the OBSERVATION -- one published code per anomaly recorded -- which is + the artifact the row owes fidelity to. Checking it against the number inside that sentence would + be the prose-reading this milestone retired. + + Both publication rules carry a corpus guard, because both skip a shape in no blocking state and a + drifted all-healthy corpus would leave them green while checking nothing. + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates the facts a report publishes and measures, by mutation, which are read. The instrument is sound and diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 3acf3d712..8c32df9e5 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -1,1714 +1,246 @@ // Copyright (c) Mike Grier. -//! Correspondences a rendered report must satisfy *between* its parts. +//! Whether a rendered report's machine-readable row is well-formed. //! -//! # Why this exists rather than more per-part tests +//! # What this used to be, and why it is not that any more //! -//! A pull-request review found [`crate::topology_report`] printing -//! `BUG IN THIS PROBE ... Nothing below about cache partitioning can be -//! trusted` while the verdict two paragraphs below read `=> agree`. Twenty-eight -//! rounds of per-artifact review and a zero-surviving-mutant `cargo-mutants` run -//! had both passed over it, because **every function involved was correct on its -//! own terms** and the defect lived in the relation between two of them. +//! This module was an oracle over the report's PROSE: it read the rendered +//! sentences, extracted values back out of them, and compared those against the +//! NDJSON row. It was built after a pull-request review found a report calling a +//! state a bug while the verdict two paragraphs below certified the same run as +//! `agree`. //! -//! That is the shape no per-part instrument can see. A test asserts one -//! function's output; a mutant perturbs one function's behaviour; a reviewer -//! reads one artifact and finds it locally true. A contradiction between two -//! locally-true parts is invisible to all three. +//! [DESIGN-NOTES.md](../DESIGN-NOTES.md#d-encoded-row-is-the-contract) retired +//! that design. The row is a machine contract -- mined across a fleet, and what +//! this workspace's designs rest on -- and the prose is for a reader. They carry +//! different obligations: the row must be CORRECT, machine-enforced; the prose +//! must be ACCURATE AND READABLE, enforced by review. Nothing is required to +//! hold *between* them. //! -//! # It reads the artifact, not the state that produced it +//! The correspondences worth keeping were never about rendering. They related a +//! STATE to the verdict, and they live in [`crate::topology::invariant`] now, as +//! predicates over the observation that run whether or not anything was +//! rendered. Of the thirty-eight functions this module carried, twenty-three +//! existed only to extract values back out of rendered text -- a parser for a +//! format this crate itself writes, and it behaved like one: a multi-byte panic, +//! a substring matching inside an opaque `io::Error`, a `trim_matches` +//! collapsing `[[0]]` and `[0]`. None of those was a defect in a probe. //! -//! Every check here works on the **rendered text** -- the thing a reader and a -//! log-mining pass actually receive. Checking internal state instead would miss -//! precisely the defect class this exists for: the state was consistent in the -//! case above, and the two renderings of it were not. +//! # What is left, and why anything is left at all //! -//! # What it deliberately does not do +//! Structure makes most of the old checks unrepresentable rather than detected, +//! which is the stronger move. What structure cannot check is **the writer** -- +//! whatever turns values into bytes is downstream of every type, and several of +//! this crate's defects lived exactly there. So one check survives: the report +//! carries exactly one machine-readable row, and that row is a well-formed flat +//! JSON object. //! -//! It does not re-derive what the renderer should have printed. A second -//! implementation of the rendering rules would be a check of the copy rather -//! than of the contract, and would drift from the original the moment either -//! moved. Each rule below relates **two things already visible in the report**, -//! so the oracle has no opinion of its own to go stale. +//! That is not a correspondence. It is the writer's own output being read back, +//! which is the one thing no amount of typing upstream can do for itself. //! -//! Over-constraining is the same defect as under-specifying, so a report that -//! omits a fact is not a violation -- absence is checked only where the report -//! itself makes a claim that requires the other part to agree. -//! -//! # Every correlation here is one the REPORT already renders twice -//! -//! That is the admission rule. A correspondence is added when the report states -//! the same fact in both halves and nothing relates the two -- never because -//! someone imagined that two things ought to correspond. The test is a property -//! of the artifact, not of anyone's intuition about it. -//! -//! **Two ways of finding one, and the rule admits both.** Most were caught the -//! hard way, by a rendered report contradicting itself; each variant below names -//! the contradiction that earned it. The structured pairs in -//! `check_structured_pairs` (private, so named rather than linked) were found -//! the systematic way instead, by walking -//! every NDJSON field against the prose and seeing which facts were rendered -//! twice with nothing comparing them -- no defect had occurred, and waiting for -//! one would have been the worse plan. -//! -//! This section said "every correlation here was observed violated", which -//! excluded the second route and so contradicted that function's own history -//! four screens below. Written today, while correcting a different overstatement -//! in the same paragraph; a rule stated more strongly than the code supports is -//! the exact defect this module exists to catch, and it went in as part of the -//! fix for one. Found by a review. -//! -//! **Stated as a rule rather than a count, deliberately.** This paragraph used -//! to read "seeded with three correlations ... a fourth is added when a fourth -//! contradiction is found" -- while the enum directly below it already had four -//! variants. The census was wrong when it was written, survived every review of -//! this branch, and would have gone stale again at the next addition even if it -//! had been right. The rule cannot: it stays true however many there are, and -//! it is the part a reader actually needs, since what matters is that nothing -//! here is speculative rather than how much of it there is. - -/// A correspondence between two parts of a report that did not hold. +//! **The key SET is deliberately not checked here yet.** Asserting it needs a +//! list of expected keys, and a list written here would be a census that rots -- +//! this component has re-corrected the same census three times in one day. Once +//! M3.3 makes the row a typed value, the key set is derivable from the type +//! rather than declared beside it, and the check belongs there. Queued in +//! [CHECKLIST.md](../CHECKLIST.md) M3.3 rather than approximated here. + +/// A way the report's machine-readable row is not well-formed. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Correspondence { - /// The prose raised an alarm while the verdict said everything matched. - /// - /// The original defect. An alarm is a statement that some part of the - /// report cannot be trusted; a verdict of `agree` is a statement that every - /// check made was made and matched. Both can be locally true and they - /// cannot both describe the same run. - AlarmWithAgreeingVerdict { - /// The alarm line found in the prose. - alarm: String, - /// Where the agreeing verdict was found: `prose` or `ndjson`. - verdict_source: &'static str, - }, - /// One fact rendered twice, with the two renderings disagreeing. - /// - /// A report carries its findings for a human in prose and for a mining pass - /// in NDJSON. A consumer that reconciles the two cannot, and neither - /// rendering is self-evidently the wrong one. - ProseAndNdjsonDisagree { - /// What the fact is called, for the reader of the failure. - fact: &'static str, - /// As the prose rendered it. - prose: String, - /// As the NDJSON rendered it. - ndjson: String, - }, - /// A hardware claim was stated without its caveat while the report's own - /// evidence says the parse was in doubt. +pub enum RowDefect { + /// The report carries no machine-readable row. /// - /// The renderer's rule is that every hardware conclusion is gated on the - /// parse being whole. A claim printed bare, in a report that elsewhere - /// reports doubt, is that rule with an exception -- and a rule with an - /// exception is not a rule. - UncaveatedClaimUnderDoubt { - /// The claim that was stated bare. - claim: &'static str, - /// The report's own visible evidence of doubt. - evidence: String, - }, - /// The banner names one machine and the body describes another. + /// Every report has one, including the unmeasured shape -- that is what lets + /// a fleet survey tell a host where discovery FAILED from a job that never + /// ran the probe. A report without one silently excludes exactly the hosts + /// most worth counting. + Missing, + /// The report carries more than one. /// - /// The banner is the line a reader uses to decide whether two runs are - /// comparable at all, so a banner describing a different machine from the - /// body under it invalidates every comparison drawn from the report -- - /// while each half stays locally correct, which is what let this survive - /// review. - BannerDisagreesWithBody { - /// The processor count the banner named. - banner: String, - /// The processor count the body reported. - body: String, + /// A mining pass reads the first line that looks like a row, so a second one + /// is not extra data -- it is an ambiguity about which line is the contract. + /// Measured before containment existed: an `io::Error` whose text contained + /// `{` was selected as the row, so a reader checked the caller's text + /// instead of the probe's. + Duplicated { + /// How many lines look like a row. + count: usize, }, - /// The prose states a fact the machine-readable line does not carry at all. - /// - /// **Absence where the report has already made the claim.** This module - /// holds that a report omitting a fact is not a violation -- but that rule - /// is about facts the report never mentions. Once the PROSE states one, the - /// report has made a claim that the other rendering is required to agree - /// with, and a missing counterpart is that requirement going unmet rather - /// than the fact being absent. - /// - /// Every comparison here was written as "both sides present, do they - /// match", so a rendering that DROPPED a field read as silence: measured, - /// deleting `"processors"` from the row, deleting the whole `policies` - /// object, and deleting the partitioning discriminator each left a report - /// the oracle accepted, with the prose still making all three claims. A - /// mining pass reading such a row gets no value and no warning. - /// - /// `report_unmeasured` needs no exemption: it renders neither side of any - /// TOPOLOGY fact, so there is no prose claim for a missing counter, cache or - /// policy to leave unanswered. - /// - /// Not "renders neither side" flatly, which is what this said and is false: - /// the short object publishes `arch`, and the banner can name the same - /// architecture, so that one correspondence IS rendered twice on an - /// unmeasured report -- and is checked there, by - /// `an_architecture_contradiction_survives_an_attribution_disclaimer`. The - /// exemption this paragraph explains is about the facts the short object - /// omits, not about the whole shape. - RenderedOnlyInProse { - /// The fact the prose stated. - fact: &'static str, - /// What the prose said, with nothing to compare it against. - prose: String, + /// The row's brackets do not balance, so it is not a JSON object. + Unbalanced { + /// The row, as rendered. + row: String, }, - /// The verdict says every check was made, and a check's evidence is absent. + /// The row repeats a key. /// - /// **An `agree` verdict is a claim about what the run DID, not only about - /// what matched.** `CrossCheck` reports `agree` to mean every check this - /// probe could make was made and matched -- so a report that agrees while - /// omitting the line a check reads is contradicting its own verdict, even - /// though the two halves it still renders agree perfectly. - /// - /// Measured: deleting the `GetActiveProcessorCount` line from an otherwise - /// untouched agreeing report left `check` returning nothing at all. The - /// counter comparison simply skipped, because it was written to compare two - /// present values and to say nothing otherwise -- the same shape as the - /// dropped-counterpart class, in the one place where the VERDICT is what the - /// missing side contradicts. - EvidenceMissingWithAgreeingVerdict { - /// The correspondence whose rendering the report did not carry. - fact: &'static str, + /// A repeated key is not a parse error in every JSON reader -- most take the + /// last -- so this is precisely the kind of malformation that survives a + /// consumer's parse and changes what it reads. + RepeatedKey { + /// The key rendered more than once. + key: String, }, } -/// Every correspondence `report` violates, in the order they were checked. -/// -/// An empty result means every correlation this oracle knows about held. It -/// does **not** mean the report is correct: an oracle is a floor, not a -/// specification. -#[must_use] -pub fn check(report: &str) -> Vec { - let mut found = Vec::new(); - let ndjson = ndjson_line(report); - - check_alarm_against_verdict(report, ndjson, &mut found); - check_prose_against_ndjson(report, ndjson, &mut found); - check_claims_against_doubt(report, ndjson, &mut found); - check_structured_pairs(report, ndjson, &mut found); - check_counters_against_verdict(report, ndjson, &mut found); - check_banner_against_body(report, ndjson, &mut found); - check_diagnostics_against_verdict(report, ndjson, &mut found); - check_partitioning_answer(report, ndjson, &mut found); - - found -} - -/// Each arm of the partitioning discriminator, and the prose that announces it. -/// -/// **Mapped from the renderer, arm by arm, rather than guessed.** A rule written -/// against a subset produces false violations on the arms it guessed wrong, and -/// that failure mode is not hypothetical here: this same module once shipped a -/// banner rule asserting a correspondence `attribution()` explicitly declines to -/// claim, and it had to be narrowed after a review reproduced the false positive. -/// -/// The markers are each arm's OPENING sentence, which is the part that cannot be -/// confused with another arm's. Changing any of these strings is a change to the -/// report's contract with its readers, not a rewording. -const PARTITIONING_ARMS: &[(&str, &str)] = &[ - ( - "outermost cache that partitions the processors it covers: ", - "level", - ), - ("no cache level reported more than one domain", "none"), - ("no cache levels were reported at all", "no_levels_reported"), - ( - "at least one cache level reported more than one distinct domain", - "not_unique", - ), - (SUMMARY_MISSING_MARKER, "summary_missing"), -]; - -/// The partitioning answer the prose gives, against the one the NDJSON publishes. -/// -/// Gap 2 of M2.10, found by a review corrupting a real report and watching the -/// oracle accept it. The oracle already compared -/// `outermost_partitioning_cache_level` -- the NUMBER -- so the level was -/// checked. What went unread was the DISCRIMINATOR, the field saying whether a -/// level was selected at all: changing `"level"` to another value left the -/// oracle silent while the prose still read `outermost cache that partitions the -/// processors it covers: L2 (8 domains)`. -/// -/// The two are opposite answers to this probe's central question -- "a level -/// partitions" against "none does" -- and the renderer is explicit about why the -/// field exists: the level alone said `null` for every absent case alike, so a -/// query counting nulls as "machines no cache level partitions" folded in -/// machines where a level DOES partition. This rule is what stops the two -/// renderings of that answer drifting apart. -fn check_partitioning_answer(report: &str, ndjson: Option<&str>, found: &mut Vec) { - let Some(ndjson) = ndjson else { - return; - }; - let announced: Vec<&str> = PARTITIONING_ARMS - .iter() - .filter(|(marker, _)| has_line_beginning(report, marker)) - .map(|(_, arm)| *arm) - .collect(); - - // **Absent WITH a prose arm is a dropped discriminator, not silence.** - // `report_unmeasured` carries neither side and so announces no arm, which is - // why the exemption needed no special case -- but returning early on the - // field alone also excused a MEASURED report that printed an arm and lost - // its discriminator. Measured: deleting the field from a `level` report left - // the prose conclusion standing with nothing to relate it to, and the oracle - // accepted it. - let Some(published) = ndjson_field(ndjson, "outermost_partitioning_cache") else { - // ANY announced arm, not just a single one. Written for the single-arm - // slice first, which excused the worse report of the two: two prose - // answers AND no discriminator. - if !announced.is_empty() { - found.push(Correspondence::RenderedOnlyInProse { - fact: "outermost partitioning answer", - prose: announced.join(" and "), - }); - } - return; - }; - - match announced.as_slice() { - // The prose names no partitioning answer at all. Not a contradiction -- - // the rule fires only where the report makes the claim twice. - [] => {} - [announced] if *announced == published => {} - [announced] => found.push(Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning answer", - prose: (*announced).to_owned(), - ndjson: published.to_owned(), - }), - // Two arms' prose in one report. The arms are exclusive by construction - // -- they are one `match` -- so this is the prose giving two answers to a - // question that has one, whatever the NDJSON says. - several => found.push(Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning answer", - prose: several.join(" and "), - ndjson: published.to_owned(), - }), - } - - // **`summary_missing` names a LEVEL in both renderings, and it was the one - // arm whose number nothing compared.** Found by a review, after the rule - // above had closed the discriminator itself -- which is the pattern this - // module keeps repeating: a rule is added per fact, so the fact added - // alongside it goes unread. - // - // The oracle's other level comparison is keyed to the `Level` arm's prose - // label, `outermost cache that partitions the processors it covers: `, so it - // never fires here. And `summary_missing` is the only non-`Level` arm whose - // NDJSON level is a NUMBER rather than `null`, which is exactly what made - // the omission invisible: the three arms beside it have no number to - // disagree about. - // - // It matters most precisely where it was missing. This arm is the state the - // renderer prints as `BUG IN THIS PROBE ... Nothing below about cache - // partitioning can be trusted` -- a report already telling its reader it is - // unreliable, in which the two renderings of WHICH level went unchecked. - if let Some(prose_level) = summary_missing_level(report) { - compare( - found, - "summary-missing outermost level", - prose_level, - ndjson_field(ndjson, "outermost_partitioning_cache_level"), - ); - } -} - -/// The level the `summary_missing` prose names, if the report carries that arm. -/// -/// Keyed to the same opening sentence [`PARTITIONING_ARMS`] uses, so the two -/// cannot drift apart: if that sentence is reworded, both stop matching together -/// rather than one silently continuing to match a report the other no longer -/// recognises. -fn summary_missing_level(report: &str) -> Option<&str> { - leading_digits(after_marker_leading_a_line(report, SUMMARY_MISSING_MARKER)?) -} - -/// The text following `marker` on a line the RENDERER leads with it. -/// -/// **A marker search over the whole report reads the caller's text as the -/// probe's.** The banner is contained into the first line rather than dropped, -/// so its content survives -- and an unanchored `find` then picks a marker out -/// of it wherever it lands. Measured before this: a banner ending -/// `... named L99 as the outermost` produced a `summary-missing outermost level` -/// of 99 against the row's 1, and one ending -/// `windows-topology-sys recorded 99 enumeration anomalies` produced an anomaly -/// count of 99 against the row's 0. Both are the oracle raising a violation -/// about a report that does not contain the defect -- a false alarm invented out -/// of caller text, which is the failure mode that costs a reader the most. -/// -/// The rest of the module reads by line start, and these two were what was left -/// of the older style. The bullet is part of the renderer's shape, not a -/// concession: `CrossCheck` writes its diagnostic entries as ` - `, -/// so the marker genuinely never begins its line. Accepting the bullet cannot -/// re-open the hole it closes, because a contained banner is one line beginning -/// `host:` and an attribution-shaped one is `host:` lines and a disclaimer -- -/// neither can present a line whose first content is `- `. -fn after_marker_leading_a_line<'a>(report: &'a str, marker: &str) -> Option<&'a str> { - report - .lines() - .find_map(|line| strip_entry_tag(line.trim_start()).strip_prefix(marker)) -} - -/// A diagnostic entry's leading tag, removed. -/// -/// **`CrossCheck`'s entries carry a tag whose spelling depends on the verdict, -/// and reading only one of them left the rule unread on the others.** The -/// `INCOMPLETE` arm writes ` - {caveat}`; the `DISAGREE` arm writes -/// ` (parse incomplete) {caveat}` and ` (not compared) {skipped}` so a -/// reader can tell the disagreement from what was merely not established. -/// Anchoring to the bullet alone therefore went blind to the anomaly count on -/// exactly the verdict where a reader most needs it. Measured: the -/// `(parse incomplete) ` rendering returned `None` where the `- ` rendering -/// returned `Some("2")`, so corrupting the count on a disagreeing report raised -/// no violation at all. -/// -/// Matches the SHAPE of a tag rather than restating the renderer's two literal -/// strings, which would be a second copy to drift. Containment makes that safe: -/// a contained banner is one line beginning `host:` and an attribution-shaped -/// one is `host:` lines and a disclaimer, so neither can present a line whose -/// first content is a bullet or a parenthesised tag. -fn strip_entry_tag(line: &str) -> &str { - if let Some(rest) = line.strip_prefix("- ") { - return rest; - } - - line.strip_prefix('(') - .and_then(|rest| rest.split_once(") ")) - .map_or(line, |(_tag, rest)| rest) -} - -/// The run of ASCII digits `text` opens with, if it opens with one. -fn leading_digits(text: &str) -> Option<&str> { - let end = text - .find(|character: char| !character.is_ascii_digit()) - .unwrap_or(text.len()); - - (end > 0).then(|| &text[..end]) -} - -/// The opening sentence of the `summary_missing` arm, up to the level it names. -const SUMMARY_MISSING_MARKER: &str = "BUG IN THIS PROBE: the topology crate named L"; - -/// The diagnostic counts, against the verdict drawn beside them and against the -/// prose that lists the same entries. -/// -/// `not_compared`, `parse_incomplete` and `enumeration_anomalies` are published -/// in the NDJSON and their entries are listed in the prose, and until now -/// nothing compared any of the three. `parse_incomplete` was read only as a -/// nonzero predicate for the heterogeneity caveat, which is a different question -/// from whether the two renderings agree. -/// -/// **The verdict rule binds to a SPECIFIED contract, not to current behaviour.** -/// [`crate::topology_report`] states it where the NDJSON is emitted: "anomalies -/// populate `parse_incomplete`, and a non-empty `parse_incomplete` forces the -/// verdict away from `agree`, so `cross_check == "agree"` IMPLIES no record -/// failed to decode", and the crate pins it with -/// `a_dropped_enumeration_record_blocks_agreement_even_when_every_counter_matches` -/// rather than leaving it a promise in a comment. So an `agree` verdict beside a -/// nonzero count of either is the report contradicting its own published rule -- -/// the same shape as the alarm-beside-an-agreeing-verdict defect this module was -/// built for, in the field a mining pass trusts most. -/// -/// The implication is stated ONE WAY and is read that way here: a run whose -/// counter failed to read has a complete parse and still reports `incomplete`, -/// so a nonzero `not_compared` is NOT asserted to force the verdict, and no rule -/// below claims it does. -fn check_diagnostics_against_verdict( - report: &str, - ndjson: Option<&str>, - found: &mut Vec, -) { - let Some(ndjson) = ndjson else { - return; - }; - - if ndjson_field(ndjson, "cross_check") == Some("agree") { - // `not_compared` belongs here too, and was missing. `CrossCheck`'s - // verdict makes `agree` imply that nothing was skipped as well as that - // nothing failed to decode, so an agreeing report publishing skipped - // work contradicts its own rule exactly as a nonzero `parse_incomplete` - // does. Measured, before this: `"cross_check":"agree"` beside - // `"not_compared":3` was accepted with no violation. Found by a review. - // `disagreements` belongs here for the same reason the other three do, - // and more directly: a non-empty `disagreements` makes the verdict - // `Disagree` by construction, so an agreeing row publishing one - // contradicts `CrossCheck::verdict` itself rather than merely the rule - // about what `agree` implies. - for key in [ - "disagreements", - "parse_incomplete", - "enumeration_anomalies", - "not_compared", - ] { - // The alarm now NAMES the conditions rather than counting them, so - // a violation reads `"parse_incomplete":["partitioning_summary_missing"]` - // instead of `"parse_incomplete":1` -- which is the whole point of - // the change, applied to the oracle's own output. - if let Some(raw) = ndjson_raw_field(ndjson, key) - && ndjson_list_len(ndjson, key).is_some_and(|entries| entries > 0) - { - found.push(Correspondence::AlarmWithAgreeingVerdict { - alarm: format!("\"{key}\":{raw}"), - verdict_source: "ndjson", - }); - } - } - } - - // **The anomaly COUNT is inside the prose sentence, not inferable from the - // line count.** `CrossCheck` emits one `parse_incomplete` entry reading - // `windows-topology-sys recorded N enumeration anomal...` however many there - // were, so counting lines can never check N -- a report can say it recorded - // 2 while publishing 99 and agree about every total. Found by a review, - // which also caught that the checklist claimed all three diagnostic counts - // were compared "against the prose listings" when this one was only ever - // checked as a nonzero predicate under an `agree` verdict. - // - // Read for EVERY verdict, deliberately. The `agree` rule above is about a - // contradiction with the verdict; this is about the two renderings of one - // number, which must agree whatever the verdict says. - if let Some(prose_anomalies) = anomaly_count_in_prose(report) { - // The row lists each anomaly's code now, so the count it is compared - // against is the list's LENGTH. Rendered back to a string rather than - // compared as a number, because `compare` reports absence too and the - // prose count arrives as text. - compare( - found, - "enumeration anomaly count", - prose_anomalies, - ndjson_list_len(ndjson, "enumeration_anomalies") - .map(|entries| entries.to_string()) - .as_deref(), - ); - } - - // The prose lists these entries one per line, and how it marks them depends - // on the verdict: the DISAGREE arm labels each kind, so the two are counted - // separately, while the INCOMPLETE arm renders both as a bare `- `, which - // makes only their total recoverable. Counting what the prose can actually - // distinguish, rather than a number it does not render, is the whole habit - // this module is built on. - if has_line_beginning(report, "=> DISAGREE") { - // **Zero lines is not a prose claim.** These compare a COUNT OF LINES - // against a field, and the renderer emits no line when the count is - // zero -- so routing them through `compare`'s absence path reported a - // dropped counterpart for a prose that had said nothing, on a fixture - // that was previously accepted. Absence matters only where the prose - // actually listed entries. - for (label, key, fact) in [ - // The DISAGREE arm renders the disagreements themselves as bare - // `- ` lines and tags the other two, so all three are separable - // here -- which is why this arm can count each of them and the - // INCOMPLETE arm below can only sum. - (" - ", "disagreements", "disagreement count"), - (" (not compared) ", "not_compared", "not compared count"), - ( - " (parse incomplete) ", - "parse_incomplete", - "parse incomplete count", +impl std::fmt::Display for RowDefect { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Missing => f.write_str("the report carries no machine-readable row"), + Self::Duplicated { count } => write!( + f, + "the report carries {count} machine-readable rows, so which one \ + is the contract is ambiguous" ), - ] { - let listed = prose_lines_beginning(report, label); - match ndjson_list_len(ndjson, key) { - Some(entries) => compare_counts(found, fact, listed, entries), - None if listed > 0 => found.push(Correspondence::RenderedOnlyInProse { - fact, - prose: listed.to_string(), - }), - None => {} + Self::Unbalanced { row } => { + write!(f, "the row's brackets do not balance: {row}") } + Self::RepeatedKey { key } => write!( + f, + "the row renders `{key}` more than once, which most JSON readers \ + resolve silently by taking the last" + ), } } - - if has_line_beginning(report, "=> INCOMPLETE") { - let listed = prose_lines_beginning(report, " - "); - let (Some(skipped), Some(caveats)) = ( - ndjson_list_len(ndjson, "not_compared"), - ndjson_list_len(ndjson, "parse_incomplete"), - ) else { - // **The prose has already listed the entries here.** This arm sums - // two fields, so it was written to return unless BOTH are present -- - // and that made a row which dropped either one silent on exactly the - // verdict whose reason those counts carry. Found by the deletion - // sweep, on a corpus shape rather than on this host: the counts are - // zero here, so the entries are absent and there is nothing to drop. - if listed > 0 { - found.push(Correspondence::RenderedOnlyInProse { - fact: "incomplete-verdict listing count", - prose: listed.to_string(), - }); - } - return; - }; - compare_counts( - found, - "incomplete-verdict listing count", - listed, - skipped + caveats, - ); - } } -/// The anomaly count named inside the diagnostic sentence, if it is present. -/// -/// Keyed to the sentence `CrossCheck` writes, and reading the number that -/// follows it. The count is rendered INSIDE one entry rather than as one entry -/// per anomaly, so nothing about the number is recoverable from counting lines. -fn anomaly_count_in_prose(report: &str) -> Option<&str> { - const MARKER: &str = "windows-topology-sys recorded "; - leading_digits(after_marker_leading_a_line(report, MARKER)?) -} - -/// How many lines of `report` begin with `prefix`. -fn prose_lines_beginning(report: &str, prefix: &str) -> usize { - report +/// Every way `report`'s machine-readable row is not well-formed. +#[must_use] +pub fn check(report: &str) -> Vec { + let rows: Vec<&str> = report .lines() - .filter(|line| line.starts_with(prefix)) - .count() -} - -/// How many entries a list-valued field carries. -/// -/// The three diagnostic fields were scalars and are now arrays of condition -/// codes, so every rule that compared a count against them asks for the length -/// here instead of parsing a number. The count is still the fact those rules -/// check; it is simply derived from the list rather than restated beside it. -/// -/// Splitting on `,` is safe for these fields and only these: a code is an -/// identifier this crate mints in `topology::diagnostic`, so it carries no -/// comma and no nesting. Pointing this at `caches`, whose entries are objects, -/// would count members rather than entries -- `cache_rows` exists for that. -fn ndjson_list_len(ndjson: &str, key: &str) -> Option { - let raw = ndjson_raw_field(ndjson, key)?; - if !raw.starts_with('[') { - return None; - } - - let inner = normalise_list(raw); - Some(if inner.is_empty() { - 0 - } else { - inner.split(',').count() - }) -} - -/// Push a disagreement between two counts of the same thing. -fn compare_counts(found: &mut Vec, fact: &'static str, prose: usize, json: usize) { - if prose != json { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact, - prose: prose.to_string(), - ndjson: json.to_string(), - }); - } -} -/// The banner names the machine the body describes. -/// -/// A run makes three discoveries of the host -- one before the measurement, -/// `measure`'s own, and one after -- and the banner used to be built from an -/// endpoint, so it could name a different topology from the body beneath it -/// with nothing in the report saying so. -/// -/// **The rule applies only where the report claims the correspondence**, and -/// that qualifier is load-bearing here. `attribution` takes the two readings -/// that bracket the measurement and renders the first as the banner; the body -/// comes from a `measure()` between them. When the two endpoints differ, or -/// either fails, the report says so in as many words -- "which of them names the -/// machine the body below describes was not established" -- and on such a report -/// the first banner line need not describe the body at all. Asserting a -/// contradiction there would be the oracle over-claiming exactly where the -/// renderer went to trouble not to, so it returns instead. -/// -/// **On the origin branch this rule was unconditional, and that was correct -/// there**: `measure_observed` builds the banner from the body's own topology, -/// which makes the mismatch unrepresentable. That construction change is a -/// separate peel, so the ambiguity is live on this branch and the rule has to -/// respect it. When the construction lands, the guard below stops being reached -/// rather than becoming wrong. -/// -/// What the rule still buys where attribution IS determinate: the banner and the -/// body remain two independent *derivations* from one topology -- -/// `Fingerprint::from_topology` and `observe`, each with its own filter for -/// which processors count. Those have already disagreed once, when -/// `from_topology` summed core-domain membership and printed `0p` for a machine -/// about to be measured on four processors. -fn check_banner_against_body(report: &str, ndjson: Option<&str>, found: &mut Vec) { - let Some(ndjson) = ndjson else { - return; - }; - - // The architecture is checked FIRST and outside the attribution exemption, - // for the reason given on the function below. - check_architecture_against_body(report, ndjson, found); - - // The report's own statement that it cannot attribute the body to either - // reading. Matched on the rendered disclaimers rather than on the count of - // `host:` lines, because the count is incidental to how `attribution` - // happens to render today and these sentences are the contract. - // - // It exempts the PROCESSOR COUNT specifically, which is what the disclaimer - // is about: the two readings name different topologies and this run cannot - // say which describes the body. - if has_line_beginning(report, "HOST READINGS DISAGREE:") - || has_line_beginning(report, "HOST NOT ESTABLISHED:") - { - return; - } + .filter(|line| line.starts_with('{')) + .collect(); - let Some(banner) = report.lines().find(|line| line.starts_with("host:")) else { - return; + let [row] = rows.as_slice() else { + return vec![if rows.is_empty() { + RowDefect::Missing + } else { + RowDefect::Duplicated { count: rows.len() } + }]; }; - // Absent on a report whose discovery failed: the banner reads `UNKNOWN` and - // `report_unmeasured` emits no processor count, so there is nothing to - // relate and no violation to claim. - let (Some(banner_count), Some(body_count)) = ( - processors_in_banner(banner), - ndjson_field(ndjson, "processors"), - ) else { - return; - }; + let mut found = Vec::new(); - if banner_count != body_count { - found.push(Correspondence::BannerDisagreesWithBody { - banner: banner_count.to_owned(), - body: body_count.to_owned(), + if !balanced(row) { + found.push(RowDefect::Unbalanced { + row: (*row).to_owned(), }); + // Every check below reads the object's members, which is not a question + // that means anything about text that is not an object. + return found; } -} -/// The architecture every banner names, against the one the body publishes. -/// -/// The architecture is rendered twice -- in the banner and as the NDJSON's -/// `arch` -- and went unread until a review corrupted one and watched the oracle -/// accept it. Both come from `std::env::consts::ARCH` today, so they cannot -/// currently differ; that is a fact about the renderer rather than a contract, -/// and exactly the kind of coincidence this oracle is built not to lean on. -/// -/// **Outside the attribution exemption, and that is the point of separating it.** -/// The disclaimers say which of the two bracket READINGS describes the body was -/// not established. That is a statement about the machine's topology, not about -/// its instruction set: when every banner the report managed to parse names the -/// same architecture, then whichever reading describes the body, the -/// architecture is that one -- so a body naming a different one is a -/// contradiction the disclaimer does not excuse. Found by a review; before this, -/// two `x86_64` banners under a disclaimer beside an `aarch64` body produced no -/// violation at all. -/// -/// **When the banners disagree with EACH OTHER about the architecture, this -/// returns**, because then the question really is unestablished and asserting -/// anything would be the over-claim the exemption exists to prevent. -/// -/// Checked before the processor-count guard as well, so it still reaches -/// `report_unmeasured`, which emits `arch` but no `processors`. -fn check_architecture_against_body(report: &str, ndjson: &str, found: &mut Vec) { - let announced: Vec<&str> = report - .lines() - .filter(|line| line.starts_with("host:")) - .filter_map(architecture_in_banner) - .collect(); - - let Some(first) = announced.first() else { - return; - }; - if announced.iter().any(|architecture| architecture != first) { - return; + let mut seen: Vec<&str> = Vec::new(); + for key in keys(row) { + if seen.contains(&key) { + found.push(RowDefect::RepeatedKey { + key: key.to_owned(), + }); + } else { + seen.push(key); + } } - // Past the two returns above, the banner HAS named an architecture and every - // reading agrees on it -- so a row without `arch` is the report stating the - // fact once, not a report that never stated it. - compare( - found, - "architecture", - first, - ndjson_field(ndjson, "arch").map(|body| body.trim_matches('"')), - ); -} -/// The architecture the banner names. -/// -/// The fingerprint renders as ` p/c ...`, optionally behind one or -/// more `!!MARKER!!` prefixes, so the architecture is the first token that is -/// not one of those. -/// -/// **Every marker is skipped, not a named list of them.** The first version of -/// this stripped `!!taint!!` alone and read `!!assumed!!` as an architecture, -/// reporting a contradiction against a perfectly good banner -- caught by an -/// existing acceptance test, which is what those are for. -/// -/// A first attempt at justifying that generality said "the renderers emit at -/// least `!!SYNTHETIC!!`, `!!UNOFFICIAL!!`, `!!RESTORED!!`, `!!assumed!!` and -/// `!!taint!!`", which is an overclaim of the kind this module exists to catch, -/// written while fixing another one. Counted: a `host:` line is rendered by -/// `banner_line`/`banner_line_for` from a `Fingerprint`, which emits -/// `!!{provenance}!!` only when the provenance is not `Measured` -- so the only -/// markers this function can meet today are **`!!SYNTHETIC!!` and -/// `!!RESTORED!!`**. `!!UNOFFICIAL!!` belongs to `BuildIdentity`, which never -/// reaches this line, and `!!assumed!!` and `!!taint!!` are not emitted anywhere -/// -- they exist only as invented fixtures in this module's own tests. -/// -/// The generality is still right, and on a better argument than a miscounted -/// list: the marker is a *shape* the banner reserves for provenance, and this -/// module observes the renderer rather than mirroring it. Matching the shape -/// cannot fall out of step; enumerating today's two spellings would. -/// -/// **Only a FINGERPRINT names an architecture, so the count shape is required -/// here.** A failed read names no architecture, and a caller may pass any string -/// as a banner. Without that guard the first token of such a line is read as an -/// architecture and contradicts the NDJSON every time: measured, `UNKNOWN` -/// against a real `"arch"` produced a false violation, and the crate's own -/// `host: TEST-FIXTURE` fixture produced fourteen more. What the failed-read -/// line actually looks like -- and why the guard has to read by position rather -/// than by substring -- is on `fingerprint_tokens`, which owns that decision. -/// (Named without a link: it is private, and a public doc may not link to it.) -/// -/// This paragraph used to describe that line as "the bare word `UNKNOWN`", which -/// is not what `banner_line_for` writes. The correction was made on -/// `fingerprint_tokens` and not here, so the two docs contradicted each other -/// in the same module -- a fix applied to one statement of a fact while another -/// statement of it survived, which is the drift this crate keeps paying for. -/// -/// Note carefully which side this constrains. Requiring the BANNER to carry -/// `p/c` is what establishes it is a fingerprint; requiring the BODY to -/// carry a processor count is the coupling that wrongly confined this rule to -/// measured reports. The first is the renderer's contract, the second was an -/// accident of where the code sat. -/// **Public because the question has to have ONE answer.** A test helper asked -/// the same thing -- "does this `host:` line name an architecture?" -- with its -/// own cheaper rule, `line.contains("p/")`. That agreed with this module until -/// this module started reading the tokens by position, and then it did not: -/// measured, a failed-discovery banner of -/// `host: UNKNOWN -- topology discovery failed: 16p/foo something opaque` -/// satisfied the helper and not the oracle, so the fact accounting demanded that -/// `arch` be read on a report where the oracle is right to say nothing, and a -/// perfectly valid unmeasured report failed the suite. -/// -/// A second implementation of a predicate is not a check of it; it is a copy -/// that agrees until it does not. Consumers ask here instead. -pub fn architecture_in_banner(banner: &str) -> Option<&str> { - fingerprint_tokens(banner).map(|(architecture, _)| architecture) -} - -/// The `` and `p/c` tokens a banner names, if it names a fingerprint. -/// -/// **Read BY POSITION, because a banner that is not a fingerprint can still -/// contain the substrings one would have.** Both readers used to search the -/// whole line -- the count as "digits before the first `p/`", the architecture -/// as "first token that is not a taint marker and does not contain `p/`" -- and -/// a banner only has to mention `p/` somewhere for that to find a fingerprint -/// in text that is not one. -/// -/// That is not hypothetical, and the doc this replaces had the renderer's own -/// shape wrong: `banner_line_for` does NOT render a failed read as the bare -/// word `UNKNOWN`. It renders -/// `host: UNKNOWN -- topology discovery failed: {error}`, with the `io::Error` -/// verbatim. Measured: an error text of `16p/foo something opaque` made the -/// count reader answer `16`, which satisfied the guard, so the architecture -/// reader then answered `UNKNOWN` and the bound assertion PANICKED on a -/// perfectly valid unmeasured report -- a probe crashing on the host whose -/// discovery failed, which is the host it exists to report. -/// -/// The fingerprint renders ` p/c smt` behind any number of -/// `!!taint!!` markers, so the two tokens are taken from their positions and -/// the second is required to have the count SHAPE. `UNKNOWN -- ...` fails that -/// on its second token and is silent, which is the right answer: a failed read -/// names no architecture, so there is nothing to compare. -/// -/// Found together rather than separately because both readers need exactly the -/// same decision -- "is this a fingerprint, and where are its parts" -- and two -/// copies of that decision are what let them disagree about it before. -fn fingerprint_tokens(banner: &str) -> Option<(&str, &str)> { - let mut tokens = banner - .strip_prefix("host:")? - .split_whitespace() - .skip_while(|token| token.starts_with("!!") && token.ends_with("!!")); - - let architecture = tokens.next()?; - let counts = tokens.next()?; - - is_count_shaped(counts).then_some((architecture, counts)) -} - -/// Whether `token` is the `p/c` the fingerprint writes. -fn is_count_shaped(token: &str) -> bool { - let Some((processors, rest)) = token.split_once("p/") else { - return false; - }; - let Some(cores) = rest.strip_suffix('c') else { - return false; - }; - - [processors, cores] - .iter() - .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit())) -} - -/// The processor count a banner line names, as it was rendered. -/// -/// Returned as text rather than parsed, so a malformed count is reported as the -/// mismatch it is instead of being silently discarded by a failed parse. -fn processors_in_banner(banner: &str) -> Option<&str> { - let (_, counts) = fingerprint_tokens(banner)?; - - counts.split_once("p/").map(|(processors, _)| processors) -} - -/// [`check`], as an assertion, for tests that render a report. -/// -/// # Panics -/// -/// Panics listing every correspondence the report violated. -pub fn assert_corresponds(report: &str) { - let violations = check(report); - assert!( - violations.is_empty(), - "the report's parts contradict each other: {violations:#?}\n\n\ - --- the report ---\n{report}" - ); -} - -/// The report's machine-readable line, if it has one. -/// -/// A report is prose with at most one NDJSON line in it. `report_unmeasured` -/// emits a much shorter object than `report`, so every field read below is -/// optional by construction. -fn ndjson_line(report: &str) -> Option<&str> { - report.lines().find(|line| line.starts_with('{')) -} - -/// The raw text of one field of a flat JSON object. -/// -/// Hand-written rather than pulled from a JSON crate because this crate has no -/// such dependency and the object is emitted a few lines away in this same -/// crate: a top-level object of machine-generated fields, whose only nesting is -/// the `caches` array of objects and the `policies` object, both of which the -/// balanced scan below handles. An earlier version of this sentence called the -/// object "flat, unnested", which would lead a future change to assume nested -/// values are unsupported when they are read here every run. It returns the value's source -/// text -- quotes stripped for a string, otherwise verbatim -- so a caller -/// compares renderings rather than parsed values, which is the point. -fn ndjson_field<'a>(line: &'a str, key: &str) -> Option<&'a str> { - let needle = format!("\"{key}\":"); - let start = line.find(&needle)? + needle.len(); - let rest = &line[start..]; - - let value = if let Some(stripped) = rest.strip_prefix('"') { - let end = stripped.find('"')?; - &stripped[..end] - } else if rest.starts_with('[') || rest.starts_with('{') { - // Balanced, not first-closer. `caches` is an array OF objects and - // `policies` is an object, so stopping at the first `]` or `}` would - // truncate both -- returning `[{"level":1,"domains":8` for a three-level - // machine, which then compares unequal against anything and reports a - // contradiction that is the reader's own parse. - let end = balanced_end(rest)?; - &rest[1..end] - } else { - let end = rest.find([',', '}']).unwrap_or(rest.len()); - &rest[..end] - }; - - Some(value.trim()) -} - -/// One field of a flat JSON object, with its delimiters left on. -/// -/// [`ndjson_field`] strips the quotes from a string and the brackets from an -/// array or object, which is what most callers want -- they are comparing -/// contents. A caller that cares whether the value IS an array needs the -/// delimiter, because a scalar and a one-element list have the same contents. -fn ndjson_raw_field<'a>(line: &'a str, key: &str) -> Option<&'a str> { - let needle = format!("\"{key}\":"); - let start = line.find(&needle)? + needle.len(); - let rest = &line[start..]; - - let end = if rest.starts_with('[') || rest.starts_with('{') { - balanced_end(rest)? + 1 - } else if let Some(after_quote) = rest.strip_prefix('"') { - after_quote.find('"')? + 2 - } else { - rest.find([',', '}']).unwrap_or(rest.len()) - }; - - Some(rest[..end].trim()) + found } -/// The index of the bracket closing the one `text` opens with. -fn balanced_end(text: &str) -> Option { +/// Whether every bracket in `row` is closed, in order. +fn balanced(row: &str) -> bool { let mut depth = 0_i32; - for (index, character) in text.char_indices() { + let mut in_string = false; + let mut escaped = false; + + for character in row.chars() { + if escaped { + escaped = false; + continue; + } match character { - '[' | '{' => depth += 1, - ']' | '}' => { + '\\' if in_string => escaped = true, + '"' => in_string = !in_string, + '[' | '{' if !in_string => depth += 1, + ']' | '}' if !in_string => { depth -= 1; - if depth == 0 { - return Some(index); + if depth < 0 { + return false; } } _ => {} } } - None -} - -/// Whether some line of the report BEGINS with this renderer token. -/// -/// **Anchored, because a report carries text the renderer does not own.** -/// `report_unmeasured` embeds the caller's `io::Error`, so an unanchored -/// substring search reads that error as if it were the probe speaking. Measured, -/// before this: an error reading `BUG IN THIS PROBE => agree` made a perfectly -/// valid unmeasured report trip the alarm rule and panic in the renderer's own -/// binding -- the oracle inventing a contradiction out of a message it should -/// have treated as opaque. Found by a review. -/// -/// **The renderer now contains that text, and this anchoring is still what -/// stops it being read.** `renderer_owns_every_line` flattens the error, so it -/// can no longer introduce a LINE -- but its words still sit inside the -/// discovery-failure line, and an unanchored search finds them there just the -/// same. The two fixes answer different halves: containment stops caller text -/// impersonating a line, anchoring stops it being read as one. This paragraph -/// said "prints verbatim", which described the state before containment and -/// made the anchoring look redundant. -/// -/// Leading whitespace is trimmed rather than matched, because the renderer -/// indents some of these lines and not others, and the indentation is -/// presentation rather than contract. What matters is that the token begins a -/// line: every token passed here is one the renderer writes at the start of a -/// line it owns, and the caller's error is embedded mid-line after -/// `MachineMemoryTopology::discover failed: `. -/// -/// **The residue, stated rather than implied, and narrower than it was.** An -/// error containing a newline followed by one of these tokens would once have -/// put that token at the start of its own line, where anchoring cannot help. The -/// renderer closed that: `report_unmeasured` passes the error through -/// `renderer_owns_every_line`, so no error can create a line any more. Measured: -/// an error of `first\nBUG IN THIS PROBE: second` produces no line beginning -/// with the alarm, and the report is accepted. -/// -/// What remains is only for a caller that hands `check` text the renderer never -/// produced -- a test constructing a report by hand, say. Bounding even that -/// needs the renderer to tell the oracle which region is opaque, which is a -/// change to the report format rather than to this reader. -fn has_line_beginning(report: &str, token: &str) -> bool { - report - .lines() - .any(|line| line.trim_start().starts_with(token)) -} - -/// The claim's line and the indented continuation beneath it. -/// -/// A gated claim and the caveat that excuses it are one rendered block: the -/// renderer writes the claim, then the caveat as an indented continuation, then -/// a blank line. Ending at that blank line is what keeps another block's caveat -/// from answering for this one. -/// -/// **This also subsumes the caller-text problem, which is why the wider -/// `renderer_prose` filter it replaced is gone.** The caveat is matched -/// mid-line, so it is the one search here that cannot anchor to a line start -- -/// and that is the SUPPRESSING direction: a caveat found where none was written -/// removes a violation. Measured, before any of this: with the caveat sentence -/// appended to the banner, `UncaveatedClaimUnderDoubt` disappeared from a report -/// that still carried the claim and still said `parse_incomplete=2`. -/// -/// A block cannot start in the banner, because the claim line is found by its -/// own opening text and containment guarantees a caller's banner is one line -/// beginning `host:`. So scoping to the block excludes caller text for a -/// structural reason rather than by listing the places caller text can appear, -/// which is what the previous filter had to do -- and got wrong once, by -/// anchoring to a title that `clean_report()` does not use. -/// `a_caveat_in_the_banner_does_not_excuse_an_uncaveated_claim` still pins it. -fn claim_block<'a>(report: &'a str, claim: &str) -> impl Iterator { - report - .lines() - .skip_while(move |line| !line.trim_start().starts_with(claim)) - .take_while(|line| !line.trim().is_empty()) -} - -/// The text after `label` on the line that begins with it. -fn prose_field<'a>(report: &'a str, label: &str) -> Option<&'a str> { - report - .lines() - .find(|line| line.starts_with(label)) - .map(|line| line[label.len()..].trim()) -} - -/// Alarms the prose can raise. Each is a statement that part of the report is -/// not to be trusted. -const ALARMS: &[&str] = &["BUG IN THIS PROBE"]; - -fn check_alarm_against_verdict( - report: &str, - ndjson: Option<&str>, - found: &mut Vec, -) { - let Some(alarm) = report.lines().find(|line| { - ALARMS - .iter() - .any(|marker| line.trim_start().starts_with(marker)) - }) else { - return; - }; - - if has_line_beginning(report, "=> agree") { - found.push(Correspondence::AlarmWithAgreeingVerdict { - alarm: alarm.trim().to_owned(), - verdict_source: "prose", - }); - } - - if ndjson.and_then(|line| ndjson_field(line, "cross_check")) == Some("agree") { - found.push(Correspondence::AlarmWithAgreeingVerdict { - alarm: alarm.trim().to_owned(), - verdict_source: "ndjson", - }); - } -} - -/// Facts this report renders twice: the prose label, the NDJSON key, and the -/// name to use when they disagree. -/// -/// Counts only. A prose line reads `processors (online) : 16` and the NDJSON -/// `"processors":16`, so the comparison is of the rendered values with the -/// prose label removed. -const DOUBLE_RENDERED: &[(&str, &str, &str)] = &[ - ("processors (online) : ", "processors", "online processors"), - ("processor groups : ", "groups", "processor groups"), - ("packages : ", "packages", "packages"), - ("physical cores : ", "cores", "physical cores"), -]; - -fn check_prose_against_ndjson(report: &str, ndjson: Option<&str>, found: &mut Vec) { - let Some(ndjson) = ndjson else { - return; - }; - - for (label, key, fact) in DOUBLE_RENDERED { - let Some(prose) = prose_field(report, label) else { - continue; - }; - let Some(json) = ndjson_field(ndjson, key) else { - // The prose made the claim; the row is required to carry it. - found.push(Correspondence::RenderedOnlyInProse { - fact, - prose: prose.to_owned(), - }); - continue; - }; - if prose != json { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact, - prose: prose.to_owned(), - ndjson: json.to_owned(), - }); - } - } - // The efficiency classes, whose two renderings differ in punctuation and so - // cannot be compared as text. This pair is here because it was wrong: the - // NDJSON once emitted the class COUNT under a plural name, so a - // single-class host printed `"efficiency_classes":1` beside a prose - // `efficiency classes: [0]` -- the same fact, in one report, in two - // renderings a consumer cannot reconcile. - // Gated on the PROSE, so a row that lost the field is a dropped counterpart - // rather than silence -- the same rule `compare` applies, restated here only - // because this pair needs RAW values and so cannot route through it. - let classes_prose = prose_field(report, " efficiency classes: "); - let classes_json = ndjson_raw_field(ndjson, "efficiency_classes"); - - if let Some(prose) = classes_prose - && classes_json.is_none() - { - found.push(Correspondence::RenderedOnlyInProse { - fact: "efficiency classes", - prose: prose.to_owned(), - }); - } - - if let (Some(prose), Some(json)) = (classes_prose, classes_json) { - // **The container is part of the fact, and comparing only the contents - // threw it away.** `normalise_list` strips `[` and `]` from both sides, - // so a scalar `1` and a list `[1]` normalise to the same `"1"` -- which - // means the very regression this pair exists for, a class COUNT emitted - // under a plural name, survives undetected on any host whose single - // class is `1`. The old test caught the historical case only because it - // used class `[0]` against a count of `1`, so the VALUES differed; it - // established nothing about the shape. Found by a review. - // - // A host with a single class `1` is a supported shape, not a contrived - // one, so this is a live hole rather than a theoretical one. - // **Both sides, because the first version of this checked one.** It - // required the NDJSON to be a list and said nothing about the prose, so - // the mirror drift -- prose falling to `efficiency classes: 1` while the - // NDJSON still renders `[1]` -- was accepted: `normalise_list` strips - // the brackets from the JSON side and the two compare equal. Measured, - // before this: `check()` returned no violation for exactly that report. - // Found by a review, in the fix for the other direction. - if prose.starts_with('[') != json.starts_with('[') { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact: "efficiency classes", - prose: prose.to_owned(), - ndjson: json.to_owned(), - }); - } else { - let prose_classes = normalise_list(prose); - let json_classes = normalise_list(json); - if prose_classes != json_classes { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact: "efficiency classes", - prose: prose_classes, - ndjson: json_classes, - }); - } - } - } - - // The verdict, which the prose states as a sentence and the NDJSON as a - // token. - let prose_verdict = if has_line_beginning(report, "=> agree") { - Some("agree") - } else if has_line_beginning(report, "=> DISAGREE") { - Some("disagree") - } else if has_line_beginning(report, "=> INCOMPLETE") { - Some("incomplete") - } else { - None - }; - - // The verdict is the report's central claim, so a row that lost it is the - // worst case of the dropped-counterpart class rather than an exception to - // it: the prose still announces an answer and nothing machine-readable - // carries it. - if let Some(prose) = prose_verdict { - compare( - found, - "cross-check verdict", - prose, - ndjson_field(ndjson, "cross_check"), - ); - } + depth == 0 && !in_string } -/// The facts whose two renderings differ in shape rather than punctuation. +/// Every key `row` renders at its top level, in the order it renders them. /// -/// Found by the M2.4 matrix rather than by a defect. The four counts already -/// checked above were the ones a reviewer had happened to look at; walking every -/// NDJSON field against the prose showed these carrying the same fact twice as -/// well, with nothing comparing them. -fn check_structured_pairs(report: &str, ndjson: Option<&str>, found: &mut Vec) { - let Some(ndjson) = ndjson else { - return; - }; - - // `NUMA domains : 1 (0 with no processors)` against two fields. - if let Some(prose) = prose_field(report, "NUMA domains : ") { - let total = prose.split_whitespace().next().unwrap_or_default(); - let without = prose - .split_once('(') - .and_then(|(_, rest)| rest.split_whitespace().next()) - .unwrap_or_default(); - - compare( - found, - "NUMA domains", - total, - ndjson_field(ndjson, "numa_domains"), - ); - compare( - found, - "NUMA domains without processors", - without, - ndjson_field(ndjson, "numa_domains_without_processors"), - ); - } - - // ` (3 reported only by CPU Sets, never by the relationship walk:` against - // the NDJSON's count of the same thing. - // - // **Rendered CONDITIONALLY**, and that is why it needs its own lookup rather - // than joining the pair above: the renderer emits the prose line only when - // the count is above zero, so on most hosts there is no line to find and - // `prose_field` returns `None`. That is silence, not agreement -- the rule - // fires only where the report actually makes the claim twice. - // - // Found by a review, and it is the third double-rendered fact this module - // shipped without reading. The other two were `arch` and the - // `outermost_partitioning_cache` discriminator, now read by - // `check_partitioning_answer` with all five arms mapped. The lesson the - // three share: - // a rule is added per fact, so the set of facts is the thing that drifts, - // and nothing here derives that set from the renderer. - // **Selected by what the line SAYS, not by being the first ` (` line.** - // `prose_field` takes the first line with the label, and ` (` is not a - // label -- it is the opening of any parenthesised continuation. A report - // with heterogeneous efficiency classes writes - // ` (heterogeneous: ...` ABOVE this one, so on that shape the first match - // was the wrong line, the `contains` guard below rejected it, and the - // CPU-Sets count went unread with no sign that it had. - // - // The accounting test reports that as an unread fact rather than hiding it, - // which is how it was found -- but only on a report carrying both, and this - // host renders neither. - if let Some(prose) = report - .lines() - .find(|line| line.starts_with(" (") && line.contains("reported only by CPU Sets")) - .map(|line| line[" (".len()..].trim()) - { - compare( - found, - "NUMA domains reported only by CPU Sets", - prose.split_whitespace().next().unwrap_or_default(), - ndjson_field(ndjson, "numa_domains_only_in_cpu_sets"), - ); - } - - // `outermost cache that partitions the processors it covers: L2 (8 domains)` - // against the level the NDJSON names. This pair is the one the original - // defect lived next to: the prose can name a level the machine-readable - // line does not. - if let Some(prose) = prose_field( - report, - "outermost cache that partitions the processors it covers: ", - ) { - let level = prose - .trim_start_matches('L') - .split_whitespace() - .next() - .unwrap_or_default(); - compare( - found, - "outermost partitioning cache level", - level, - ndjson_field(ndjson, "outermost_partitioning_cache_level"), - ); - } - - // The policy table against the `policies` object. A policy's domain count is - // what the whole report is for, so two renderings of it disagreeing would - // mislead exactly the reader who came for the answer. - // - // **The NAMES are compared before the counts, because the name is a - // double-rendered fact and not merely a lookup aid.** Locating the NDJSON - // entry by the prose name and comparing only the value makes a failed lookup - // silent: `find()` returns `None`, `compare` returns early, and renaming - // `by-core` on one side alone -- or dropping the entry, or adding one the - // prose never mentions -- is accepted. Found by a review. - // **The prose section is what decides whether the pair is required, so the - // CONTAINER's absence is checked the same way a member's is.** Gating the - // whole block on the object existing made "the renderer dropped the entire - // `policies` object" silent while the prose table still stood beside it -- - // the member-level tests covered an entry going missing from the object, - // never the object going missing from the row. Measured: deleting it left a - // report the oracle accepted. - let policies_section = has_line_beginning(report, "domains each policy would produce:"); - let policies = ndjson_field(ndjson, "policies"); - - if policies_section && policies.is_none() { - found.push(Correspondence::RenderedOnlyInProse { - fact: "policy names", - prose: "domains each policy would produce:".to_owned(), - }); - } - - if let Some(policies) = policies { - // Gated on the prose SECTION, not on the rows parsing. The header is - // what establishes that the report renders this set twice; requiring a - // non-empty row list instead would make "the prose table lost all its - // rows" look like silence rather than the disagreement it is. - if policies_section { - compare_membership( - found, - "policy names", - &policy_rows(report) - .into_iter() - .map(|(name, _)| name) - .collect::>(), - &object_keys(policies), - ); - } - - for (name, count) in policy_rows(report) { - let key = format!("\"{name}\":"); - let json = policies - .find(&key) - .map(|at| &policies[at + key.len()..]) - .map(|rest| { - let end = rest.find(',').unwrap_or(rest.len()); - rest[..end].trim() - }); - - // **An entry the object does not carry is a MEMBERSHIP finding, and - // `compare_membership` above has already reported it by name.** - // Letting the per-entry rule report absence as well produced a - // second, weaker violation for the same defect -- one that says a - // count had no counterpart without saying which policy it belonged - // to. Each rule reports its own concern once. - if json.is_some() { - compare(found, "policy domain count", &count, json); - } - } - } - - // The cache table against the `caches` array, level by level -- and the set - // of LEVELS first, for the reason given above. A cache moved from level 3 to - // level 9 in one rendering only is the same silent-lookup defect: the prose - // still reads `L3`, nothing matches it, and the report is accepted. - // The same container rule as `policies`: the prose SECTION is what makes the - // pair required, so losing the whole array is a dropped counterpart and not - // silence. - let caches_section = has_line_beginning(report, "caches:"); - let caches_field = ndjson_field(ndjson, "caches"); - - if caches_section && caches_field.is_none() { - found.push(Correspondence::RenderedOnlyInProse { - fact: "cache levels", - prose: "caches:".to_owned(), - }); - } - - if let Some(caches) = caches_field { - if caches_section { - compare_membership( - found, - "cache levels", - &cache_rows(report) - .into_iter() - .map(|(level, _)| level) - .collect::>(), - &cache_levels(caches), - ); - } - - for (level, domains) in cache_rows(report) { - // **The object is located by its `level` member, then read for its - // `domains` member -- two independent steps, so member order and - // spelling are separate questions.** - // - // (History, because the shape of the bug is the reason for the - // shape of the code. This used to match one literal, - // `"level":N,"domains":`, which silently required the two members to - // be adjacent and in that order. Renaming, reordering or dropping - // `domains` alone made the lookup miss, and the prose domain count - // went uncompared while `cache_levels` still found every level and - // reported membership as agreeing. Measured then: both - // `{"level":1,"x-domains":8}` and `{"domains":8,"level":1}` were - // accepted, where `{"level":1,"domains":9}` was caught. The tests - // below now require the reordered object to stay readable and the - // renamed one to be reported.) - // - // A level the array does not carry at all is `compare_membership`'s - // finding above, reported by level number, so it is skipped here -- - // but an object that IS there and cannot answer is a dropped - // counterpart, which is `compare`'s business. - if let Some(object) = cache_object(caches, &level) { - compare( - found, - "cache domain count", - &domains, - ndjson_field(object, "domains"), - ); +/// Top level only, deliberately: a nested object's members are that object's +/// keys, and repeating one there is a different question from repeating one in +/// the row. +#[must_use] +pub fn keys(row: &str) -> Vec<&str> { + let mut names = Vec::new(); + let mut depth = 0_i32; + let mut rest = row; + + while let Some(quote) = rest.find('"') { + for character in rest[..quote].chars() { + match character { + '[' | '{' => depth += 1, + ']' | '}' => depth -= 1, + _ => {} } } - } -} -/// Push a disagreement when two renderings of one SET differ. -/// -/// Sorted before comparing, so a renderer free to emit its entries in a -/// different order from the prose is not accused of disagreeing about which -/// entries exist. Both sides get the same comparator, so the comparison stays -/// consistent whatever that order is. -fn compare_membership( - found: &mut Vec, - fact: &'static str, - prose: &[String], - ndjson: &[String], -) { - let mut prose_sorted = prose.to_vec(); - let mut ndjson_sorted = ndjson.to_vec(); - prose_sorted.sort(); - ndjson_sorted.sort(); - - if prose_sorted != ndjson_sorted { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact, - prose: prose_sorted.join(", "), - ndjson: ndjson_sorted.join(", "), - }); - } -} - -/// The keys of a flat JSON object, in the order it renders them. -/// -/// A key is a quoted string followed immediately by `:`. `policies` is flat -- -/// name to count -- so no nesting has to be tracked here, and a value that -/// happened to be a string could not be mistaken for a key because it is not -/// followed by a colon. -fn object_keys(object: &str) -> Vec { - let mut keys = Vec::new(); - let mut rest = object; - - while let Some(open) = rest.find('"') { - let after_open = &rest[open + 1..]; - let Some(close) = after_open.find('"') else { + let after = &rest[quote + 1..]; + let Some(end) = after.find('"') else { break; }; - let (key, tail) = after_open.split_at(close); - let tail = &tail[1..]; - if tail.starts_with(':') { - keys.push(key.to_owned()); - } - rest = tail; - } + let name = &after[..end]; + let tail = after[end + 1..].trim_start(); - keys -} - -/// The `level` of each entry of the `caches` array, as rendered. -fn cache_levels(caches: &str) -> Vec { - const NEEDLE: &str = "\"level\":"; - let mut levels = Vec::new(); - let mut rest = caches; - - while let Some(at) = rest.find(NEEDLE) { - let after = &rest[at + NEEDLE.len()..]; - let end = after.find([',', '}']).unwrap_or(after.len()); - levels.push(after[..end].trim().to_owned()); - rest = &after[end..]; - } + // A name followed by `:` at depth 1 is a key of the row itself. Anything + // else is a value, or a key of a nested object. + if tail.starts_with(':') && depth == 1 { + names.push(name); + } - levels -} -/// Push a disagreement when both renderings are present and differ. -/// One prose reading against its machine-readable counterpart. -/// -/// **Absence is reported HERE, so every rule inherits it.** Each caller reaches -/// this only after finding the prose, so a `None` counterpart is not "the report -/// does not mention this fact" -- it is "the report states this fact once and -/// the row lost it". Returning early on `None` made that silent for every -/// comparison routed through this helper at once. -/// -/// Fixing it at the three reported call sites first, rather than here, is what -/// left the rest: a later review named five more, and a deletion sweep of the -/// real report then found twelve keys whose removal the oracle accepted. The -/// helper is the only place the rule cannot be forgotten for the next fact -/// somebody adds. -fn compare(found: &mut Vec, fact: &'static str, prose: &str, json: Option<&str>) { - let Some(json) = json else { - found.push(Correspondence::RenderedOnlyInProse { - fact, - prose: prose.to_owned(), - }); - return; - }; - if prose != json { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact, - prose: prose.to_owned(), - ndjson: json.to_owned(), - }); + rest = &after[end + 1..]; } -} -/// `(policy name, domain count)` for each row of the policy table. -fn policy_rows(report: &str) -> Vec<(String, String)> { - report - .lines() - .skip_while(|line| !line.starts_with("domains each policy would produce:")) - .skip(1) - .take_while(|line| line.starts_with(" ")) - .filter_map(|line| { - let mut parts = line.split_whitespace(); - Some((parts.next()?.to_owned(), parts.next()?.to_owned())) - }) - .collect() + names } -/// The object in the `caches` array that names `level`, if one does. +/// The report's machine-readable row, if it carries exactly one well-formed one. /// -/// Compares whole members rather than searching for a prefix, so `"level":1` -/// does not match the object for level 10, and does not care what order the -/// members are written in. -fn cache_object<'a>(caches: &'a str, level: &str) -> Option<&'a str> { - let named = format!("\"level\":{level}"); - - caches.split('{').find_map(|chunk| { - let object = chunk.split('}').next()?; - - object - .split(',') - .any(|member| member.trim() == named) - .then_some(object) - }) -} - -/// `(level, domain count)` for each row of the cache table. -fn cache_rows(report: &str) -> Vec<(String, String)> { - report - .lines() - .skip_while(|line| !line.starts_with("caches:")) - .skip(1) - .take_while(|line| line.starts_with(" ")) - .filter_map(|line| { - let mut parts = line.split_whitespace(); - let level = parts.next()?.strip_prefix('L')?.to_owned(); - Some((level, parts.next()?.to_owned())) - }) - .collect() +/// Public because the instruments in `tests/` read the row to ask what it +/// publishes, and a second implementation of "which line is the row" is the kind +/// of copy that agrees until it does not. +#[must_use] +pub fn row(report: &str) -> Option<&str> { + let mut rows = report.lines().filter(|line| line.starts_with('{')); + let row = rows.next()?; + (rows.next().is_none() && balanced(row)).then_some(row) } -/// The independently-read Win32 counters against the verdict drawn from them. +/// [`check`], as an assertion, for tests that render a report. /// -/// A different shape from the rules above, and the one closest to what this -/// probe is *for*. The prose prints each counter beside the enumerated value it -/// was read to check; the whole point of the run is that a mismatch is a -/// finding. So a counter that disagrees with the enumeration while the verdict -/// reads `agree` is the original defect in its purest form -- the report -/// showing its own contradicting evidence directly above a verdict denying it. -fn check_counters_against_verdict( - report: &str, - ndjson: Option<&str>, - found: &mut Vec, -) { - let Some(ndjson) = ndjson else { - return; - }; - if ndjson_field(ndjson, "cross_check") != Some("agree") { - return; - } - - // Only the two counters that are a direct count of an enumerated quantity. - // `GetNumaHighestNodeNumber` is deliberately absent: it reports the largest - // node NUMBER, which the report itself says is not a count, so comparing it - // against `numa_domains` would manufacture a disagreement on any machine - // with sparse node numbering. - for (label, key, fact) in [ - ( - " GetActiveProcessorCount : ", - "processors", - "active processor count against the enumeration", - ), - ( - " GetActiveProcessorGroupCount: ", - "groups", - "active group count against the enumeration", - ), - ] { - // **Reached only under an agreeing verdict**, which is what makes the - // absence of either side a contradiction rather than silence: `agree` - // asserts the check was MADE, and the counter line is the evidence it - // was. Skipping quietly accepted a report that claimed a check it did - // not show -- measured, by deleting the `GetActiveProcessorCount` line - // and watching `check` return nothing. - let (Some(counter), Some(enumerated)) = - (prose_field(report, label), ndjson_field(ndjson, key)) - else { - found.push(Correspondence::EvidenceMissingWithAgreeingVerdict { fact }); - continue; - }; - if counter != enumerated { - found.push(Correspondence::ProseAndNdjsonDisagree { - fact, - prose: counter.to_owned(), - ndjson: enumerated.to_owned(), - }); - } - } -} - -/// A list of numbers as a comparable string, whichever way it was punctuated. +/// # Panics /// -/// **Exactly one outer pair of brackets comes off, so nesting survives the -/// normalisation.** `trim_matches` removes every consecutive bracket, which -/// collapsed `[0]` and `[[0]]` to the same `0` -- so a renderer that regressed -/// to a nested array beside one-level prose would have compared equal. The -/// punctuation this is meant to forgive is one side writing `[0, 1]` where the -/// other writes `0,1`; a difference in DEPTH is a real disagreement and must -/// survive to be reported. -fn normalise_list(rendered: &str) -> String { - rendered - .strip_prefix('[') - .and_then(|inner| inner.strip_suffix(']')) - .unwrap_or(rendered) - .split(',') - .map(str::trim) - .filter(|piece| !piece.is_empty()) - .collect::>() - .join(",") -} - -/// Hardware claims the prose can make, each with the caveat that must accompany -/// it when the parse is in doubt. -const GATED_CLAIMS: &[(&str, &str, &str)] = &[( - "(heterogeneous: an I/O thread left unconstrained can land on an", - "This run did not establish that the parse is whole", - "heterogeneity", -)]; - -fn check_claims_against_doubt(report: &str, ndjson: Option<&str>, found: &mut Vec) { - let Some(ndjson) = ndjson else { - return; - }; - - // The report's own visible evidence that its parse was in doubt. - // - // These two are exactly what `CrossCheck::parse_in_doubt` is defined as -- - // a non-empty `parse_incomplete` or a non-empty `disagreements`, the latter - // being what makes the verdict `disagree`. That correspondence is the point - // rather than a coincidence: if the definition changes and this does not, - // the sabotage check in M2.2 is what should notice. - let mut evidence = Vec::new(); - if let Some(entries) = ndjson_list_len(ndjson, "parse_incomplete") - && entries > 0 - { - // Names the conditions rather than counting them, so the violation - // message says WHY the parse was in doubt. - evidence.push(format!( - "parse_incomplete={}", - ndjson_raw_field(ndjson, "parse_incomplete").unwrap_or_default() - )); - } - if ndjson_field(ndjson, "cross_check") == Some("disagree") { - evidence.push("cross_check=disagree".to_owned()); - } - - if evidence.is_empty() { - return; - } - - for (claim, caveat, name) in GATED_CLAIMS { - // The CLAIM is anchored and the CAVEAT is not, deliberately. A claim - // found where none was made invents a violation, which is the direction - // that must not be fooled by embedded text; a caveat found where none - // was made only SUPPRESSES one. And the caveat token is written to match - // mid-line on purpose -- the renderer prints it after `(of the levels - // that decoded. ` in one arm and after `(` in another -- so anchoring it - // would stop it matching the lines it exists for. - // **Scoped to the claim's OWN block, not the whole report.** A review - // predicted that another block's caveat could satisfy this one: the - // `Level` cache arm under `parse_in_doubt` writes the same sentence, and - // a report can be heterogeneous and in doubt and take that arm at once. - // - // Measured on exactly that crossed shape: it does NOT mask, because the - // renderer WRAPS the cache arm's sentence -- `... did not establish that - // the parse` ends one line and `is whole ...` begins the next -- so no - // single line carries the token. The finding was wrong about today's - // renderer and right about the code: that protection is an accident of - // where a line happens to break, and reflowing that sentence would - // silently turn the oracle blind to an uncaveated hardware claim. - // - // The block is the claim's line and the indented continuation under it, - // which is what the renderer actually emits and what the caveat belongs - // to. An accident that holds is still an accident. - if has_line_beginning(report, claim) - && !claim_block(report, claim).any(|line| line.contains(caveat)) - { - found.push(Correspondence::UncaveatedClaimUnderDoubt { - claim: name, - evidence: evidence.join(", "), - }); - } - } +/// Panics listing every way the row is malformed. +pub fn assert_corresponds(report: &str) { + let defects = check(report); + assert!( + defects.is_empty(), + "a rendered report's machine-readable row is malformed in {} way(s):\n{}\n\n\ + --- the report ---\n{report}", + defects.len(), + defects + .iter() + .map(|defect| format!(" - {defect}")) + .collect::>() + .join("\n"), + ); } #[cfg(test)] diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index 3340ba744..4f0561ad0 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -1,2520 +1,167 @@ // Copyright (c) Mike Grier. -//! Tests for the report oracle. -//! -//! Half of these assert the oracle **accepts** something. That is deliberate, -//! and follows `windows-file-watcher`'s `ContractChecker`: an oracle that -//! rejects legal reports is as broken as one that passes illegal ones, and it -//! fails in the more expensive direction, because the noise trains a reader to -//! ignore it. -//! -//! The reports here are hand-written text rather than rendered from an -//! `Observation`. That is the point: the oracle reads artifacts, so it must be -//! testable with artifacts, including ones no renderer would currently produce. - -use super::{Correspondence, check}; - -/// A fingerprint-shaped banner naming the architecture this build actually -/// targets. -/// -/// **Not a literal, because the renderer publishes `std::env::consts::ARCH`.** -/// A banner hard-coding `x86_64` agrees with the body on an x86_64 host and -/// contradicts it everywhere else, so every test that feeds a banner to the -/// RENDERER failed on `i686-pc-windows-msvc` -- five of them, each reporting -/// `prose: "x86_64"` against `ndjson: "x86"`. CI builds `aarch64` but does not -/// run the suite there, so the fleet never saw it either. -/// -/// Fixtures built entirely by hand are unaffected: their banner and their NDJSON -/// both say `x86_64`, so they agree with each other whatever the host is. Only -/// the ones that mix a literal banner with a rendered body were wrong. -fn host_banner(rest: &str) -> String { - format!("host: {} {rest}", std::env::consts::ARCH) -} - -/// A fingerprint this build can always produce, naming the build's architecture. -/// -/// Tests that need a successful bracket reading construct one rather than -/// calling `Fingerprint::discover()`, so they do not depend on the host being -/// able to read its own topology -- which is a failure this crate exists to -/// report, not one a test should be defeated by. -fn built_fingerprint() -> windows_placement_probe::fingerprint::Fingerprint { - windows_placement_probe::fingerprint::Fingerprint { - arch: std::env::consts::ARCH, - processors: 16, - cores: 8, - smt: true, - partitioning_cache_level: Some(2), - cache_domain_sizes: vec![8, 8], - efficiency_classes: vec![(0, 16)], - numa_node_sizes: vec![16], - provenance: windows_topology_sys::Provenance::Measured, - } -} - -/// A report body with the shape the topology probe emits, for a host that is -/// unremarkable and agrees with itself. -fn clean_report() -> String { - [ - "host: x86_64 16p/8c", - "== what does this machine look like? ==", - "", - "processors (online) : 16", - "processor groups : 1", - "packages : 1", - "NUMA domains : 1 (0 with no processors)", - "physical cores : 8", - " cores with SMT : 8", - " efficiency classes: [0]", - "", - "caches:", - " L1 8 domain(s), processors per domain: [2, 2, 2, 2, 2, 2, 2, 2]", - " L3 1 domain(s), processors per domain: [16]", - "", - "outermost cache that partitions the processors it covers: L1 (8 domains)", - "", - "domains each policy would produce:", - " single 1", - " by-core 8", - "", - "cross-check against independently read Win32 counters:", - " GetActiveProcessorCount : 16", - " GetActiveProcessorGroupCount: 1", - " GetNumaHighestNodeNumber : 0", - " => agree. Every check this probe could make was made and matched.", - r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16,"groups":1,"packages":1,"numa_domains":1,"numa_domains_without_processors":0,"cores":8,"efficiency_classes":[0],"caches":[{"level":1,"domains":8},{"level":3,"domains":1}],"outermost_partitioning_cache_level":1,"outermost_partitioning_cache":"level","policies":{"single":1,"by-core":8},"cross_check":"agree","parse_incomplete":[]}"#, - ] - .join("\n") -} - -// --- must reject ------------------------------------------------------------ - -#[test] -fn an_alarm_beside_an_agreeing_prose_verdict_is_a_violation() { - // The original defect, reduced: `BUG IN THIS PROBE` and `=> agree` in one - // report. Both statements were locally true and they cannot both describe - // the same run. - let report = clean_report().replace( - "cross-check against independently read Win32 counters:", - "BUG IN THIS PROBE: the topology crate named L3 as the outermost\ncross-check against independently read Win32 counters:", - ); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::AlarmWithAgreeingVerdict { - verdict_source: "prose", - .. - } - )), - "an alarm printed beside `=> agree` must be reported, got {violations:#?}" - ); -} - -#[test] -fn an_alarm_beside_an_agreeing_ndjson_verdict_is_a_violation() { - // The same contradiction reaching a mining pass instead of a reader. It is - // reported separately because the two consumers are separate: a fleet - // survey never sees the prose. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed.", - ) - .replace( - "cross-check against independently read Win32 counters:", - "BUG IN THIS PROBE: something\ncross-check against independently read Win32 counters:", - ); - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::AlarmWithAgreeingVerdict { - verdict_source: "ndjson", - .. - } - )), - "an alarm beside `\"cross_check\":\"agree\"` must be reported, got {violations:#?}" - ); -} - -#[test] -fn a_count_the_prose_and_the_ndjson_disagree_about_is_a_violation() { - let report = clean_report().replace("processors (online) : 16", "processors (online) : 8"); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "online processors", - .. - } - )), - "two renderings of the processor count must agree, got {violations:#?}" - ); -} - -#[test] -fn a_class_count_rendered_where_the_class_list_belongs_is_a_violation() { - // The historical defect this pair exists for: the NDJSON emitted the class - // COUNT under a plural name, so a single-class host printed - // `"efficiency_classes":1` beside a prose `efficiency classes: [0]`. Same - // fact, same report, and the two readings differ -- one says "one class", - // the other says "class one". - let report = clean_report().replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":1"#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "efficiency classes", - .. - } - )), - "a class count where the class list belongs must be reported, got {violations:#?}" - ); -} - -#[test] -fn a_verdict_the_prose_and_the_ndjson_disagree_about_is_a_violation() { - let report = clean_report().replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "cross-check verdict", - .. - } - )), - "the verdict must read the same in both renderings, got {violations:#?}" - ); -} - -#[test] -fn a_bare_hardware_claim_under_an_incomplete_parse_is_a_violation() { - let report = clean_report() - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", - ) - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#) - .replace(r#""parse_incomplete":[]"#, r#""parse_incomplete":["no_cores","no_packages"]"#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::UncaveatedClaimUnderDoubt { - claim: "heterogeneity", - .. - } - )), - "a hardware claim stated bare under a short parse must be reported, got {violations:#?}" - ); -} - -#[test] -fn a_bare_hardware_claim_under_a_disagreeing_cross_check_is_a_violation() { - // The other half of `parse_in_doubt`. A disagreement is doubt about the - // parse just as much as an incomplete one, and the renderer's gate covers - // both -- so an oracle that only knew about `parse_incomplete` would pass - // exactly half the cases the rule is written for. - let report = clean_report() - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", - ) - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#) - .replace( - " => agree. Every check this probe could make was made and matched.", - " => DISAGREE. This is a finding, not a nuisance:", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::UncaveatedClaimUnderDoubt { - claim: "heterogeneity", - .. - } - )), - "a hardware claim stated bare under a disagreement must be reported, got {violations:#?}" - ); -} - -#[test] -fn every_double_rendered_fact_is_actually_read() { - // The failure mode that would make this whole oracle worthless, and would - // look exactly like success: a prose label that does not match what the - // renderer emits makes the lookup return `None`, the comparison is skipped, - // and the report passes having been checked for nothing. - // - // So each pair is exercised individually rather than trusted. Corrupting - // one prose value must produce one violation naming that fact; if a label - // ever drifts from the renderer, the corresponding case here stops firing - // and this test fails rather than the oracle going quietly blind. - // - // The labels themselves were confirmed against a real `probe-topology` run, - // which is what makes the fixture above a fixture and not a guess. - for (label, wrong) in [ - ("processors (online) : 16", "processors (online) : 99"), - ("processor groups : 1", "processor groups : 99"), - ("packages : 1", "packages : 99"), - ("physical cores : 8", "physical cores : 99"), - (" efficiency classes: [0]", " efficiency classes: [9]"), - ] { - let report = clean_report().replace(label, wrong); - let violations = check(&report); - - assert!( - violations - .iter() - .any(|v| matches!(v, Correspondence::ProseAndNdjsonDisagree { .. })), - "corrupting `{label}` produced no violation, so the oracle is not \ - reading that line at all -- got {violations:#?}" - ); - } -} - -// --- the cells the M2.4 matrix walk added ------------------------------------ - -#[test] -fn a_counter_that_contradicts_the_enumeration_under_an_agreeing_verdict_is_a_violation() { - // The rule closest to what this probe is *for*, and the original defect in - // its purest form: the report printing its own contradicting evidence - // directly above a verdict denying it. The whole run exists to compare an - // independently read counter against the enumeration, so a mismatch is the - // finding -- and `agree` says there was none. - let report = clean_report().replace( - " GetActiveProcessorCount : 16", - " GetActiveProcessorCount : 8", - ); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "active processor count against the enumeration", - .. - } - )), - "a counter disagreeing with the enumeration under `agree` must be \ - reported, got {violations:#?}" - ); -} -#[test] -fn a_contradicting_counter_is_accepted_when_the_verdict_reports_it() { - // The legal shape, and the one the probe exists to produce. Rejecting it - // would fire on every host that actually has the disagreement this probe - // hunts for -- the run most worth reading. - let report = clean_report() - .replace( - " GetActiveProcessorCount : 16", - " GetActiveProcessorCount : 8", - ) - .replace( - " => agree. Every check this probe could make was made and matched.", - " => DISAGREE. This is a finding, not a nuisance:", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#); - - assert_eq!(check(&report), Vec::new()); -} - -#[test] -fn the_highest_numa_node_number_is_not_compared_against_the_domain_count() { - // Deliberately absent from the counter rule, and pinned so it stays absent. - // `GetNumaHighestNodeNumber` reports the largest node NUMBER, which the - // report itself says is not a count; comparing it against `numa_domains` - // would manufacture a disagreement on any machine with sparse node - // numbering. Over-constraining is the same defect as under-specifying. - let report = clean_report().replace( - " GetNumaHighestNodeNumber : 0", - " GetNumaHighestNodeNumber : 7", - ); - - assert_eq!(check(&report), Vec::new()); -} - -#[test] -fn a_policy_count_the_two_renderings_disagree_about_is_a_violation() { - // The domain count per policy is the answer the whole report exists to - // give, so two renderings of it disagreeing misleads exactly the reader who - // came for it. - let report = clean_report().replace(r#""by-core":8"#, r#""by-core":4"#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "policy domain count", - .. - } - )), - "the policy table and the policies object must agree, got {violations:#?}" - ); -} - -#[test] -fn a_cache_domain_count_the_two_renderings_disagree_about_is_a_violation() { - let report = clean_report().replace(r#"{"level":3,"domains":1}"#, r#"{"level":3,"domains":9}"#); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "cache domain count", - .. - } - )), - "the cache table and the caches array must agree, got {violations:#?}" - ); -} - -#[test] -fn an_outermost_level_the_two_renderings_disagree_about_is_a_violation() { - let report = clean_report().replace( - r#""outermost_partitioning_cache_level":1"#, - r#""outermost_partitioning_cache_level":3"#, - ); - - let violations = check(&report); - - assert!( - violations.iter().any(|v| matches!( - v, - Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning cache level", - .. - } - )), - "the named outermost level must match the machine-readable one, got {violations:#?}" - ); -} - -#[test] -fn a_nested_container_is_read_whole_rather_than_to_its_first_closer() { - // `caches` is an array OF objects, so a reader stopping at the first `}` - // would see only its first entry -- and would then silently skip every - // later level rather than compare it. This corrupts the LAST cache entry, - // which only a balanced read can reach. - let report = clean_report().replace(r#"{"level":3,"domains":1}"#, r#"{"level":3,"domains":5}"#); - - assert!( - !check(&report).is_empty(), - "a disagreement in the last element of a nested container must still be \ - found, or the container is being truncated at its first closer" - ); -} - -// --- must accept ------------------------------------------------------------ - -#[test] -fn a_report_that_agrees_with_itself_is_accepted() { - assert_eq!(check(&clean_report()), Vec::new()); -} - -#[test] -fn an_alarm_with_a_verdict_that_is_not_agree_is_accepted() { - // The legal shape of an alarm, and the one the fix produced. Rejecting it - // would make the oracle unusable on exactly the reports it was written to - // protect. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - "cross-check against independently read Win32 counters:", - "BUG IN THIS PROBE: something\ncross-check against independently read Win32 counters:", - ); - assert_eq!(check(&report), Vec::new()); -} - -#[test] -fn a_hardware_claim_with_its_caveat_under_doubt_is_accepted() { - // Doubt plus a claim is legal when the claim is caveated. This is the - // shape the renderer actually produces, so an oracle that rejected it - // would fire on every heterogeneous host with a short parse. - // - // **The verdict moves with the doubt, and an earlier version of this - // fixture forgot that.** It set `"parse_incomplete":2` while leaving the - // verdict at `agree`, which the renderer cannot emit: it publishes the rule - // that a non-empty `parse_incomplete` forces the verdict away from `agree`. - // So the comment above claimed "the shape the renderer actually produces" - // about a shape it cannot produce -- caught when the rule that reads those - // counts was added and rejected this fixture. Both renderings of the verdict - // move together here, because the report renders it twice. - let report = clean_report() - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an\n (This run did not establish that the parse is whole, and the classes", - ) - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - a cache record failed to decode\n\ - \x20 - a second cache record failed to decode", - ) - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":["no_cores","no_packages"],"not_compared":[],"enumeration_anomalies":[]}"#, - ); - - assert_eq!(check(&report), Vec::new()); -} - -#[test] -fn a_bare_hardware_claim_with_no_doubt_reported_is_accepted() { - // The common case on a healthy heterogeneous host: the claim is bare - // because there is nothing to caveat. - let report = clean_report() - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", - ) - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[0,1]"#); - - assert_eq!(check(&report), Vec::new()); -} +//! Tests for the row's well-formedness check. +//! +//! Half of these assert ACCEPTANCE. A check that fires on a legal row costs a +//! reader more than one that misses an illegal one, because noise trains them to +//! ignore the instrument -- and the reports this runs against are the ones a +//! fleet survey mines, so a false alarm is a false finding about a host. -#[test] -fn a_report_with_no_ndjson_line_is_accepted() { - // `report_unmeasured` and any future prose-only report. Every NDJSON read - // is optional by construction, so a missing line is silence rather than a - // violation. - let prose = clean_report() - .lines() - .filter(|line| !line.starts_with('{')) - .collect::>() - .join("\n"); +use super::{RowDefect, check, keys, row}; - assert_eq!(check(&prose), Vec::new()); +/// A well-formed row, in the shape the renderer emits. +fn clean_row() -> String { + concat!( + r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16,"#, + r#""efficiency_classes":[0],"caches":[{"level":1,"domains":8}],"#, + r#""policies":{"single":1},"cross_check":"agree","disagreements":[],"#, + r#""parse_incomplete":[]}"# + ) + .to_owned() } -#[test] -fn a_shorter_ndjson_object_is_accepted() { - // `report_unmeasured` emits only `reason`, `arch` and `cross_check`. Fields - // the oracle knows about but the report does not carry are absent, not - // wrong. - let report = [ - "host: x86_64 16p/8c", - "the topology could not be read: something went wrong", - r#"{"reason":"x-probe-topology","arch":"x86_64","cross_check":"not_measured"}"#, - ] - .join("\n"); - - assert_eq!(check(&report), Vec::new()); +/// A report carrying `row` under a line of prose. +fn report_with(row: &str) -> String { + format!("host: x86_64 16p/8c\nsome prose the reader gets\n{row}") } #[test] -fn the_two_class_list_punctuations_are_read_as_the_same_list() { - // The prose renders `[0, 1]` through `Debug` and the NDJSON `[0,1]` through - // a join. They are the same fact, and an oracle that compared them as text - // would report every multi-class host as a contradiction. - let report = clean_report() - .replace(" efficiency classes: [0]", " efficiency classes: [0, 1]") - .replace( - r#""efficiency_classes":[0]"#, - r#""efficiency_classes":[0,1]"#, - ); - - assert_eq!(check(&report), Vec::new()); +fn a_well_formed_row_is_accepted() { + assert_eq!(check(&report_with(&clean_row())), Vec::new()); } #[test] -fn a_banner_naming_a_different_machine_from_the_body_is_a_violation() { - // The M2.5 defect. Both halves are locally correct -- the banner faithfully - // renders one topology and the body another -- and the report reconciles - // them nowhere, so a reader deciding whether two runs are comparable is - // reading a line about a machine the numbers did not come from. - let report = clean_report().replace("host: x86_64 16p/8c", "host: x86_64 8p/8c"); - assert!( - !report.contains("16p/8c"), - "the sabotage must actually have landed, or this test proves nothing" - ); - +fn a_report_with_no_row_is_a_defect() { + // Every report has one, including the unmeasured shape -- that is what lets + // a survey tell a host where discovery failed from a job that never ran the + // probe. assert_eq!( - check(&report), - vec![Correspondence::BannerDisagreesWithBody { - banner: "8".to_owned(), - body: "16".to_owned(), - }] + check("host: x86_64 16p/8c\nprose only, no row"), + vec![RowDefect::Missing] ); } #[test] -fn a_banner_for_a_host_that_could_not_be_read_is_accepted() { - // `Fingerprint::discover` failing renders `UNKNOWN` with no count in it. - // There is nothing to relate, and reporting a contradiction would turn a - // gap in the measurement into a claim about the report -- the inversion - // this whole crate is built to avoid. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: UNKNOWN -- topology discovery failed: access denied", - ); +fn a_report_with_two_rows_is_a_defect() { + // A mining pass reads the first line that looks like a row, so a second is + // not extra data -- it is an ambiguity about which line is the contract. + let two = format!("{}\n{}", report_with(&clean_row()), clean_row()); - assert_eq!(check(&report), Vec::new()); + assert_eq!(check(&two), vec![RowDefect::Duplicated { count: 2 }]); } #[test] -fn a_tainted_banner_is_still_read_for_its_count() { - // An unmeasured topology renders behind a `!!...!! ` prefix. The taint says - // the numbers are not to be trusted as hardware -- it does not excuse the - // banner from naming the same numbers the body does, and a reader - // reconciling the two is exactly who the marker is for. - let report = clean_report().replace("host: x86_64 16p/8c", "host: !!assumed!! x86_64 8p/8c"); +fn an_unbalanced_row_is_a_defect() { + let truncated = r#"{"reason":"x-probe-topology","caches":[{"level":1}"#; assert_eq!( - check(&report), - vec![Correspondence::BannerDisagreesWithBody { - banner: "8".to_owned(), - body: "16".to_owned(), + check(&report_with(truncated)), + vec![RowDefect::Unbalanced { + row: truncated.to_owned() }] ); } #[test] -fn the_second_banner_line_of_a_disagreeing_bracket_is_not_compared() { - // When the endpoint readings differ, `attribution` prints the other reading - // too and says plainly that which one names the machine was not - // established. The body deliberately does not describe that second reading, - // so comparing it here would report a contradiction as a defect when it is - // the renderer being honest -- an oracle that over-constrains fails in the - // more expensive direction. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: x86_64 16p/8c\nhost: x86_64 8p/8c\nHOST READINGS DISAGREE: the two readings above \ - bracket the measurement\nand differ, so which of them names the machine the body below \ - describes\nwas not established.", - ); - - assert_eq!(check(&report), Vec::new()); -} - -#[test] -fn an_indeterminate_attribution_is_not_a_banner_violation() { - // **The report says the correspondence was not established, so the oracle - // must not assert it.** `attribution` on this branch takes the two bracket - // readings and renders, when they differ: - // - // HOST READINGS DISAGREE: ... which of them names the machine the body - // below describes was not established. - // - // The body comes from a `measure()` between them, so the FIRST banner line - // is one endpoint and need not describe the body. Reporting a contradiction - // there would be the oracle over-claiming exactly as the renderer went to - // trouble not to -- and over-constraining is the failure this file's other - // half exists to catch. - // - // This is the seam the origin branch closed with `measure_observed`, which - // builds the banner from the body's own topology. That construction change - // is not in this peel, so the ambiguity is real here and the oracle has to - // respect it. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: x86_64 8p/4c\n\ - host: x86_64 16p/8c\n\ - HOST READINGS DISAGREE: the two readings above bracket the measurement\n\ - and differ, so which of them names the machine the body below describes\n\ - was not established.", - ); - - assert_eq!( - check(&report), - Vec::new(), - "the first banner reading names 8 processors and the body 16, which the \ - report itself declines to call a contradiction" - ); -} - -#[test] -fn an_unestablished_host_is_not_a_banner_violation() { - // The other indeterminate form: at least one bracket reading failed, so - // nothing confirmed the host held still. Same reasoning, different text, - // and it is a separate arm of `attribution`'s match -- checking only the - // disagree case would leave this one asserting a correspondence the report - // does not claim. - // - // **The FAILED reading is the second one, and that ordering is the test.** - // An earlier version put `UNKNOWN` first, which is equally legal -- the - // `_` arm of `attribution` covers (Err, Ok), (Ok, Err) and (Err, Err) -- - // but it made this test vacuous: `processors_in_banner("host: UNKNOWN")` - // finds no count, so the rule returned at the count guard and never - // reached the disclaimer. Measured: deleting the `HOST NOT ESTABLISHED` - // arm of the exemption left that version, and the whole suite, green. - // Putting the successful reading first gives the rule a count to compare, - // so the exemption is the only thing that can suppress the violation. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: x86_64 8p/4c\n\ - host: UNKNOWN\n\ - HOST NOT ESTABLISHED: at least one of the two readings that bracket the measurement\n\ - failed, so nothing confirmed the machine held still under it.", - ); - - assert_eq!( - check(&report), - Vec::new(), - "a failed bracket reading is not evidence the banner contradicts the body" - ); -} - -#[test] -fn the_unestablished_host_fixture_would_be_a_violation_without_the_disclaimer() { - // The control that keeps the test above honest. Same report, same 8-versus-16 - // contradiction, with only the disclaimer removed -- and now it MUST be a - // violation. Without this, a future edit that stops the rule reaching the - // comparison at all would leave the acceptance test passing for the wrong - // reason, which is precisely how the previous version went vacuous. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: x86_64 8p/4c\n\ - host: UNKNOWN", - ); - - assert_eq!( - check(&report), - vec![Correspondence::BannerDisagreesWithBody { - banner: "8".to_owned(), - body: "16".to_owned(), - }], - "with no disclaimer the banner's 8 processors contradict the body's 16, so \ - the acceptance test above is established by the exemption rather than by \ - the comparison being unreachable" - ); -} -#[test] -fn an_architecture_the_banner_and_the_ndjson_disagree_about_is_a_violation() { - // Found by a review corrupting the NDJSON `arch` and watching the oracle - // accept it. Both renderings come from `std::env::consts::ARCH` today, so - // they cannot currently differ -- which is a fact about the renderer rather - // than a contract, and exactly the kind of coincidence this oracle is built - // not to lean on. - let report = clean_report().replace(r#""arch":"x86_64""#, r#""arch":"aarch64""#); - - assert!( - check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "architecture", - .. - } - )), - "the banner names x86_64 and the body aarch64: {:#?}", - check(&report) - ); -} - -#[test] -fn a_tainted_banner_is_still_read_for_its_architecture() { - // The taint prefix is a rendering of doubt about the READING, not a - // different machine, so the architecture behind it is still the one the - // banner claims. Skipping it here would quietly drop the correspondence on - // exactly the reports where a reader most wants it checked. - let report = clean_report() - .replace( - "host: x86_64 16p/8c", - "host: !!assumed!! !!taint!! x86_64 16p/8c", - ) - .replace(r#""arch":"x86_64""#, r#""arch":"aarch64""#); - - assert!( - check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "architecture", - .. - } - )), - "a tainted banner still names an architecture: {:#?}", - check(&report) - ); -} - -#[test] -fn an_architecture_disagreement_is_read_on_an_unmeasured_report_too() { - // The architecture rule was written below the processor-count guard, which - // confined it to MEASURED reports without saying so: `report_unmeasured` - // renders `arch` and no `processors`, so the guard returned first and the - // architecture went uncompared on exactly the reports that carry least - // else. Found by a review; measured before the fix as `check()` returning - // no violation at all for the report below. - // - // This is the shape `report_unmeasured` emits when the bracket reading - // succeeded and the measurement did not, so the banner names a real - // architecture while the body is the short object. - let report = "host: x86_64 16p/8c\n\ - MachineMemoryTopology::discover failed: a simulated failure\n\ - {\"reason\":\"x-probe-topology\",\"arch\":\"aarch64\",\"cross_check\":\"not_measured\"}\n"; - - assert_eq!( - check(report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "architecture", - prose: "x86_64".to_owned(), - ndjson: "aarch64".to_owned(), - }], - "an unmeasured report renders the architecture twice like any other, so \ - the absence of a processor count must not suppress the comparison" - ); -} - -#[test] -fn a_cpu_set_only_numa_count_the_two_renderings_disagree_about_is_a_violation() { - // The third double-rendered fact this module shipped without reading, after - // `arch` and the outermost-cache discriminator. Found by a review, not by - // the oracle's own coverage -- which is the argument for M2.10's approach of - // deriving the fact set rather than extending it by hand. - // - // The prose line is rendered ONLY when the count is above zero, so the - // fixture has to add it: `clean_report()` describes a host with none. - let report = clean_report().replace( - "NUMA domains : 1 (0 with no processors)", - "NUMA domains : 1 (0 with no processors)\n (5 reported only by CPU Sets, never by the relationship walk:", - ); - let report = report.replace( - r#""numa_domains_without_processors":0"#, - r#""numa_domains_without_processors":0,"numa_domains_only_in_cpu_sets":9"#, - ); - - assert!( - check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "NUMA domains reported only by CPU Sets", - .. - } - )), - "the prose says 5 and the body 9: {:#?}", - check(&report) - ); -} - -#[test] -fn a_report_with_no_cpu_set_only_line_is_accepted() { - // The conditional half. On a host where the count is zero the renderer emits - // no such line, and absence is silence rather than a disagreement with the - // NDJSON's `0`. An oracle that read the missing line as a mismatch would - // fire on almost every host -- the over-constraining failure this file's - // acceptance half exists to catch. - let report = clean_report().replace( - r#""numa_domains_without_processors":0"#, - r#""numa_domains_without_processors":0,"numa_domains_only_in_cpu_sets":0"#, - ); - - assert_eq!( - check(&report), - Vec::new(), - "a host with no CPU-Set-only domains renders no line to compare" - ); -} - -#[test] -fn a_policy_the_ndjson_renames_is_a_violation() { - // Gap 4, found by a review: the policy NAME is a double-rendered fact, and - // locating the NDJSON entry by it made a failed lookup silent. Before this - // rule, renaming one side alone left the oracle with nothing to compare and - // the report was accepted. - let report = clean_report().replace(r#""by-core":8"#, r#""by-cores":8"#); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "policy names", - prose: "by-core, single".to_owned(), - ndjson: "by-cores, single".to_owned(), - }], - "the prose names a policy the NDJSON does not" - ); -} - -#[test] -fn a_policy_missing_from_the_ndjson_is_a_violation() { - // The same silence in its other form. Dropping the entry leaves the prose - // row with nothing to match, which the per-entry comparison cannot report. - let report = clean_report().replace(r#","by-core":8"#, ""); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "policy names", - prose: "by-core, single".to_owned(), - ndjson: "single".to_owned(), - }], - "a policy the prose reports is absent from the machine-readable line" - ); -} - -#[test] -fn a_policy_only_the_ndjson_reports_is_a_violation() { - // The third form, and the one a per-entry loop over PROSE rows can never - // see: an entry the prose never mentions is not iterated at all. - let report = clean_report().replace(r#""by-core":8}"#, r#""by-core":8,"by-l3":2}"#); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "policy names", - prose: "by-core, single".to_owned(), - ndjson: "by-core, by-l3, single".to_owned(), - }], - "the NDJSON carries a policy the prose does not report" - ); -} - -#[test] -fn a_cache_the_ndjson_moves_to_another_level_is_a_violation() { - // The cache half of gap 4, and the exact case the review named: the prose - // still reads `L3` while the NDJSON calls it level 9, so the level lookup - // matches nothing and the domain count goes uncompared. - let report = clean_report().replace(r#"{"level":3,"domains":1}"#, r#"{"level":9,"domains":1}"#); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "cache levels", - prose: "1, 3".to_owned(), - ndjson: "1, 9".to_owned(), - }], - "the two renderings disagree about which cache levels exist" - ); -} - -#[test] -fn collections_rendered_in_different_orders_are_accepted() { - // The acceptance half, and the reason the comparison sorts. Nothing obliges - // the NDJSON to emit its entries in the prose's order, so a rule comparing - // sequences would report a contradiction about ordering that neither - // rendering claims. Both sides get the same comparator, so this stays - // consistent whatever order either chooses. - let report = clean_report() - .replace( - r#""policies":{"single":1,"by-core":8}"#, - r#""policies":{"by-core":8,"single":1}"#, - ) - .replace( - r#""caches":[{"level":1,"domains":8},{"level":3,"domains":1}]"#, - r#""caches":[{"level":3,"domains":1},{"level":1,"domains":8}]"#, - ); - - assert_eq!( - check(&report), - Vec::new(), - "the same entries in a different order are the same entries" - ); -} - -#[test] -fn an_agreeing_verdict_beside_a_nonzero_parse_incomplete_is_a_violation() { - // Gap 5, and the reason it carries a correctness question rather than only - // a completeness one. The renderer publishes the rule where it emits the - // NDJSON: anomalies populate `parse_incomplete`, a non-empty - // `parse_incomplete` forces the verdict away from `agree`, so - // `cross_check == "agree"` implies no record failed to decode. An `agree` - // beside a nonzero count is therefore the report contradicting its own - // published rule, in the field a mining pass trusts before any other. - let report = clean_report().replace( - r#""parse_incomplete":[]"#, - r#""parse_incomplete":["no_cores","no_packages"]"#, - ); - - assert_eq!( - check(&report), - vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""parse_incomplete":["no_cores","no_packages"]"#.to_owned(), - verdict_source: "ndjson", - }], - "a parse that did not complete cannot sit beside a verdict saying every \ - check matched" - ); -} - -#[test] -fn an_agreeing_verdict_beside_a_nonzero_anomaly_count_is_a_violation() { - // The same rule reached through the other field it names. Anomalies populate - // `parse_incomplete`, so an `agree` verdict rules both out. - let report = clean_report().replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"enumeration_anomalies":["undersized"]}"#, - ); - - assert_eq!( - check(&report), - vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""enumeration_anomalies":["undersized"]"#.to_owned(), - verdict_source: "ndjson", - }], - "a dropped enumeration record cannot sit beside an agreeing verdict" - ); -} - -#[test] -fn a_not_compared_count_the_two_renderings_disagree_about_is_a_violation() { - // The completeness half. Under DISAGREE the prose labels each skipped check - // on its own line, so the two renderings of how many there were can be - // compared directly. - // `clean_report()` renders no `not_compared` at all -- it is narrower than - // the real renderer -- so the field has to be added here rather than - // replaced. Written as a replacement of an absent key, this test passed - // while checking nothing, which is how the first draft of it went green. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => DISAGREE. This is a finding, not a nuisance:\n\ - \x20 - the group count disagrees\n\ - \x20 (not compared) GetNumaHighestNodeNumber is not a count", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"disagree""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"disagreements":["processor_groups"],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed","active_processor_count_failed"],"enumeration_anomalies":[]}"#, - ); +fn a_bracket_inside_a_string_does_not_unbalance_a_row() { + // **The acceptance half that matters most**, because the renderer does emit + // brackets inside strings: a failed discovery's `io::Error` is interpolated + // into a string value, and an OS message is free to contain one. A balance + // check that counted them would report every such host as malformed. + let with_brackets = r#"{"reason":"x-probe-topology","error":"failed at [0] {oops}"}"#; - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "not compared count", - prose: "1".to_owned(), - ndjson: "4".to_owned(), - }], - "the prose lists one skipped check and the NDJSON claims four" - ); + assert_eq!(check(&report_with(with_brackets)), Vec::new()); } #[test] -fn an_incomplete_verdict_listing_fewer_entries_than_it_counts_is_a_violation() { - // Under INCOMPLETE both kinds render as a bare `- `, so only their TOTAL is - // recoverable from the prose -- and that total is what this compares. - // Claiming to separate them here would be reading a distinction the prose - // does not draw. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - a cache record failed to decode", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":["no_cores","no_packages","not_measured"],"not_compared":[],"enumeration_anomalies":[]}"#, - ); +fn an_escaped_quote_does_not_end_a_string() { + // The other half of the same hazard: a `\"` inside an error message would + // otherwise close the string early and put the rest of the message into + // bracket-counting. + let escaped = r#"{"reason":"x-probe-topology","error":"he said \"[\" and left"}"#; - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "incomplete-verdict listing count", - prose: "1".to_owned(), - ndjson: "3".to_owned(), - }], - "one entry is listed where the counts total three" - ); + assert_eq!(check(&report_with(escaped)), Vec::new()); } #[test] -fn a_nonzero_not_compared_beside_an_incomplete_verdict_is_accepted() { - // The acceptance half, and it pins the direction of the rule. The renderer - // states the implication ONE WAY: a run whose counter failed to read has a - // complete parse and still reports `incomplete`. So a nonzero - // `not_compared` is legal here, and a rule asserting it forces the verdict - // would be claiming more than the contract does. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - GetActiveProcessorCount could not be read", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"not_compared":["machine_changed"],"enumeration_anomalies":[]}"#, - ); +fn a_repeated_key_is_a_defect() { + // Not a parse error in most readers -- they take the last -- so this is + // precisely the malformation that survives a consumer's parse and changes + // what it reads. + let repeated = r#"{"reason":"x-probe-topology","processors":16,"processors":8}"#; assert_eq!( - check(&report), - Vec::new(), - "a counter that could not be read is exactly what an incomplete verdict \ - reports, and the parse is untouched by it" + check(&report_with(repeated)), + vec![RowDefect::RepeatedKey { + key: "processors".to_owned() + }] ); } -/// The prose each discriminator arm announces itself with, as the renderer -/// writes it. Used to exercise every arm rather than the one this host happens -/// to produce. -fn partitioning_prose(arm: &str) -> &'static str { - match arm { - "level" => "\noutermost cache that partitions the processors it covers: L1 (8 domains)", - "none" => { - "\nno cache level reported more than one domain, so nothing here divides\nthe work by cache." - } - "no_levels_reported" => { - "\nno cache levels were reported at all, so nothing here says whether a\ncache boundary divides this machine." - } - "not_unique" => { - "\nat least one cache level reported more than one distinct domain, but\nno unique outermost one was established: either two partition this\nmachine incomparably, or the candidates were rejected as overlapping\n-- in which case none of them partitions it at all." - } - "summary_missing" => { - "\nBUG IN THIS PROBE: the topology crate named L1 as the outermost\npartitioning cache and this survey carries no summary for it. Nothing\nbelow about cache partitioning can be trusted." - } - other => unreachable!("unmapped arm {other}"), - } -} - -/// `clean_report()` with its partitioning prose and discriminator set -/// independently, so the two can be made to disagree. -fn report_with_partitioning(prose_arm: &str, published: &str) -> String { - clean_report() - .replace( - "\noutermost cache that partitions the processors it covers: L1 (8 domains)", - partitioning_prose(prose_arm), - ) - // **Replaces the discriminator rather than adding one.** This appended a - // second member, which was harmless while `clean_report()` carried no - // discriminator at all -- and stopped being harmless the moment the - // fixture was corrected to publish the field the renderer always emits. - // Measured then: every fixture from this builder carried - // `"outermost_partitioning_cache"` TWICE, so the row was not JSON any - // consumer could parse, and the tests passed only because - // `ndjson_field` happens to read the first of the two. - .replace( - r#""outermost_partitioning_cache":"level""#, - &format!(r#""outermost_partitioning_cache":"{published}""#), - ) -} - -#[test] -fn the_partitioning_fixtures_publish_one_discriminator_each() { - // **The fixture builder must produce a row a consumer could parse.** It - // appended the discriminator rather than replacing it, which was invisible - // while `clean_report()` carried none -- and the moment that fixture was - // corrected to publish what the renderer always emits, every report from - // this builder carried the key TWICE. The suite stayed green because - // `ndjson_field` reads the first of the two, so the tests were right by - // accident about an artifact the crate cannot emit. - for arm in [ - "level", - "none", - "no_levels_reported", - "not_unique", - "summary_missing", - ] { - let text = report_with_partitioning(arm, arm); - let row = text - .lines() - .find(|line| line.starts_with('{')) - .unwrap_or_default(); - - assert_eq!( - row.matches(r#""outermost_partitioning_cache":"#).count(), - 1, - "the {arm} fixture must name the discriminator once:\n{row}" - ); - assert!( - row.contains(&format!(r#""outermost_partitioning_cache":"{arm}""#)), - "and it must be the arm asked for:\n{row}" - ); - } -} - -#[test] -fn every_partitioning_arm_agreeing_with_its_prose_is_accepted() { - // The acceptance half, walked over EVERY arm rather than the one this host - // produces. The checklist item that queued this work said to map every arm - // to its prose before writing the rule, because a rule against a guessed - // subset fires falsely on the arms it guessed wrong -- which this module has - // already done once, in the banner rule. - for arm in [ - "level", - "none", - "no_levels_reported", - "not_unique", - "summary_missing", - ] { - let report = report_with_partitioning(arm, arm); - let violations = check(&report); - - // `summary_missing` opens with `BUG IN THIS PROBE`, which the alarm rule - // reads -- correctly, and beside an agreeing verdict. That is a real - // correspondence about a different fact, so it is expected here rather - // than suppressed. - let partitioning: Vec<_> = violations - .iter() - .filter(|violation| { - matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning answer", - .. - } - ) - }) - .collect(); - - assert!( - partitioning.is_empty(), - "arm {arm} agrees with its own prose and must not be reported: {violations:#?}" - ); - } -} - -#[test] -fn every_partitioning_arm_contradicting_its_prose_is_a_violation() { - // Corrupting each arm in turn, which is the other half of what the item - // asked for. Each arm is paired with a DIFFERENT published value, so no arm - // is left resting on another's coverage. - for (prose_arm, published) in [ - ("level", "none"), - ("none", "level"), - ("no_levels_reported", "not_unique"), - ("not_unique", "no_levels_reported"), - ("summary_missing", "level"), - ] { - let report = report_with_partitioning(prose_arm, published); - - assert!( - check(&report).contains(&Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning answer", - prose: prose_arm.to_owned(), - ndjson: published.to_owned(), - }), - "prose announcing {prose_arm} beside a published {published} is two \ - opposite answers to this probe's central question, and went unread \ - until this rule: {:#?}", - check(&report) - ); - } -} - #[test] -fn the_defect_the_review_found_is_a_violation() { - // The concrete case reported: a real report whose prose names a partitioning - // level while the discriminator says no level partitions. Before this rule - // the oracle accepted it, because it compared only the LEVEL NUMBER, which - // both renderings still agreed about. - let report = report_with_partitioning("level", "none"); +fn a_key_repeated_inside_a_nested_object_is_not_the_rows_key() { + // Top level only: a nested object's members are that object's keys, and + // repeating one there is a different question. `policies` renders arbitrary + // policy names, so a name colliding with a row key is possible. + let nested = r#"{"reason":"x-probe-topology","processors":16,"policies":{"processors":2}}"#; - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "outermost partitioning answer", - prose: "level".to_owned(), - ndjson: "none".to_owned(), - }], - "the prose names L1 as partitioning while the NDJSON says none does" - ); + assert_eq!(check(&report_with(nested)), Vec::new()); } #[test] -fn a_report_publishing_no_discriminator_is_accepted() { - // **Asserted against the artifact that really carries neither.** This - // asserted on `clean_report()`, whose row had no discriminator when the test - // was written -- and then the fixture was corrected to publish the field the - // renderer always emits, and the test went on passing while testing the - // opposite of its name. It would have stayed green if the missing - // -counterpart rule had regressed. - // - // `report_unmeasured` is the shape where the question actually arises: every - // MEASURED report announces a partitioning arm, because `PartitioningCache` - // has no silent variant, so a measured row that dropped the discriminator is - // a dropped counterpart and not silence -- which is - // `a_dropped_counterpart_is_a_violation_and_not_silence`. Here there is no - // prose claim, so there is nothing to relate. - let unmeasured = crate::topology_report::report_unmeasured( - &host_banner("16p/8c"), - &std::io::Error::other("a simulated failure"), - ); +fn the_rows_keys_are_read_at_the_top_level_only() { + let row = clean_row(); - assert!( - !unmeasured.contains("outermost_partitioning_cache"), - "this test is vacuous unless the row really lacks the field:\n{unmeasured}" - ); assert_eq!( - check(&unmeasured), - Vec::new(), - "a report that makes the claim in neither rendering has nothing to relate" - ); -} - -#[test] -fn a_summary_missing_level_the_two_renderings_disagree_about_is_a_violation() { - // Gap 6, and the sixth unread double-rendering found by a sixth reviewer - // rather than by this module's own coverage -- which is the finding M2.10 - // exists for, arriving on schedule while that item sat open. - // - // `summary_missing` is the one non-`Level` arm whose NDJSON level is a - // NUMBER rather than `null`, so it is the only one of the three that can - // disagree with the prose at all. The oracle's other level comparison is - // keyed to the `Level` arm's prose label and never fires here, so before - // this rule the two numbers were rendered side by side and never related. - let report = report_with_partitioning("summary_missing", "summary_missing").replace( - r#""outermost_partitioning_cache_level":1"#, - r#""outermost_partitioning_cache_level":99"#, - ); - - assert!( - check(&report).contains(&Correspondence::ProseAndNdjsonDisagree { - fact: "summary-missing outermost level", - prose: "1".to_owned(), - ndjson: "99".to_owned(), - }), - "the prose names L1 and the NDJSON publishes 99: {:#?}", - check(&report) + keys(&row), + vec![ + "reason", + "arch", + "processors", + "efficiency_classes", + "caches", + "policies", + "cross_check", + "disagreements", + "parse_incomplete", + ], + "the nested `level`, `domains` and `single` are not the ROW's keys" ); } #[test] -fn a_summary_missing_level_both_renderings_agree_about_is_accepted() { - // The acceptance half. The arm always reports `BUG IN THIS PROBE`, so this - // report is not silent -- the alarm rule reads it correctly, and that is a - // true correspondence about a different fact. What must NOT appear is a - // disagreement about the level, which both renderings give as 1. - let report = report_with_partitioning("summary_missing", "summary_missing"); - - assert!( - !check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "summary-missing outermost level", - .. - } - )), - "both renderings name L1, so the level is not a disagreement: {:#?}", - check(&report) - ); -} +fn the_row_accessor_declines_an_ambiguous_or_malformed_report() { + let one = report_with(&clean_row()); + let two = format!("{}\n{}", report_with(&clean_row()), clean_row()); + let malformed = report_with(r#"{"unbalanced":["#); -#[test] -fn a_report_with_no_summary_missing_arm_reports_no_level_of_it() { - // The rule is keyed to the arm's own sentence, so a report that does not - // carry that arm has nothing to relate. Pinned because a marker matched too - // loosely would fire on every report that happens to mention a level. - assert!( - !check(&clean_report()).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "summary-missing outermost level", - .. - } - )), - "the clean report announces the `level` arm, not `summary_missing`" - ); + assert_eq!(row(&one), Some(clean_row().as_str())); + assert_eq!(row("prose only"), None); + assert_eq!(row(&two), None); + assert_eq!(row(&malformed), None); } #[test] -fn a_class_count_that_matches_the_single_class_is_still_a_violation() { - // The regression this pair exists for, on the host that hides it. The NDJSON - // once emitted the class COUNT under a plural name; the original test caught - // that with prose `[0]` against a count of `1`, where the VALUES differ. On a - // host whose single class is `1`, the count and the list have the same - // contents, and stripping the brackets from both made them identical. - // - // Found by a review. The container is part of the fact. - let report = clean_report() - .replace(" efficiency classes: [0]", " efficiency classes: [1]") - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":1"#); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "efficiency classes", - prose: "[1]".to_owned(), - ndjson: "1".to_owned(), - }], - "a scalar where a list belongs is the historical defect, whatever the value" - ); -} - -#[test] -fn a_single_class_list_rendered_as_a_list_is_accepted() { - // The acceptance half, so the rule reads the CONTAINER rather than merely - // rejecting anything whose text is short. - // - // **The first draft of this test replaced `[1]` with `[1]`**, which matches - // nothing in a fixture that renders `[0]` -- so it re-checked the clean - // report and established nothing about single-class hosts at all. Written - // while fixing a defect of exactly that shape, which is how persistent it - // is. - let report = clean_report() - .replace(" efficiency classes: [0]", " efficiency classes: [1]") - .replace(r#""efficiency_classes":[0]"#, r#""efficiency_classes":[1]"#); - - assert_eq!( - check(&report), - Vec::new(), - "a single class rendered as a list on both sides agrees, so the rule reads \ - the container and not merely the digit" - ); -} - -#[test] -fn an_anomaly_count_the_two_renderings_disagree_about_is_a_violation() { - // `CrossCheck` renders the anomaly count INSIDE one diagnostic sentence, - // however many anomalies there were, so no count of prose lines can check - // it. Before this rule the field was read only as a nonzero predicate under - // an `agree` verdict, which left the number itself unrelated for every other - // verdict. Found by a review. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - windows-topology-sys recorded 2 enumeration anomalies, so what Windows returned", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer","trailing_bytes","truncated_array","undersized"]}"#, - ); - - assert!( - check(&report).contains(&Correspondence::ProseAndNdjsonDisagree { - fact: "enumeration anomaly count", - prose: "2".to_owned(), - ndjson: "5".to_owned(), - }), - "the prose says it recorded 2 and the field lists 5: {:#?}", - check(&report) - ); -} - -#[test] -fn an_anomaly_count_both_renderings_agree_about_is_accepted() { - // The acceptance half, on the same shape. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - windows-topology-sys recorded 2 enumeration anomalies, so what Windows returned", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer"]}"#, - ); - - assert_eq!( - check(&report), - Vec::new(), - "two and two agree, and the incomplete listing totals one entry" - ); -} - -#[test] -fn an_architecture_contradiction_survives_an_attribution_disclaimer() { - // The disclaimer says which of the two bracket READINGS describes the body - // was not established -- a statement about the machine's topology, not its - // instruction set. When both readings name the same architecture, whichever - // one describes the body, the architecture is that one. So a body naming a - // different one contradicts them both, and the exemption does not cover it. - // - // Found by a review: before this, the disclaimer returned before the - // architecture was ever compared, and this report produced no violation. - let report = clean_report() - .replace( - "host: x86_64 16p/8c", - "host: x86_64 8p/4c\n\ - host: x86_64 16p/8c\n\ - HOST READINGS DISAGREE: the two readings above bracket the measurement\n\ - and differ, so which of them names the machine the body below describes\n\ - was not established.", - ) - .replace(r#""arch":"x86_64""#, r#""arch":"aarch64""#); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "architecture", - prose: "x86_64".to_owned(), - ndjson: "aarch64".to_owned(), - }], - "both readings say x86_64 and the body says aarch64; the processor-count \ - exemption does not reach this" - ); -} - -#[test] -fn banners_disagreeing_about_the_architecture_are_not_a_violation() { - // The limit of the rule above, and the reason it is stated as "every banner - // agrees" rather than "the first banner". When the two readings name - // DIFFERENT architectures, which one describes the body really is - // unestablished, and asserting either would be the over-claim the exemption - // exists to prevent. - let report = clean_report().replace( - "host: x86_64 16p/8c", - "host: aarch64 8p/4c\n\ - host: x86_64 16p/8c\n\ - HOST READINGS DISAGREE: the two readings above bracket the measurement\n\ - and differ, so which of them names the machine the body below describes\n\ - was not established.", - ); - - assert_eq!( - check(&report), - Vec::new(), - "the readings disagree with each other, so the body agrees with one of them \ - and this run cannot say which should have described it" - ); -} - -#[test] -fn a_class_list_the_prose_renders_as_a_scalar_is_a_violation() { - // The mirror of `a_class_count_that_matches_the_single_class_is_still_a_violation`. - // That one pinned the NDJSON side; this pins the prose side, because the - // first fix checked only one of them and `normalise_list` strips the - // brackets from whichever side has them. Found by a review of that fix. - let report = clean_report().replace(" efficiency classes: [0]", " efficiency classes: 0"); - - assert_eq!( - check(&report), - vec![Correspondence::ProseAndNdjsonDisagree { - fact: "efficiency classes", - prose: "0".to_owned(), - ndjson: "[0]".to_owned(), - }], - "the container is part of the fact in BOTH renderings, not just the \ - machine-readable one" - ); -} - -#[test] -fn an_agreeing_verdict_beside_skipped_work_is_a_violation() { - // `CrossCheck`'s verdict makes `agree` imply that nothing was skipped as - // well as that nothing failed to decode, so an agreeing report publishing a - // nonzero `not_compared` contradicts its own published rule -- the same - // shape as a nonzero `parse_incomplete` beside `agree`, which this module - // already read. Found by a review. - let report = clean_report().replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed"],"enumeration_anomalies":[]}"#, - ); - - assert_eq!( - check(&report), - vec![Correspondence::AlarmWithAgreeingVerdict { - alarm: r#""not_compared":["machine_changed","bracket_not_established","highest_numa_node_failed"]"#.to_owned(), - verdict_source: "ndjson", - }], - "work the probe skipped cannot sit beside a verdict saying every check \ - it could make was made" - ); -} - -#[test] -fn a_caller_error_that_mimics_the_probe_is_not_read_as_the_probe() { - // `report_unmeasured` embeds the caller's `io::Error`, so the report carries - // text the renderer does not own. An unanchored substring search reads that - // text as if the probe had spoken it. - // - // The renderer CONTAINS that text now -- `renderer_owns_every_line` flattens - // it, so it cannot introduce a line -- but its words still sit inside the - // discovery-failure line, which is why this test is about anchoring rather - // than about containment. The two answer different halves. - // - // Measured, before the anchoring: this report tripped the alarm rule and - // panicked inside the renderer's own binding -- the oracle inventing a - // contradiction out of a message it should treat as opaque. Found by a - // review. - let report = crate::topology_report::report_unmeasured( - &host_banner("16p/8c"), - &std::io::Error::other("BUG IN THIS PROBE => agree"), - ); - - assert_eq!( - check(&report), - Vec::new(), - "the probe reported a failure whose MESSAGE mentions an alarm and a \ - verdict; neither is a line this renderer wrote" - ); -} - -#[test] -fn a_real_alarm_is_still_read_when_the_renderer_writes_it() { - // The other direction, so the anchoring cannot quietly turn the alarm rule - // off. The renderer writes `BUG IN THIS PROBE` at the start of its own line. - let report = report_with_partitioning("summary_missing", "summary_missing"); - - assert!( - check(&report) - .iter() - .any(|violation| matches!(violation, Correspondence::AlarmWithAgreeingVerdict { .. })), - "an alarm the renderer itself wrote, beside an agreeing verdict, is still \ - a violation: {:#?}", - check(&report) - ); -} - -#[test] -fn a_caller_error_cannot_introduce_a_line_of_its_own() { - // The stronger form of the same defect, and the one that is worse than a - // false alarm. `report_unmeasured` interpolated the caller's `io::Error` - // VERBATIM when this was written, so an error carrying NEWLINES could put - // lines into the report that this renderer never wrote -- and anchoring to - // line starts does not help when the injected text starts its own line. - // - // Stated in the past tense because the renderer has since been fixed: - // `renderer_owns_every_line` flattens the error, so it can no longer create - // a line at all. This test is what keeps that true, so it describes the - // regression it prevents rather than a hazard that is still open. - // - // Two of them, because they fail differently: - // - // `\nBUG IN THIS PROBE\n=> agree` invents an alarm beside a verdict; - // `\n{"cross_check":"agree"}` is selected as the machine-readable row, - // so the oracle checks the CALLER's text - // instead of the probe's. - // - // The renderer now flattens caller text, so neither can create a line. - // Found by a review, which was right that documenting the hole was not the - // same as closing it. - for injection in [ - "x\nBUG IN THIS PROBE\n=> agree", - "x\n{\"reason\":\"x-probe-topology\",\"arch\":\"aarch64\",\"cross_check\":\"agree\"}", - ] { - let report = crate::topology_report::report_unmeasured( - &host_banner("16p/8c"), - &std::io::Error::other(injection), - ); - - assert_eq!( - report.lines().filter(|line| line.starts_with('{')).count(), - 1, - "the report must carry exactly one machine-readable row, and it must \ - be the renderer's:\n{report}" - ); - assert_eq!( - check(&report), - Vec::new(), - "an error message is opaque payload, not the probe speaking:\n{report}" - ); - } -} - -#[test] -fn a_banner_cannot_introduce_a_line_of_its_own() { - // The banner is caller-supplied too, and reaches every report through - // `preamble` rather than only the unmeasured one. - let report = crate::topology_report::report_unmeasured( - &host_banner("16p/8c\n=> agree\nBUG IN THIS PROBE"), - &std::io::Error::other("a simulated failure"), - ); - - assert_eq!( - check(&report), - Vec::new(), - "a banner cannot smuggle in a verdict or an alarm:\n{report}" - ); -} - -#[test] -fn an_attribution_banner_keeps_its_lines() { - // **The regression this pair exists for.** `attribution` renders two `host:` - // readings and a disclaimer when they differ, so the banner legitimately - // spans several lines. An earlier containment flattened it unconditionally: - // the disclaimer stopped being a line of its own, the oracle's exemption for - // it stopped firing, and the second reading vanished. - // - // Nothing caught it because this host's two readings agree, so every report - // rendered here carries a one-line banner -- the shape blindness this branch - // keeps paying for, this time in the renderer rather than an instrument. - // **Built rather than discovered.** This called `Fingerprint::discover()` and - // panicked if it failed -- on a crate whose whole point is that discovery can - // fail, and whose renderer has a dedicated arm for exactly that. A host that - // could not read its own topology would have failed this test for a reason it - // is not about. Found by a review. - // - // The architecture comes from the build so the banner agrees with the row; - // a literal would contradict it off that architecture, which is the same - // portability defect that cost five failures on `i686-pc-windows-msvc`. - let banner = crate::topology_report::attribution( - &Ok(built_fingerprint()), - &Err(std::io::Error::other("the second reading failed")), - ); - let text = crate::topology_report::report_unmeasured( - &banner, - &std::io::Error::other("a simulated failure"), - ); - - assert!( - text.lines() - .any(|line| line.starts_with("HOST NOT ESTABLISHED:")), - "the disclaimer must remain a line of its own, or the oracle's exemption \ - for it cannot fire:\n{text}" - ); - assert_eq!( - text.lines() - .filter(|line| line.starts_with("host:")) - .count(), - 2, - "both bracket readings must survive as their own lines:\n{text}" - ); -} - -#[test] -fn a_banner_cannot_occupy_a_reserved_line_position() { - // The other half. The banner is a `&str` any caller can supply and it - // occupies the first line, so an arbitrary string could impersonate a line - // the renderer reserves -- which flattening newlines did not stop, because - // the banner IS a line. - // - // Measured before this: a banner of a whole NDJSON object gave the report - // TWO machine-readable rows and the oracle read the caller's rather than the - // renderer's; a banner of `=> agree` was read as a verdict and panicked a - // valid unmeasured report. - for impersonation in [ - r#"{"reason":"x-probe-topology","arch":"aarch64","cross_check":"agree"}"#, - "=> agree. Every check this probe could make was made and matched.", - "BUG IN THIS PROBE: pretending to be an alarm", - ] { - let text = crate::topology_report::report_unmeasured( - impersonation, - &std::io::Error::other("a simulated failure"), - ); - - assert_eq!( - text.lines().filter(|line| line.starts_with('{')).count(), - 1, - "exactly one machine-readable row, and it is the renderer's:\n{text}" - ); - assert_eq!( - check(&text), - Vec::new(), - "a banner names a machine; it cannot be a verdict, an alarm, or a \ - row:\n{text}" - ); - } -} - -#[test] -fn a_contained_banner_still_says_what_it_said() { - // **Containment must not be destruction, and an earlier test could not tell - // the difference.** It asserted the report was SAFE -- one machine-readable - // row, no violations -- which a containment that threw the banner away - // entirely also satisfies. Mutation testing found exactly that: replacing - // `renderer_owns_every_line` with `String::new()` or a constant survived, - // because nothing checked the banner still named the machine. - // - // A banner a reader cannot read is not a fixed banner. The first line has to - // remain the host's, whatever had to be done to make it safe to print. - let text = crate::topology_report::report_unmeasured( - "an-odd-machine\nrunning-something-unusual", - &std::io::Error::other("a simulated failure"), - ); - let first = text.lines().next().unwrap_or_default(); - - assert!( - first.starts_with("host:"), - "the first line is the host banner:\n{text}" - ); - for word in ["an-odd-machine", "running-something-unusual"] { - assert!( - first.contains(word), - "containment flattens the banner; it does not discard it -- {word} is \ - missing from {first:?}:\n{text}" - ); - } -} - -// --- what the mutation sweep found nothing pinned -------------------------- - -#[test] -#[should_panic(expected = "the report's parts contradict each other")] -fn assert_corresponds_panics_on_a_contradicting_report() { - // **The deepest thing nothing checked.** Every other instrument in this - // crate trusts `assert_corresponds`: the renderers are bound to it, the - // real-host test calls it, and the corpus reaches it by rendering. Replacing - // its body with `()` survived the mutation sweep -- because every test that - // would notice goes THROUGH it, so a no-op assertion makes them all pass. - // - // The one direction nothing could establish from the inside. - super::assert_corresponds( - &clean_report().replace("processors (online) : 16", "processors (online) : 8"), - ); -} - -#[test] -fn assert_corresponds_accepts_a_report_that_agrees_with_itself() { - // The other half, so the fix above cannot be "always panic". - super::assert_corresponds(&clean_report()); -} - -#[test] -fn a_summary_missing_marker_with_no_level_number_names_no_level() { - // `(end > 0)` is what stops a marker with no digits after it reporting an - // EMPTY level as though it were one. Mutating it to `>=` survived, because - // every fixture puts a number there. - let report = report_with_partitioning("summary_missing", "summary_missing") - .replace("named L1 as the outermost", "named Lx as the outermost"); - - assert!( - !check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "summary-missing outermost level", - .. - } - )), - "a marker with no level number names no level, so there is nothing to \ - relate: {:#?}", - check(&report) - ); -} - -#[test] -fn an_anomaly_sentence_with_no_count_names_no_count() { - // The same guard in the anomaly reader, and the same reason it survived. - let report = clean_report() - .replace( - " => agree. Every check this probe could make was made and matched.", - " => INCOMPLETE. Nothing this probe compared disagreed, but this run\n\ - \x20 did not establish that the parse is consistent:\n\ - \x20 - windows-topology-sys recorded many enumeration anomalies", - ) - .replace(r#""cross_check":"agree""#, r#""cross_check":"incomplete""#) - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":["no_cores"],"not_compared":[],"enumeration_anomalies":["undersized","overruns_buffer","trailing_bytes"]}"#, - ); - - assert!( - !check(&report).iter().any(|violation| matches!( - violation, - Correspondence::ProseAndNdjsonDisagree { - fact: "enumeration anomaly count", - .. - } - )), - "a sentence with no number in it states no count: {:#?}", - check(&report) - ); -} - -#[test] -fn a_half_marker_is_not_a_marker() { - // A provenance marker is the SHAPE `!!...!!`, and both ends are required. - // Mutating the `&&` to `||` survived, because no fixture carried a token - // with only one end -- so nothing established that a half-marker is read as - // an ordinary token rather than skipped as decoration. - let report = clean_report().replace("host: x86_64 16p/8c", "host: !!SYNTHETIC 16p/8c"); - - assert!( - check(&report).contains(&Correspondence::ProseAndNdjsonDisagree { - fact: "architecture", - prose: "!!SYNTHETIC".to_owned(), - ndjson: "x86_64".to_owned(), - }), - "`!!SYNTHETIC` is not the marker shape, so it is where the architecture \ - should have been: {:#?}", - check(&report) - ); -} - -#[test] -fn a_string_field_is_read_with_its_quotes_when_the_container_matters() { - // `ndjson_raw_field` keeps the delimiters so a caller can tell a scalar from - // a one-element list. Its STRING branch had no exercise at all: the only - // caller asks about `efficiency_classes`, which is always an array, so - // mutating the `+ 2` that steps over both quotes survived twice. - let line = r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16}"#; - - assert_eq!(super::ndjson_raw_field(line, "arch"), Some("\"x86_64\"")); - assert_eq!(super::ndjson_raw_field(line, "processors"), Some("16")); - assert_eq!(super::ndjson_raw_field(line, "absent"), None); -} - -#[test] -fn a_marker_in_the_banner_is_not_the_probe_speaking() { - // **The banner is caller text, and containment keeps its CONTENT.** It is - // flattened onto one line beginning `host:`, which stops it impersonating a - // verdict, an alarm or a row -- but the words survive, so a reader that - // searches the whole report still finds a marker inside them. - // - // Two readers did. Measured before the fix: this banner yielded a - // summary-missing level of 99 against the row's 1, and the anomaly banner - // below yielded a count of 99 against the row's 0. Both are the oracle - // raising a violation about a report that does not contain the defect -- - // worse than missing one, because a false alarm sends a reader hunting a - // contradiction the probe never rendered. - let level = report_with_partitioning("level", "level").replace( - "host: x86_64 16p/8c", - "host: x86_64 16p/8c BUG IN THIS PROBE: the topology crate named L99 as the outermost", - ); - - assert_eq!( - check(&level), - [], - "a banner quoting the summary-missing sentence is not a summary-missing arm" - ); - - let anomalies = clean_report() - .replace( - r#""parse_incomplete":[]}"#, - r#""parse_incomplete":[],"not_compared":[],"enumeration_anomalies":[]}"#, - ) - .replace( - "host: x86_64 16p/8c", - "host: x86_64 16p/8c windows-topology-sys recorded 99 enumeration anomalies", - ); - - assert_eq!( - check(&anomalies), - [], - "a banner quoting the anomaly sentence is not a diagnostic entry" - ); -} - -#[test] -fn the_anchored_readers_still_read_the_lines_the_renderer_writes() { - // The acceptance half. Anchoring is only worth having if it still reads the - // real thing, and the two markers sit differently on their lines: the - // summary-missing sentence BEGINS its line, while `CrossCheck` writes its - // diagnostic entries as ` - `, so the anomaly marker never - // does. A reader anchored to line starts alone would have gone blind to the - // second while looking fixed. - let summary = report_with_partitioning("summary_missing", "summary_missing"); - - assert_eq!( - super::summary_missing_level(&summary), - Some("1"), - "the renderer's own summary-missing line is still read" - ); - - let bulleted = " => INCOMPLETE. Nothing this probe compared disagreed:\n\ - \x20 - windows-topology-sys recorded 2 enumeration anomalies, so what Windows returned"; - - assert_eq!( - super::anomaly_count_in_prose(bulleted), - Some("2"), - "the renderer's bulleted diagnostic entry is still read" - ); -} - -#[test] -fn a_banner_cannot_state_a_disagreement_and_deny_it() { - // **A shape `attribution` cannot produce must not reach the reader.** - // `attribution` prints ONE `host:` line when the two bracket readings agree - // and TWO with a disclaimer when they do not; the disclaimer is the sentence - // that says which reading describes the body was not established. - // - // The recogniser used to accept any number of `host:` lines with the - // disclaimer optional, so two readings naming DIFFERENT machines passed - // through verbatim with nothing saying they conflicted. Measured then: that - // banner rendered beside an `"arch":"x86_64"` row and `check` returned no - // violations, because the architecture rule's exemption for an unestablished - // host covered a banner that had never claimed to be unestablished. - // Both readings name the BUILD's architecture, not a literal: the row - // publishes `std::env::consts::ARCH`, so a hard-coded one contradicts it off - // x86_64 and this test would fail for a reason it is not about. Measured - // while writing it -- the first draft said `aarch64` and the bound oracle - // fired on the contradiction rather than the shape. - let reading = format!("host: {} 16p/8c", std::env::consts::ARCH); - let two_readings = crate::topology_report::report_unmeasured( - &format!("{reading}\n{reading}"), - &std::io::Error::other("a simulated failure"), - ); - - assert!( - two_readings.starts_with(&format!("{reading} {reading}\n")), - "an unattributable banner is contained onto one line, not trusted: {two_readings}" - ); - - // The two readings agree here so containment cannot manufacture an - // architecture contradiction, which is what lets this test assert the SHAPE - // on its own. The contradicting case is the one the bound oracle now - // catches. - // - // The acceptance half -- that a banner `attribution` really did write still - // passes through with its lines intact -- is - // `an_attribution_banner_keeps_its_lines`, which renders the two-readings - // -and-a-disclaimer shape and asserts both the disclaimer line and the host - // line count. Tightening the cardinality wrongly would turn that test red, - // so it is not restated here. -} - -#[test] -fn a_banner_no_one_wrote_in_ascii_does_not_panic_the_oracle() { - // **`check` is public and the banner is caller text, so a slice that is - // wrong on a multi-byte char is a panic out of the oracle rather than a - // violation.** Containment neutralises `\n` and `\r` only; every other byte - // reaches the readers. Measured before the fix, on the banner below: - // `start byte index 8 is not a char boundary; it is inside '-'` raised from - // `processors_in_banner`. - // - // A localised `io::Error` interpolated by `banner_line_for` is the - // plausible route on a non-English host, so this is not only a fuzzing - // curiosity. - let report = clean_report().replace("host: x86_64 16p/8c", "host: \u{2013}16p/8c"); - - // The banner's count is now unreadable as a number, which is a - // disagreement with the body and not an error -- what matters here is that - // the oracle ANSWERS instead of unwinding. - let violations = check(&report); - - assert!( - violations - .iter() - .all(|found| !matches!(found, Correspondence::AlarmWithAgreeingVerdict { .. })), - "the odd banner must not be read as an alarm: {violations:?}" - ); - - // The same char in every other position the readers touch. - for banner in [ - "host: \u{2013} 16p/8c", - "host: x86_64 16p/8c \u{2013}", - "\u{2013}", - "host: \u{65e5}16p/8c", - ] { - let report = clean_report().replace("host: x86_64 16p/8c", banner); - let _ = check(&report); - } -} - -#[test] -fn the_anomaly_count_is_read_under_a_disagreeing_verdict_too() { - // **The entry's tag depends on the verdict.** `CrossCheck` writes - // `parse_incomplete` entries as `- {caveat}` under `INCOMPLETE`, but under - // `DISAGREE` as `(parse incomplete) {caveat}` so a reader can tell the - // disagreement from what was merely not established. The reader anchored to - // the bullet alone, so on a disagreeing report the anomaly sentence was - // never found and the two renderings of the count went uncompared -- - // directly contradicting the comment above the rule, which says it is read - // for every verdict. - // - // Measured before the fix: the `(parse incomplete) ` rendering returned - // `None` where the `- ` rendering returned `Some("2")`. - let entry = "windows-topology-sys recorded 2 enumeration anomalies, so what Windows returned"; - - for line in [ - format!(" - {entry}"), - format!(" (parse incomplete) {entry}"), - format!(" (not compared) {entry}"), - ] { - assert_eq!( - super::anomaly_count_in_prose(&line), - Some("2"), - "every tag the renderer can write must leave the sentence readable: {line}" - ); - } - - // The rejection half stays intact: a tag is renderer-owned because a - // contained banner is one line beginning `host:`, so caller text cannot - // present one. - assert_eq!( - super::anomaly_count_in_prose(&format!("host: x86_64 16p/8c {entry}")), - None, - "a banner quoting the sentence is still not a diagnostic entry" - ); -} - -#[test] -fn a_failed_discovery_banner_is_not_read_as_a_fingerprint() { - // **A banner only has to MENTION `p/` for a search to find a fingerprint in - // text that is not one.** `banner_line_for` renders a failed read as - // `host: UNKNOWN -- topology discovery failed: {error}` with the - // `io::Error` verbatim -- not as the bare word `UNKNOWN`, which is what this - // module's doc claimed until this test was written. - // - // So an error text of `16p/foo something opaque` made the count reader - // answer `16`, which satisfied the guard, and the architecture reader then - // answered `UNKNOWN` against a real `arch`. Measured before the fix: the - // bound assertion PANICKED on a valid unmeasured report -- the probe - // crashing on the host whose discovery failed, which is the host it exists - // to report. - let error = std::io::Error::other("16p/foo something opaque"); - let banner = windows_placement_probe::fingerprint::banner_line_for(&Err( - std::io::Error::other("16p/foo something opaque"), - )); - - assert!( - banner.contains("p/"), - "this test is pointless unless the error text reaches the banner: {banner}" - ); - assert_eq!( - super::processors_in_banner(&banner), - None, - "a failed read names no fingerprint, whatever its error text spells" - ); - assert_eq!(super::architecture_in_banner(&banner), None); - - // End to end: rendering asserts on its own output under this build, so a - // false violation here is a panic rather than a return value. - let text = crate::topology_report::report_unmeasured(&banner, &error); - - assert!(text.contains("UNKNOWN"), "{text}"); - - // The acceptance half: a real fingerprint is still read. - let real = host_banner("16p/8c"); - - assert_eq!(super::processors_in_banner(&real), Some("16")); - assert_eq!( - super::architecture_in_banner(&real), - Some(std::env::consts::ARCH) - ); - - // And a taint marker still does not displace the two tokens. - let tainted = format!("host: !!SYNTHETIC!! {} 16p/8c", std::env::consts::ARCH); - - assert_eq!( - super::architecture_in_banner(&tainted), - Some(std::env::consts::ARCH) - ); -} - -#[test] -fn a_nested_list_does_not_compare_equal_to_a_flat_one() { - // `trim_matches` removes EVERY consecutive bracket, so `[0]` and `[[0]]` - // both normalised to `0` and a renderer that regressed to a nested array - // beside one-level prose would have compared equal. The punctuation this is - // meant to forgive is `[0, 1]` against `0,1`; a difference in DEPTH is a - // real disagreement. - assert_eq!( - super::normalise_list("[0, 1]"), - super::normalise_list("0,1") - ); - assert_eq!(super::normalise_list("[0]"), "0"); - assert_ne!( - super::normalise_list("[[0]]"), - super::normalise_list("[0]"), - "a nested list is structurally different and must not normalise away" - ); -} - -#[test] -fn a_caveat_in_the_banner_does_not_excuse_an_uncaveated_claim() { - // **The caveat is matched mid-line, so it is the one search here that - // cannot anchor to a line start -- and it is the dangerous direction.** A - // claim found where none was made invents a violation; a caveat found where - // none was made SUPPRESSES one. Measured before the fix: appending the - // caveat sentence to the banner made `UncaveatedClaimUnderDoubt` vanish from - // a report that still carried the claim and still said `parse_incomplete=2`. - let claimed = clean_report() - .replace(r#""parse_incomplete":[]}"#, r#""parse_incomplete":["no_cores","no_packages"]}"#) - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", - ); - - let uncaveated = |text: &str| { - check(text) - .iter() - .any(|found| matches!(found, Correspondence::UncaveatedClaimUnderDoubt { .. })) - }; - - assert!( - uncaveated(&claimed), - "the claim is bare and the parse is in doubt: {:#?}", - check(&claimed) - ); - - let banner_says_it = claimed.replace( - "host: x86_64 16p/8c", - "host: x86_64 16p/8c This run did not establish that the parse is whole", - ); - - assert!( - uncaveated(&banner_says_it), - "a caveat the RENDERER did not write must not excuse the claim: {:#?}", - check(&banner_says_it) - ); - - // The acceptance half: the renderer's own caveat still excuses it. - let properly_caveated = claimed.replace( - " (heterogeneous: an I/O thread left unconstrained can land on an", - " (heterogeneous: an I/O thread left unconstrained can land on an\n (This run did not establish that the parse is whole, and the classes", - ); - - assert!( - !uncaveated(&properly_caveated), - "the caveat beside the claim is what the rule exists to accept: {:#?}", - check(&properly_caveated) - ); -} - -#[test] -fn the_cpu_sets_count_is_selected_by_what_the_line_says_not_by_its_position() { - // **This guards an order dependency, not a defect that was live.** A review - // reported that the heterogeneity line shadows this one, because both open - // ` (` and the reader took the FIRST such line. Checked against the - // renderer: the CPU-Sets line is written in the NUMA block - // (`topology_report.rs:271`) and the heterogeneity line in the - // efficiency-class block (`:295`), so the CPU-Sets line comes first and the - // finding does not reproduce. A fixture with both, in the renderer's order, - // passes under the old reader too -- measured. - // - // The reader is still fixed, because ` (` is not a label. It opens ANY - // parenthesised continuation, so the old code was correct only by the - // accident that nothing else opens one above this. The failure mode that - // accident was hiding is silent: a new continuation added above would make - // the rule read the wrong line, find no CPU-Sets text, and compare nothing - // -- with no error and no sign that a fact had stopped being checked. - // - // So the shape below is one the renderer cannot currently emit. It is the - // only shape that can distinguish selecting by CONTENT from selecting by - // POSITION, which is the property actually being asserted. - let report = clean_report() - .replace( - "packages : 1", - "packages : 1\n (a continuation this renderer does not write today)", - ) - .replace( - "NUMA domains : 1 (0 with no processors)", - "NUMA domains : 1 (0 with no processors)\n (5 reported only by CPU Sets, never by the relationship walk:", - ) - .replace( - r#""numa_domains_without_processors":0"#, - r#""numa_domains_without_processors":0,"numa_domains_only_in_cpu_sets":9"#, - ); - - assert!( - check(&report).iter().any(|found| matches!( - found, - Correspondence::ProseAndNdjsonDisagree { - fact: "NUMA domains reported only by CPU Sets", - .. - } - )), - "the prose says 5 and the body 9, below an unrelated continuation: {:#?}", - check(&report) - ); - - // The renderer's real order still reads, so the fix did not trade one - // position dependency for another. - let as_rendered = clean_report() - .replace( - "NUMA domains : 1 (0 with no processors)", - "NUMA domains : 1 (0 with no processors)\n (5 reported only by CPU Sets, never by the relationship walk:", - ) - .replace( - " efficiency classes: [0]", - " efficiency classes: [0, 1]\n (heterogeneous: an I/O thread left unconstrained can land on an", - ) - .replace( - r#""numa_domains_without_processors":0"#, - r#""numa_domains_without_processors":0,"numa_domains_only_in_cpu_sets":5"#, - ); - - assert!( - !check(&as_rendered).iter().any(|found| matches!( - found, - Correspondence::ProseAndNdjsonDisagree { - fact: "NUMA domains reported only by CPU Sets", - .. - } - )), - "both say 5, so there is nothing to report: {:#?}", - check(&as_rendered) - ); -} - -#[test] -fn a_dropped_counterpart_is_a_violation_and_not_silence() { - // **Every comparison here was "both sides present, do they match", so a - // rendering that DROPPED a field read as silence.** The module's rule that - // an omitted fact is not a violation is about facts the report never - // mentions; once the prose states one, the report has made a claim the - // other rendering is required to answer. - // - // Measured before this: each of the three deletions below left a report the - // oracle accepted, with the prose still making all three claims. A mining - // pass reading such a row gets no value and no warning. - let named = |text: &str, wanted: &str| { - check(text).iter().any(|found| { - matches!(found, Correspondence::RenderedOnlyInProse { fact, .. } if *fact == wanted) - }) - }; - - // A single count, from the four the prose and the row both carry. - let no_processors = clean_report().replace(r#""processors":16,"#, ""); - - assert!( - named(&no_processors, "online processors"), - "the prose still says 16: {:#?}", - check(&no_processors) - ); - - // A whole CONTAINER, which the member-level tests never covered: they check - // an entry going missing from the object, not the object from the row. - let no_policies = clean_report().replace(r#""policies":{"single":1,"by-core":8},"#, ""); - - assert!( - named(&no_policies, "policy names"), - "the prose table still stands: {:#?}", - check(&no_policies) - ); - - // The discriminator, whose early return excused a measured report that - // printed an arm and lost it. - let no_discriminator = clean_report().replace(r#""outermost_partitioning_cache":"level","#, ""); - - assert!( - named(&no_discriminator, "outermost partitioning answer"), - "the prose still announces the level arm: {:#?}", - check(&no_discriminator) - ); - - // **The acceptance half, and the reason no exemption was needed.** - // `report_unmeasured` renders neither side of the TOPOLOGY facts deleted - // above, so it makes no prose claim for those missing fields to leave - // unanswered -- the shape that would have forced a special case if the rule - // had been keyed to the field instead. - // - // Narrowed deliberately: the short object DOES publish `arch`, and the - // banner can name the same architecture, so that correspondence is rendered - // twice here and is checked. "Renders neither side" would have been a - // tidier sentence and a false one. - let unmeasured = crate::topology_report::report_unmeasured( - &host_banner("16p/8c"), - &std::io::Error::other("a simulated failure"), - ); - - assert_eq!( - check(&unmeasured), - [], - "a report that claims nothing cannot leave a claim unanswered" - ); -} - -#[test] -fn a_multiline_discovery_error_does_not_cost_the_disclaimer_its_own_line() { - // **A banner line has to be a line.** `banner_line_for` interpolates a - // failed read's `io::Error` verbatim and an OS error may contain a newline, - // so a single READING could arrive as two lines. `attribution` then composes - // a banner with more lines than readings, `is_attribution_shaped` stops - // recognising the renderer's own output, and `preamble` contains the whole - // value -- taking the renderer-owned disclaimer down with it. - // - // Measured before the fix: a two-line error gave a six-line attribution and - // a report with no `HOST NOT ESTABLISHED:` line at all, so the oracle's - // exemption for an unestablished host stopped firing on a report that had - // legitimately earned it. - let error = || std::io::Error::other("line one\nline two"); - let banner = crate::topology_report::attribution(&Err(error()), &Err(error())); - - assert_eq!( - banner.lines().count(), - 4, - "two readings and a two-line disclaimer, whatever the OS wrote: {banner}" - ); - - let text = crate::topology_report::report_unmeasured(&banner, &error()); - - assert!( - text.lines() - .any(|line| line.starts_with("HOST NOT ESTABLISHED:")), - "the disclaimer must survive as a line of its own: {text}" - ); - assert_eq!( - text.lines() - .filter(|line| line.starts_with("host:")) - .count(), - 2, - "one line per reading, not one per line of error text: {text}" - ); -} - -#[test] -fn a_cache_object_is_read_however_its_members_are_written() { - // **The lookup matched `"level":N,"domains":` as one literal**, which - // requires the two members to be adjacent and in that order. So renaming or - // moving `domains` made the lookup miss, `compare` was never reached, and - // the prose domain count went unchecked -- while `cache_levels` still found - // every level and reported membership as agreeing, which is what made the - // report look whole. - let with_cache = |object: &str| clean_report().replace(r#"{"level":1,"domains":8}"#, object); - - let names = |text: &str, fact: &str| { - check(text).iter().any(|found| { - matches!(found, Correspondence::RenderedOnlyInProse { fact: named, .. } if *named == fact) - || matches!(found, Correspondence::ProseAndNdjsonDisagree { fact: named, .. } if *named == fact) - }) - }; - - // The member is gone: the object is present and cannot answer, which is a - // dropped counterpart rather than a membership problem. - assert!( - names( - &with_cache(r#"{"level":1,"x-domains":8}"#), - "cache domain count" - ), - "a renamed member leaves the prose count with nothing to agree with: {:#?}", - check(&with_cache(r#"{"level":1,"x-domains":8}"#)) - ); - - // **Reordering alone is NOT a contradiction**, so it cannot distinguish a - // reader that handles it from one that is blind -- both say nothing. The - // case that separates them reorders AND disagrees. - assert_eq!( - check(&with_cache(r#"{"domains":8,"level":1}"#)), - [], - "member order is not a disagreement; the value still agrees" - ); - assert!( - names( - &with_cache(r#"{"domains":9,"level":1}"#), - "cache domain count" - ), - "reordered members must still be READ, which only a wrong value can show: {:#?}", - check(&with_cache(r#"{"domains":9,"level":1}"#)) - ); - - // And an object for a level the prose does not list is membership's - // business, not this rule's -- `"level":1` must not match level 10. - assert_eq!( - check(&with_cache( - r#"{"level":1,"domains":8},{"level":10,"domains":4}"# - )) - .iter() - .filter(|found| matches!( - found, - Correspondence::ProseAndNdjsonDisagree { - fact: "cache domain count", - .. - } - )) - .count(), - 0, - "an extra level is a membership finding, reported by level number" - ); -} - -#[test] -fn a_disclaimer_welded_to_a_reading_is_not_attribution_shaped() { - // **The recogniser matched the disclaimer as a SUFFIX, which says nothing - // about whether it starts a line.** `attribution` always writes it after a - // newline, but `trim_end_matches('\n')` accepted however many newlines it - // found -- including none -- so a caller could weld the disclaimer onto the - // second reading and have the whole thing passed through verbatim. - // - // Measured before the fix: the report's second line came out as - // `host: 16p/8cHOST READINGS DISAGREE: ...`, which this renderer - // cannot produce. Containment exists to stop caller text occupying a line - // the renderer reserves; trusting an unproducible shape hands it one. - let reading = format!("host: {} 16p/8c", std::env::consts::ARCH); - let welded = format!( - "{reading}\n{reading}HOST READINGS DISAGREE: the two readings above bracket the measurement\n\ - and differ, so which of them names the machine the body below describes\n\ - was not established." - ); - - let text = crate::topology_report::report_unmeasured( - &welded, - &std::io::Error::other("a simulated failure"), - ); - - // **Containment means ONE LINE, and that is what to assert.** A first - // version asserted the welded substring was gone, which it is not and should - // not be: flattening replaces newlines, and the weld had none, so the two - // stay adjacent. What changes is that the whole banner now occupies a single - // line the renderer prefixed, instead of contributing three lines of its own - // with a disclaimer that looks renderer-owned. - let banner_lines = text - .lines() - .take_while(|line| !line.starts_with("== processor topology")) - .count(); - - assert_eq!( - banner_lines, 1, - "a welded disclaimer must be contained onto one line, not trusted as \ - three:\n{text}" - ); - assert!( - !text - .lines() - .any(|line| line.starts_with("HOST READINGS DISAGREE")), - "and it must not be left standing as a renderer-owned disclaimer:\n{text}" - ); - - // The acceptance half: what `attribution` really writes still passes - // through with its lines intact, which is what - // `an_attribution_banner_keeps_its_lines` asserts in full. Checked here too - // because the fix tightened the very predicate that test depends on. - let genuine = crate::topology_report::attribution( - &Ok(built_fingerprint()), - &Err(std::io::Error::other("the second reading failed")), - ); - let rendered = crate::topology_report::report_unmeasured( - &genuine, - &std::io::Error::other("a simulated failure"), - ); - - assert!( - rendered.starts_with(&format!("{genuine}\n")), - "a banner attribution really did write must still pass through:\n{rendered}" - ); -} - -#[test] -fn an_agreeing_verdict_must_show_the_counters_it_claims_to_have_checked() { - // **`agree` is a claim about what the run DID, not only about what - // matched.** `CrossCheck` reports it to mean every check this probe could - // make was made and matched -- so a report that agrees while omitting the - // line a check reads contradicts its own verdict, even though the halves it - // still renders agree perfectly. - // - // The rule was written to compare two present values and to say nothing - // otherwise, which is the dropped-counterpart shape in the one place where - // the VERDICT is what the missing side contradicts. Measured before the fix: - // deleting the counter line left `check` returning nothing at all. - for label in [ - " GetActiveProcessorCount : ", - " GetActiveProcessorGroupCount: ", - ] { - let line = clean_report() - .lines() - .find(|line| line.starts_with(label)) - .map(str::to_owned) - .unwrap_or_else(|| { - panic!("the fixture must carry {label:?} for this to mean anything") - }); - let without = clean_report().replace(&format!("{line}\n"), ""); - - assert_ne!(without, clean_report(), "the removal must apply: {label:?}"); - assert!( - without.contains(r#""cross_check":"agree""#), - "the verdict must still claim agreement, or there is nothing to \ - contradict: {label:?}" - ); - assert!( - check(&without).iter().any(|found| matches!( - found, - Correspondence::EvidenceMissingWithAgreeingVerdict { .. } - )), - "a counter the verdict claims to have checked is missing, and the \ - oracle accepted it: {:#?}", - check(&without) - ); - } +fn a_defect_is_reported_once_per_repeated_key_rather_than_per_occurrence() { + // Three renderings of one key is one defect about that key, not two. A rule + // that reported per occurrence would make a reader count to work out how + // many keys were involved. + let thrice = r#"{"a":1,"a":2,"a":3,"b":1,"b":2}"#; - // The acceptance half: the untouched fixture renders both counters and is - // accepted, so the rule fires on absence rather than on everything. assert_eq!( - check(&clean_report()), - [], - "a report that shows its counters must still be accepted" + check(&report_with(thrice)), + vec![ + RowDefect::RepeatedKey { + key: "a".to_owned() + }, + RowDefect::RepeatedKey { + key: "a".to_owned() + }, + RowDefect::RepeatedKey { + key: "b".to_owned() + }, + ], + "one entry per EXTRA rendering, so the count reads as how many times the \ + row said it again" ); } diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 12f0604f6..2383a75c7 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -32,7 +32,7 @@ use windows_placement_probe::fingerprint::Fingerprint; use windows_platform_probes::report_oracle; -use windows_platform_probes::topology::measure; +use windows_platform_probes::topology::{invariant, measure}; use windows_platform_probes::topology_report::{attribution, report, report_unmeasured}; /// The report exactly as `probe-topology` composes it. @@ -55,1000 +55,27 @@ fn real_report() -> (String, bool) { } #[test] -fn a_report_rendered_from_this_host_agrees_with_itself() { - let (text, _) = real_report(); - - // The whole assertion. Not "the report says X" -- "the report does not - // contradict itself", which is checkable without knowing anything about - // the machine. - report_oracle::assert_corresponds(&text); -} - -#[test] -fn the_oracle_is_actually_reading_this_host_s_report() { - // Without this, the test above is worth nothing on a host whose report the - // oracle cannot parse: every lookup returns `None`, every comparison is - // skipped, and it passes having checked exactly zero correspondences. +fn a_report_rendered_from_this_host_is_well_formed() { + // **The real host, which no fixture can stand in for.** A fixture is a + // report somebody wrote down, so it can only exercise shapes its author + // imagined -- and the defect this file was built around was a state nobody + // had. On CI this runs across the hosted runner fleet, a slow survey of + // shapes no fixture anticipates. // - // The unit tests pin the oracle against a fixture, which cannot notice the - // renderer drifting away from it. Only the real artifact can, and only if - // something requires a violation to appear. - // - // So a fact this host really rendered is corrupted, and the oracle must - // report it. `"processors":0` is the corruption because no host has zero - // online processors, so it disagrees with the prose on every machine - // without needing to know what the prose says. + // It asserts nothing about this MACHINE, deliberately: a test expecting a + // processor count or a verdict would fail on the next runner shape rather + // than on a defect. What it checks is that whatever this host produced, the + // row a survey will mine is well-formed -- a property every host satisfies, + // including one whose topology cannot be read at all. let (text, measured) = real_report(); - if !measured { - // `report_unmeasured` carries no counts to corrupt. A host whose - // topology cannot be read is a legitimate outcome -- "cannot measure" - // is a third answer in this crate -- and skipping is honest here in a - // way it would not be for the assertion above, which still ran. - note("topology could not be read on this host; corruption check skipped"); - return; - } - - // Every double-rendered fact, not just one. Corrupting a single field would - // leave the other pairs unguarded: the renderer could drift away from the - // oracle's other prose labels and this would still pass on the strength of - // the one. - // - // **Each corruption is chosen against the value actually rendered, and keys - // this host did not render are skipped.** An earlier version replaced every - // count with `0`, on the reasoning that no host has zero processors, groups, - // packages or cores -- which is true of those four and false of the report - // as a whole, and the difference would have failed on legitimate hosts - // rather than on a defect: - // - // * `outermost_partitioning_cache_level` is rendered `null` for the three - // absent-partitioning variants, and no prose level accompanies it, so - // rewriting it to `0` yields no correspondence to violate. - // * `"domains":` does not appear at all on a host that reports no caches. - // * A successfully measured but incomplete parse can render `0` decoded - // packages or cores, where replacing `0` with `0` changes nothing and - // the sabotage silently fails to apply. - // - // So the replacement is `1` where the report says `0` and `0` otherwise, - // which is guaranteed to differ from whatever this host rendered, and a key - // that is absent or `null` is skipped with a reason rather than asserted - // against. - // - // The skip cannot swallow the whole test, and the guard names WHICH facts it - // requires rather than how many. - // - // **A count was not enough, and the difference is not academic.** An earlier - // version asserted `exercised >= 4` against a running total, which the four - // CONDITIONAL entries can satisfy on their own -- so if the four - // unconditional keys drifted out of the renderer, all four would skip, the - // conditional four would make the total, and this test would pass having - // checked none of the facts its comment claimed it required. Demonstrated by - // a reviewer: forcing the four unconditional lookups to miss produced four - // skip messages and a PASS. The comment claimed coverage while the code - // enforced quantity, which is the defect this whole branch is about, in the - // guard written to prevent it. - let mut exercised: Vec<&str> = Vec::new(); - - // `true` where a measured report must carry the fact, so a skip is a defect - // rather than a legal shape. - // - // **Six are unconditional, not four.** An earlier version marked - // `"numa_domains":` and the `"single":` policy optional on the stated - // grounds that "the policy and NUMA entries depend on what the host has". - // They do not, and the renderer says so plainly: `report()` writes the - // `NUMA domains` prose line and the `"numa_domains"` field with no - // condition around either, and `domain_counts()` begins every policy table - // with `("single", 1)` -- a count clamped to one because there is always at - // least one domain. The claim was about the host; the truth was about the - // renderer, which is the same confusion in miniature that this whole test - // exists to catch. - // - // Marking a mandatory fact optional inverts the guard: measured, renaming - // the NDJSON `"numa_domains"` key, and separately renaming the `single` - // policy, each left this test GREEN -- so it passed precisely when the - // renderer-to-oracle binding for a mandatory fact disappeared, which is the - // one event it is here to detect. - // - // The two that remain optional are genuinely conditional: the outermost - // level is `null` on three partitioning variants, and `"domains":` is - // absent when no caches are reported. - // The fact each key must be reported under. A LIST, not one name, because - // one key can be read by different rules depending on the shape the host - // produced -- see the outermost level below. Every entry is still a specific - // fact, so a neighbouring rule cannot stand in for any of them. - for (key, facts, required) in [ - ("\"processors\":", &["online processors"][..], true), - ("\"groups\":", &["processor groups"][..], true), - ("\"packages\":", &["packages"][..], true), - ("\"cores\":", &["physical cores"][..], true), - ("\"numa_domains\":", &["NUMA domains"][..], true), - ( - // **Two facts, because two arms name a level.** The `Level` arm - // renders `outermost cache that partitions the processors it - // covers: L2`, and `SummaryMissing` renders `BUG IN THIS PROBE: the - // topology crate named L2 as the outermost` -- different sentences, - // read by different rules, reported under different fact names. The - // arms come from one `match` and so are mutually exclusive, which is - // what makes accepting either of them precise rather than loose. - // - // Found by a review. With only the first name here, a host that - // produced the `SummaryMissing` shape failed this guard even though - // the field WAS read -- an instrument reporting a defect in the - // renderer that was really a defect in the instrument. - "\"outermost_partitioning_cache_level\":", - &[ - "outermost partitioning cache level", - "summary-missing outermost level", - ][..], - false, - ), - // Nested, and reached by their inner keys: `policies` is an object and - // `caches` an array of objects, so these corrupt the first entry of - // each rather than the container. - ("\"single\":", &["policy domain count"][..], true), - ("\"domains\":", &["cache domain count"][..], false), - ] { - let Some(rendered) = rendered_value(&text, key) else { - assert!( - !required, - "{key} is rendered by every measured report, and this one does \ - not carry it. Either the renderer dropped the field or the key \ - spelling drifted -- both of which make the correspondence stop \ - being checked.\n\n--- the report ---\n{text}" - ); - note(&format!("{key} is not rendered on this host; skipped")); - continue; - }; - if rendered == "null" { - assert!( - !required, - "{key} is rendered null, which no measured report does for this \ - fact.\n\n--- the report ---\n{text}" - ); - note(&format!( - "{key} is rendered null on this host, so no prose accompanies it; skipped" - )); - continue; - } - - let corrupted = corrupt_count(&text, key, if rendered == "0" { "1" } else { "0" }); - assert_ne!( - corrupted, text, - "corrupting {key} changed nothing, so this proves nothing -- a \ - sabotage that fails to apply is indistinguishable from an \ - instrument that fails to fire.\n\n--- the report ---\n{text}" - ); - - // **A violation naming THIS FACT, not merely one of the right kind.** - // This guard has now been wrong twice in the same direction, each fix - // stopping one step short. It first asserted only that the list was - // non-empty. That was strengthened to require a - // `ProseAndNdjsonDisagree`, with a comment correctly explaining that - // corrupting `processors` ALSO trips the cross-check counter rule -- - // and then not acting on it, because the counter rule emits that very - // variant. So the strengthened form still passed while the prose reader - // was blind. - // - // Measured, one entry blinded at a time by breaking its `DOUBLE_RENDERED` - // prose label: with the variant-only assertion, `processors` and `groups` - // both stayed GREEN (masked by their counter rules at - // `check_counters_against_enumeration`), while `packages` went red - // because nothing else reads it. Two of the four required facts were - // unchecked by the guard whose whole purpose is to prove they are - // checked. - // - // Matching on `fact` is what closes it: no neighbouring rule can supply - // another rule's fact name. - let violations = report_oracle::check(&corrupted); - assert!( - violations.iter().any(|violation| matches!( - violation, - report_oracle::Correspondence::ProseAndNdjsonDisagree { fact: named, .. } - if facts.contains(named) - )), - "the oracle read no prose-against-NDJSON correspondence for {key} \ - (expected one of the facts {facts:?}) in a report this host actually \ - produced, so `a_report_rendered_from_this_host_agrees_with_itself` \ - is passing without checking that fact. The renderer has probably \ - drifted from the prose label the oracle looks for.\n\ngot \ - {violations:#?}\n\n--- the report ---\n{text}" - ); - exercised.push(key); - } - - // Named, not counted. Each unconditional fact must appear in what was - // actually exercised, so no number of conditional entries can stand in for - // one of them. - // - // This list is deliberately a SECOND statement of which facts are - // mandatory, rather than being derived from the `required` column above. - // Deriving it would make deleting a table row silently legal -- the row - // would vanish from both the loop and the requirement in one edit. Two - // independent statements mean a row cannot be dropped without this list - // noticing, which is the same reasoning the oracle itself is built on: - // relate two renderings rather than trusting one. - for required in [ - "\"processors\":", - "\"groups\":", - "\"packages\":", - "\"cores\":", - "\"numa_domains\":", - "\"single\":", - ] { - assert!( - exercised.contains(&required), - "{required} was never exercised on this host, so the correspondence \ - it names went unchecked. Exercised: {exercised:?}\n\n\ - --- the report ---\n{text}" - ); - } -} - -/// The value the NDJSON renders for `key`, or `None` when it renders none. -/// -/// Used to decide whether a fact is present at all before corrupting it, and to -/// choose a replacement that differs from what this host actually rendered. -fn rendered_value(report: &str, key: &str) -> Option { - let line = report.lines().find(|line| line.starts_with('{'))?; - let start = line.find(key)? + key.len(); - let end = line[start..] - .find([',', '}']) - .map_or(line.len(), |offset| start + offset); - - Some(line[start..end].to_owned()) -} - -/// Rewrite one NDJSON count to `value`, which the caller picks to differ from -/// what this host rendered. -fn corrupt_count(report: &str, key: &str, value: &str) -> String { - report - .lines() - .map(|line| { - if line.starts_with('{') { - replace_json_number(line, key, value) - } else { - line.to_owned() - } - }) - .collect::>() - .join("\n") -} - -/// Replace the numeric value following `key` in a flat JSON line. -fn replace_json_number(line: &str, key: &str, value: &str) -> String { - let Some(start) = line.find(key) else { - return line.to_owned(); - }; - let after = start + key.len(); - let end = line[after..] - .find([',', '}']) - .map_or(line.len(), |offset| after + offset); - - format!("{}{key}{value}{}", &line[..start], &line[end..]) -} - -// --- M2.10: the fact set, derived from the artifact rather than restated ------ - -/// Why a fact may legitimately go unread on some host. -#[derive(Clone, Copy)] -enum Silence { - /// Never. The prose always states this fact, so it must always be read. - Never, - /// When the key renders this exact value, the prose makes no matching claim. - AtValue(&'static str), - /// When the key renders this value AND the verdict is not `agree`. - /// - /// **Two different rules read these counts, and only one of them is always - /// available.** At zero the prose sentence carrying the count is absent, so - /// the count comparison cannot fire -- but under an AGREEING verdict a - /// nonzero value is a violation in its own right, so the zero-to-nonzero - /// corruption is still caught. Declaring the key silent at zero outright - /// excused both, which made the assertion vacuous on this host and let the - /// declaration's own prose claim a guarantee it had just given away. - /// Measured: with no excuse at all, exactly one mutation went unnoticed -- - /// the deletion -- and on a DISAGREE shape, exactly one went unnoticed -- - /// the corruption. Neither blanket answer is right; the verdict is what - /// separates them. - AtValueUnlessAgreeing(&'static str), - /// When no `host:` line names an architecture. The banner is the only prose - /// rendering of the architecture, and a failed discovery renders `UNKNOWN`, - /// which names none -- so on a host whose bracket reads both failed there is - /// nothing to relate. Conditioned on the REPORT rather than on the key's own - /// value, which is why this cannot be expressed as `AtValue`. - WhenNoBannerArchitecture, -} - -/// Every fact a report may publish, what this test requires of it, and the -/// correspondences that count as reading it. -/// -/// `reads` is the crux. An earlier version treated a key as read when ANY -/// violation appeared after corrupting it, which is the same defect this branch -/// spent rounds fixing elsewhere: corrupting `parse_incomplete` on a -/// heterogeneous report also trips `UncaveatedClaimUnderDoubt`, so the -/// diagnostics reader could be deleted entirely and this test would stay green. -/// Naming the facts that belong to each key is what makes the measurement mean -/// something. Found by a review. -/// -/// `empty_replacement` is the minimal non-empty value to substitute for an empty -/// container. Both empty shapes still have a second rendering -- the renderer -/// emits `efficiency classes: []` unconditionally, and `caches:` followed by -/// `none reported` -- so treating them as silent would let a no-class or -/// no-cache host pass while the reader was gone. Also found by a review. -/// `absent_is_silent_at` is separate from `silence` because the two questions -/// have different answers for the diagnostic counts. At `0` those keys render NO -/// prose entry, so deleting them leaves no claim for the dropped-counterpart -/// rule to answer and the oracle is right to say nothing -- while CORRUPTING -/// them to nonzero is still caught, by the rule that a non-`agree` count -/// contradicts an agreeing verdict. Declaring them `Silence::AtValue("0")` -/// would have excused both and thrown away coverage that exists, so the -/// narrower statement is the true one. -struct Fact { - key: &'static str, - silence: Silence, - reads: &'static [&'static str], - empty_replacement: Option<&'static str>, - /// The value at which the prose renders nothing, so the key's ABSENCE has no - /// prose claim to contradict. Applies only to the deletion mutation. - absent_is_silent_at: Option<&'static str>, - /// The prose label of an independently-read counter for this key, if one - /// exists. Corrupting THAT line moves a side only the counter rule reads, - /// which is what forces that rule to be exercised separately from the - /// prose/NDJSON rule that shares the key. - counter_prose: Option<&'static str>, - why: &'static str, -} - -const fn fact(key: &'static str, reads: &'static [&'static str]) -> Fact { - Fact { - key, - silence: Silence::Never, - reads, - empty_replacement: None, - absent_is_silent_at: None, - counter_prose: None, - why: "", - } -} - -/// The first object in the rendered `caches` array, if the report has one. -/// -/// Taken from the artifact rather than assumed, so a host with no L1 -- or no -/// caches at all -- is described rather than mismatched. -fn first_cache_object(report: &str) -> Option { - let line = report.lines().find(|line| line.starts_with('{'))?; - let at = line.find(r#""caches":[{"#)? + r#""caches":["#.len(); - let rest = &line[at..]; - let end = rest.find('}')? + 1; - - Some(rest[..end].to_owned()) -} - -/// The report with `label`'s prose value replaced by one that disagrees. -/// -/// Moves only the prose side, so the NDJSON still agrees with its own prose and -/// the only rule that can notice is the one reading this counter. -fn corrupt_counter_prose(report: &str, label: &str) -> Option { - let line = report.lines().find(|line| line.starts_with(label))?; - let rendered = line[label.len()..].trim(); - let flipped = if rendered == "0" { "1" } else { "0" }; - - Some(report.replace(line, &format!("{label}{flipped}"))) -} - -const FACTS: &[Fact] = &[ - Fact { - silence: Silence::WhenNoBannerArchitecture, - why: "a report whose bracket reads both failed renders `UNKNOWN`, which \ - names no architecture, so the body's `arch` has no prose to relate", - ..fact("arch", &["architecture"]) - }, - // **Two rules read these keys, so the mutation set has to separate them.** - // Corrupting the NDJSON value moves the enumerated side, which BOTH the - // prose/NDJSON rule and the cross-check counter rule can see -- so the - // ordinary rule's violation satisfied the accounting and masked whether the - // counter rule was read at all. Measured: deleting both counter rules left - // every test in this file green. - // - // `counter_prose` names the line only the counter rule reads. Corrupting it - // leaves the enumeration agreeing with its own prose, so the only violation - // available names the counter fact -- and if that rule is gone, nothing is - // reported and the accounting fails, which is the point. - Fact { - counter_prose: Some(" GetActiveProcessorCount : "), - ..fact( - "processors", - &[ - "online processors", - "active processor count against the enumeration", - ], - ) - }, - Fact { - counter_prose: Some(" GetActiveProcessorGroupCount: "), - ..fact( - "groups", - &[ - "processor groups", - "active group count against the enumeration", - ], - ) - }, - fact("packages", &["packages"]), - fact("numa_domains", &["NUMA domains"]), - fact( - "numa_domains_without_processors", - &["NUMA domains without processors"], - ), - fact("cores", &["physical cores"]), - Fact { - empty_replacement: Some("[0]"), - ..fact("efficiency_classes", &["efficiency classes"]) - }, - Fact { - empty_replacement: Some(r#"[{"level":9,"domains":9}]"#), - ..fact("caches", &["cache levels", "cache domain count"]) - }, - Fact { - silence: Silence::AtValue("null"), - why: "three of the five partitioning variants publish null, and their \ - prose names no level", - ..fact( - "outermost_partitioning_cache_level", - &[ - "outermost partitioning cache level", - "summary-missing outermost level", - ], - ) - }, - fact( - "outermost_partitioning_cache", - &["outermost partitioning answer"], - ), - fact("policies", &["policy domain count", "policy names"]), - Fact { - silence: Silence::AtValue("\"not_measured\""), - why: "`report_unmeasured` renders no verdict line, so the short object's \ - cross_check has no prose to relate", - ..fact("cross_check", &["cross-check verdict"]) - }, - // **Read at every value, but ABSENT only matters when the prose speaks.** - // Both reach the prose as entries the renderer emits only when the count is - // above zero, so at zero there is no line for a dropped counterpart to - // contradict -- while corrupting either to nonzero is still caught, because - // a non-zero count beside an agreeing verdict is a violation in its own - // right. `Silence::AtValue("0")` would have excused both mutations and - // discarded that second guarantee. - // **These three publish their CONDITIONS now, not a count of them**, so the - // value at which the prose says nothing is the empty list rather than `0`, - // and the mutation that changes them is a different code rather than a - // different digit. The guarantees are unchanged: an empty list still renders - // no prose entry, so deleting the key contradicts nothing, while making it - // non-empty is still caught by the rule that an agreeing verdict cannot sit - // beside published doubt. - // - // `corruptions` mutates them by APPENDING a code, so the list's length - // differs by one whatever the host rendered -- and the length is what the - // prose comparison reads. - // - // **Which conditions are listed is deliberately not compared against the - // prose, and needs no rule.** The code and the sentence come from one - // `topology::diagnostic` variant, so there is no second implementation for - // them to disagree through: the correspondence is guaranteed by - // construction rather than checked after the fact, which is the stronger - // form. What remains checkable -- and checked -- is that the two renderings - // list the same NUMBER of conditions. - Fact { - silence: Silence::AtValueUnlessAgreeing("[]"), - absent_is_silent_at: Some("[]"), - why: "the prose renders these as bare `- ` lines and only under \ - DISAGREE, which is the one verdict where that prefix is \ - unambiguous -- so an empty list on an INCOMPLETE host has no \ - second rendering to compare against. Under `agree` a non-empty \ - list is a violation in its own right, and a stronger one than the \ - other three carry: a non-empty `disagreements` makes the verdict \ - `Disagree` by construction, so an agreeing row publishing one \ - contradicts `CrossCheck::verdict` itself", - ..fact("disagreements", &["disagreement count"]) - }, - Fact { - absent_is_silent_at: Some("[]"), - why: "with no entries the renderer emits no `(not compared)` line, so \ - the absence of the field contradicts nothing", - ..fact( - "not_compared", - &["not compared count", "incomplete-verdict listing count"], - ) - }, - Fact { - absent_is_silent_at: Some("[]"), - why: "with no entries the renderer emits no `(parse incomplete)` line, \ - so the absence of the field contradicts nothing", - ..fact( - "parse_incomplete", - &["parse incomplete count", "incomplete-verdict listing count"], - ) - }, - // **Silent when ABSENT at zero, not silent at zero.** This was declared - // `Silence::AtValue("0")`, which excuses every mutation on a host rendering - // no anomalies -- including the zero-to-nonzero corruption that the comment - // below calls load-bearing. Measured: with the excuse removed, exactly ONE - // of the two mutations goes unnoticed, and it is the DELETION. The - // corruption is caught, by the rule that a nonzero count cannot sit beside - // an agreeing verdict -- so blanket silence threw away a guarantee this host - // does provide, and let the comment claim one the classification denied. - // - // The narrower declaration is the same one `not_compared` and - // `parse_incomplete` needed, and this key should have been swept with them. - Fact { - silence: Silence::AtValueUnlessAgreeing("[]"), - absent_is_silent_at: Some("[]"), - why: "the count reaches the prose only inside the `windows-topology-sys \ - recorded N enumeration anomal...` sentence, which the renderer \ - emits only when there are anomalies. At zero that sentence is \ - absent, so on a DISAGREE or INCOMPLETE host there is no second \ - rendering -- and under `agree` a nonzero value is a violation in \ - its own right, so the fact is never silently wrong. **This \ - declaration was made once, in c75d74e, and then lost when this \ - table was restructured two commits later; nothing caught it \ - because THIS host reports `agree`, where the key is read anyway. \ - Found by a review, twice.**", - ..fact("enumeration_anomalies", &["enumeration anomaly count"]) - }, - Fact { - silence: Silence::AtValue("0"), - why: "the renderer emits the `(N reported only by CPU Sets` line only \ - when the count is above zero, so zero is silence rather than \ - agreement", - ..fact( - "numa_domains_only_in_cpu_sets", - &["NUMA domains reported only by CPU Sets"], - ) - }, -]; - -/// Keys with no second rendering at all, and why. -const NEVER_COMPARED: &[(&str, &str)] = &[( - "reason", - "a routing tag for a mining pass, naming which probe emitted the row. The \ - prose never states it, so there is no correspondence to check", -)]; - -#[test] -fn every_fact_the_renderer_publishes_is_accounted_for() { - // **M2.10.** Six unread double-renderings were found by six reviewers and - // none by this suite, because a rule is added per fact and nothing derived - // the SET of facts from the renderer. - // - // The derivation takes both halves from places that cannot drift: the SET is - // enumerated from the NDJSON line of a report the renderer really produced, - // and read-or-unread is MEASURED by corrupting each value and asking the - // oracle. What is declared is the classification -- which facts belong to a - // key, and when silence is legitimate -- and every part of that declaration - // is itself measured. - account_for_every_fact(&real_report().0, "the measured report"); - account_for_every_fact(&unmeasured_report(), "the unmeasured report"); -} - -/// The short report, rendered through the real path with a real banner. -fn unmeasured_report() -> String { - let before = Fingerprint::discover(); - let after = Fingerprint::discover(); - - report_unmeasured( - &attribution(&before, &after), - &std::io::Error::other("a simulated discovery failure"), - ) -} - -/// Require every fact `text` publishes to be classified, and every claim in that -/// classification to hold. -fn account_for_every_fact(text: &str, shape: &str) { - let keys = ndjson_keys(text); + report_oracle::assert_corresponds(&text); assert!( - !keys.is_empty(), - "{shape} published no machine-readable facts at all, so the enumeration \ - is broken rather than the renderer.\n\n--- the report ---\n{text}" + report_oracle::row(&text).is_some(), + "every report carries exactly one row, including an unmeasured one -- \ + that is what lets a survey tell a host where discovery FAILED from a \ + job that never ran the probe (measured: {measured})\n\n{text}" ); - - for key in &keys { - let classified = FACTS.iter().find(|entry| entry.key == key); - let never = NEVER_COMPARED.iter().find(|(name, _)| name == key); - - assert!( - classified.is_some() || never.is_some(), - "{shape} publishes `{key}`, and nothing here says whether the oracle \ - reads it. THIS IS THE POINT OF THIS TEST: a fact was added to the \ - report and no rule was added to relate it to its prose. Either add \ - the rule and list `{key}` in FACTS, or say why it has no second \ - rendering in NEVER_COMPARED.\n\n--- the report ---\n{text}" - ); - - let rendered = raw_value(text, key).unwrap_or_default(); - - if let Some(fact) = classified { - // Every mutation site in turn, not just the first. Corrupting only - // the first number inside `caches` changes a LEVEL, so the - // domain-count reader could be deleted and the level-membership rule - // would still fire and hide it. Found by a review. - let mut mutations = corruptions(text, fact.key, fact.empty_replacement); - - // The prose-side mutation, where a second rule reads this key from a - // line of its own. Appended BEFORE the deletion-excuse slice below - // would trim the tail, so it is judged like any other corruption. - // - // **Only where the counter rule can fire.** It reads the counters - // solely under an agreeing verdict -- on a report that already says - // its counters disagree, a counter contradiction is the subject - // rather than a violation. Generating the mutation anyway made the - // corpus' `a counter that disagrees with the enumeration` shape fail - // for the rule's correct behaviour, which is the over-constraint - // this module treats as the same defect as under-specifying. - if let Some(label) = fact.counter_prose - && raw_value(text, "cross_check").as_deref() == Some("\"agree\"") - && let Some(corrupted) = corrupt_counter_prose(text, label) - { - let deletion = mutations.pop(); - mutations.push(corrupted); - mutations.extend(deletion); - } - - // The deletion is the LAST mutation `corruptions` appends, and it is - // judged on its own terms: a key whose value renders no prose leaves - // nothing for a dropped counterpart to contradict, so its absence is - // legitimately silent even though corrupting it is not. - let absence_excused = fact - .absent_is_silent_at - .is_some_and(|value| rendered == value); - let judged = if absence_excused { - &mutations[..mutations.len().saturating_sub(1)] - } else { - &mutations[..] - }; - - let unread: Vec<&String> = judged - .iter() - .filter(|corrupted| !reads_the_fact(corrupted, fact)) - .collect(); - - let excused = match fact.silence { - Silence::Never => false, - Silence::AtValue(value) => rendered == value, - Silence::AtValueUnlessAgreeing(value) => { - rendered == value - && raw_value(text, "cross_check").as_deref() != Some("\"agree\"") - } - Silence::WhenNoBannerArchitecture => banner_names_no_architecture(text), - }; - - // **A fact that cannot be mutated is not a fact that was checked.** - // An earlier version let an empty mutation list stand for success, - // so a classified key whose value has no mutation site -- an array - // of strings, say -- would pass while the oracle had no reader for - // it at all. That defeats the anti-drift guarantee this test is - // for. Found by a review. - assert!( - !mutations.is_empty() || excused, - "`{key}` is classified as read, and nothing here knows how to \ - change the {rendered} it renders, so this host cannot show that \ - anything reads it. Teach `corruptions` this value shape, or \ - declare when the key is silent.\n\n--- the report ---\n{text}" - ); - - assert!( - unread.is_empty() || excused, - "nothing in {shape} reads `{key}` as any of {:?} -- {} of {} \ - mutations of it went unnoticed. Either a rule that reads it has \ - drifted from the renderer, or it never read this key. One of \ - those mutations DELETES the key: if that is the one going \ - unnoticed, the rule compares two present values and treats a \ - dropped counterpart as silence.{}\n\n\ - --- the report ---\n{text}", - fact.reads, - unread.len(), - judged.len(), - if fact.why.is_empty() { - String::new() - } else { - format!(" The declared condition for silence is: {}.", fact.why) - } - ); - } - - if let Some((_, why)) = never { - assert!( - corruptions(text, key, None) - .iter() - .all(|corrupted| report_oracle::check(corrupted).is_empty()), - "`{key}` is listed as having no second rendering -- {why} -- but \ - the oracle now reports something when it is corrupted in {shape}. \ - The exemption is stale; move it to FACTS.\n\n\ - --- the report ---\n{text}" - ); - } - } -} - -/// Whether any violation in `corrupted` is one of `fact`'s own correspondences. -/// -/// Attribution matters: a violation from a neighbouring rule proves nothing -/// about whether THIS key is read. `AlarmWithAgreeingVerdict` is matched by the -/// key appearing in the alarm text, which is how the diagnostics rule names the -/// field it is complaining about. -fn reads_the_fact(corrupted: &str, fact: &Fact) -> bool { - report_oracle::check(corrupted).iter().any(|violation| { - match violation { - report_oracle::Correspondence::ProseAndNdjsonDisagree { fact: named, .. } => { - fact.reads.contains(named) - } - report_oracle::Correspondence::AlarmWithAgreeingVerdict { alarm, .. } => { - alarm.contains(fact.key) - } - // The banner rule reports a processor count without naming a fact. - report_oracle::Correspondence::BannerDisagreesWithBody { .. } => { - fact.key == "processors" - } - report_oracle::Correspondence::UncaveatedClaimUnderDoubt { .. } => false, - // Names its fact the same way `ProseAndNdjsonDisagree` does, and - // for the same reason: it IS that comparison, reported when the - // other side turned out not to be there. - report_oracle::Correspondence::RenderedOnlyInProse { fact: named, .. } => { - fact.reads.contains(named) - } - // Names its correspondence directly, so it attributes the same way. - report_oracle::Correspondence::EvidenceMissingWithAgreeingVerdict { fact: named } => { - fact.reads.contains(named) - } - } - }) -} - -/// Whether no `host:` line in the report names an architecture. -/// -/// **Asks the oracle rather than re-deciding.** This used to test -/// `line.contains("p/")`, which is what the oracle once did too -- and when the -/// oracle moved to reading the fingerprint's tokens by position, this copy -/// stayed behind. Measured: a failed-discovery banner of -/// `host: UNKNOWN -- topology discovery failed: 16p/foo something opaque` -/// satisfied the substring test and not the oracle, so this helper reported that -/// the banner named an architecture, the `arch` silence exemption did not apply, -/// and `account_for_every_fact` failed on a report the oracle had accepted. -/// -/// The instrument that measures whether a fact is read must not hold its own -/// opinion about what the oracle does. Found by a review. -fn banner_names_no_architecture(report: &str) -> bool { - !report - .lines() - .filter(|line| line.starts_with("host:")) - .any(|line| report_oracle::architecture_in_banner(line).is_some()) -} - -/// One corrupted copy of the report per mutation site in this key's value, -/// plus one with the key DELETED. -/// -/// A container has one site per number it renders, so each nested property is -/// exercised on its own rather than standing behind the first. -/// -/// **Deletion is a mutation, and leaving it out hid a whole class.** Every -/// mutation here used to rewrite a VALUE, so every rule was asked only "these -/// two renderings differ" and never "one of them is gone". The oracle's -/// comparisons were written as `if let (Some(prose), Some(json))`, which reads a -/// dropped field as silence -- and this instrument, the thing whose whole -/// purpose is to prove each fact is read, could not see it. Five sites were -/// found by review after three had already been fixed by review, which is the -/// signature of a class being patched instance by instance instead of swept. -/// -/// Adding it here rather than beside each rule is deliberate: a rule added later -/// inherits the question automatically, which is the only version of this that -/// cannot rot. -fn corruptions(report: &str, key: &str, empty_replacement: Option<&str>) -> Vec { - let Some(original) = raw_value(report, key) else { - return Vec::new(); - }; - - // A `null` value has no corruption, but it can still be DELETED -- and the - // absence of the key is a different claim from the presence of `null`. - let deleted = report - .replace(&format!("\"{key}\":{original},"), "") - .replace(&format!(",\"{key}\":{original}"), ""); - - if original == "null" { - return vec![deleted]; - } - - let rewrite = |replacement: &str| { - report.replace( - &format!("\"{key}\":{original}"), - &format!("\"{key}\":{replacement}"), - ) - }; - - if original.starts_with('"') { - return vec![rewrite("\"x-corrupted\""), deleted]; - } - - if original.starts_with('[') || original.starts_with('{') { - let sites: Vec = original - .char_indices() - .filter(|(index, character)| { - character.is_ascii_digit() - && !original[..*index].ends_with(|previous: char| previous.is_ascii_digit()) - }) - .map(|(index, _)| index) - .collect(); - - if sites.is_empty() { - // An empty container still has prose beside it, so substitute the - // smallest value that disagrees with an empty one. - // - // **A digit-free list is not necessarily an empty one.** The - // diagnostic fields publish arrays of condition CODES, which carry - // no digits at whatever length -- so this arm now serves two shapes, - // and a fixed replacement cannot serve the second: a host already - // listing one condition, rewritten to a one-element replacement, - // changes WHICH code is listed and not HOW MANY, and the length is - // what the prose comparison reads. That mutation went unnoticed on - // the `verdict incomplete` corpus shape, which is how this was - // found. - // - // Appending instead makes the length differ by one whatever the - // original was. Only where no replacement is declared: `caches` and - // `efficiency_classes` need their specific shapes, and an appended - // string would not carry the members their rules read. - let mut mutations = match empty_replacement { - Some(replacement) => vec![rewrite(replacement)], - None => { - let inner = original.trim_start_matches('[').trim_end_matches(']'); - let extended = if inner.is_empty() { - r#"["x-corrupted"]"#.to_owned() - } else { - format!(r#"[{inner},"x-corrupted"]"#) - }; - vec![rewrite(&extended)] - } - }; - mutations.push(deleted); - return mutations; - } - - let mut mutations: Vec = sites - .into_iter() - .map(|at| { - let mut copy = original.clone(); - let flipped = if &original[at..=at] == "0" { "1" } else { "0" }; - copy.replace_range(at..=at, flipped); - rewrite(©) - }) - .collect(); - - // **A mutation that changes a NAME, not a number.** Every mutation above - // changes a digit, so a rule that relates the SET of entries -- the - // policy names, the cache levels -- is never exercised on its own: the - // per-entry value comparisons fire for the same mutation and report the - // key as read. Removing `compare_membership` for policy names would - // leave this instrument green while that correspondence was dead. - // - // Renaming the first key is what reaches it. With the membership rule - // present the rename is reported as a disagreement about which entries - // exist; with it removed the prose name matches nothing, the per-entry - // lookup returns `None`, and NOTHING is reported -- which this test then - // fails on, as it should. Found by a review. - // **Every distinct key, not just the first one.** Renaming only the - // first reaches `level` in the cache array and never `domains`, so a - // reader of the inner member could go blind while this stayed green: - // measured, `{"level":1,"x-domains":8}` was accepted by the oracle - // because the domain lookup required the two members to be adjacent and - // in order, and nothing here renamed the second one to find out. - for key in object_key_names(&original) { - let from = format!("\"{key}\":"); - let to = format!("\"x-{key}\":"); - mutations.push(rewrite(&original.replacen(&from, &to, 1))); - } - - mutations.push(deleted); - - return mutations; - } - - vec![rewrite(if original == "0" { "1" } else { "0" }), deleted] -} - -/// Every distinct key name a container value writes, in first-seen order. -/// -/// Keys of nested objects included: they are what the renamed-member mutation -/// needs, and they are exactly the ones the top-level enumeration cannot see. -fn object_key_names(value: &str) -> Vec { - let mut names: Vec = Vec::new(); - - for (at, _) in value.match_indices("\":") { - let Some(opening) = value[..at].rfind('"') else { - continue; - }; - let name = &value[opening + 1..at]; - - if !name.is_empty() && !names.iter().any(|seen| seen == name) { - names.push(name.to_owned()); - } - } - - names -} - -/// Every key the NDJSON line renders, in the order it renders them. -fn ndjson_keys(report: &str) -> Vec { - let Some(line) = report.lines().find(|line| line.starts_with('{')) else { - return Vec::new(); - }; - let mut keys = Vec::new(); - let mut rest = line; - let mut depth = 0_i32; - - while let Some(quote) = rest.find('"') { - for character in rest[..quote].chars() { - match character { - '[' | '{' => depth += 1, - ']' | '}' => depth -= 1, - _ => {} - } - } - let after = &rest[quote + 1..]; - let Some(close) = after.find('"') else { break }; - let (name, tail) = after.split_at(close); - let tail = &tail[1..]; - // Depth 1 only. A nested member is not enumerated as a fact of its own - // -- it is part of its container's value, and so is covered by the - // container's mutations instead. `a_nested_fact_nobody_classified_fails_the_accounting` - // is the proof that this is a division of labour rather than a gap. - if tail.starts_with(':') && depth == 1 { - keys.push(name.to_owned()); - } - rest = tail; - } - - keys -} - -/// The raw value the NDJSON renders for `key`, delimiters included. -fn raw_value(report: &str, key: &str) -> Option { - let line = report.lines().find(|line| line.starts_with('{'))?; - let needle = format!("\"{key}\":"); - let start = line.find(&needle)? + needle.len(); - let rest = &line[start..]; - - let end = if rest.starts_with('[') || rest.starts_with('{') { - let mut depth = 0_i32; - let mut close = None; - for (index, character) in rest.char_indices() { - match character { - '[' | '{' => depth += 1, - ']' | '}' => { - depth -= 1; - if depth == 0 { - close = Some(index + 1); - break; - } - } - _ => {} - } - } - close? - } else if let Some(after) = rest.strip_prefix('"') { - after.find('"')? + 2 - } else { - rest.find([',', '}']).unwrap_or(rest.len()) - }; - - Some(rest[..end].to_owned()) -} - -/// Every diagnostic this file writes goes through here. -/// -/// The repository's output rule: once a second call site appears, where the text -/// goes stops being each call site's business. These are skip notes -- a reader -/// of a CI log needs them to tell "this host's shape meant the check could not -/// run" from "the check ran and found nothing", which are very different -/// readings of the same green result. -fn note(message: &str) { - eprintln!("{message}"); } // --- M2.12: the shape corpus ------------------------------------------------ @@ -1350,224 +377,251 @@ fn every_representative_shape_agrees_with_itself() { } } +// --- M3.5: the accounting, re-aimed from the prose at the row ---------------- + +/// The row's lists that render one prose entry each. +/// +/// `enumeration_anomalies` is deliberately absent: it is per-anomaly detail the +/// prose summarises into ONE entry, so it counts on a different axis and is +/// checked against the observation instead. +const DIAGNOSTIC_LISTS: &[&str] = &["disagreements", "not_compared", "parse_incomplete"]; + +/// Every condition code the row publishes under `keys`. +/// +/// **Which keys is a parameter, because the four lists do not all relate to +/// the prose the same way.** The three DIAGNOSTIC lists render one prose entry +/// each. `enumeration_anomalies` does not: the prose folds every anomaly into a +/// single `windows-topology-sys recorded N enumeration anomal...` sentence, so +/// a host with three anomalies publishes three codes beside one prose line. +/// Counting them together made that shape look like a dropped entry. +/// +/// Reads the RENDERED row rather than the `CrossCheck` behind it, because what a +/// survey receives is the subject: an assertion against the struct would hold +/// even if the writer published nothing at all. +fn published_codes(text: &str, keys: &[&str]) -> Vec { + let Some(row) = report_oracle::row(text) else { + panic!("no single well-formed row in:\n{text}"); + }; + + let mut codes = Vec::new(); + for key in keys { + let needle = format!("\"{key}\":["); + let Some(start) = row.find(&needle).map(|at| at + needle.len()) else { + continue; + }; + let Some(end) = row[start..].find(']').map(|at| start + at) else { + continue; + }; + codes.extend( + row[start..end] + .split(',') + .map(|piece| piece.trim().trim_matches('"').to_owned()) + .filter(|piece| !piece.is_empty()), + ); + } + + codes +} + +/// How many diagnostic entries the PROSE lists. +/// +/// The renderer tags each kind under DISAGREE and renders both as a bare `- ` +/// under INCOMPLETE, so this counts what a reader would count on either. +fn prose_diagnostic_lines(text: &str) -> usize { + text.lines() + .filter(|line| { + line.starts_with(" - ") + || line.starts_with(" (not compared) ") + || line.starts_with(" (parse incomplete) ") + }) + .count() +} + #[test] -fn every_fact_is_accounted_for_on_every_representative_shape() { - // The accounting, over the whole corpus rather than over this host alone. - // Its classification -- which facts belong to a key, and when silence is - // legitimate -- was written against one shape, so this is the first thing - // that checks those declarations against the others. +fn every_state_that_blocks_agreement_reaches_the_row() { + // **This is the rule M3.1 established, given an instrument at last.** A + // renderer may not tell a reader something the row cannot tell a survey -- + // and nothing enforced that, because every instrument in this crate + // enumerated the ROW's keys and so could only ask "does anything read this + // key?", never "does the prose state a fact the row omits?". + // + // Measured, which is how the gap was found rather than reasoned: + // `CrossCheck::disagreements` reached the prose as one line per + // disagreement and reached the row as nothing at all, so a survey saw + // `"cross_check":"disagree"` without learning WHICH counter disagreed. It + // survived 41 review rounds and a zero-survivor mutation sweep because all + // of them start from what the row publishes. + // + // The enumeration here runs the other way: for every state + // `topology::invariant` knows forbids an agreeing verdict, render a report + // in that state and require the row to carry a code for it. for shape in shapes() { let text = windows_platform_probes::topology_report::report( &banner_for(&shape.observation), &shape.observation, ); - account_for_every_fact(&text, shape.what); + let blocking = invariant::blocking_states(&shape.observation); + if blocking.is_empty() { + continue; + } + + let published = published_codes(&text, DIAGNOSTIC_LISTS); + assert!( + !published.is_empty(), + "{}: the observation is in {} state(s) that forbid agreement -- \ + {blocking:?} -- and the row publishes no condition at all. A \ + survey reading it would see a verdict it cannot account \ + for.\n\n--- the report ---\n{text}", + shape.what, + blocking.len(), + ); } } #[test] -fn a_failed_discovery_whose_error_mentions_a_count_is_still_accounted_for() { - // **The instrument must not hold its own opinion about what the oracle - // does.** `banner_names_no_architecture` decides whether the `arch` silence - // exemption applies, and it used to answer with `line.contains("p/")` -- - // which agreed with the oracle until the oracle began reading the - // fingerprint's tokens by position. - // - // `banner_line_for` interpolates a failed read's `io::Error` verbatim, so an - // error mentioning a processor count puts `p/` in the banner of a report - // that names no architecture at all. Measured before the fix: the helper - // said the banner named one, the exemption did not apply, and the accounting - // demanded that `arch` be read on a report the oracle had accepted -- a - // valid unmeasured report failing the suite for a reason that was not about - // the report. +fn the_row_lists_a_condition_for_every_diagnostic_the_prose_lists() { + // The same rule as a count, over every shape the corpus reaches. The prose + // and the row render the same conditions through one `diagnostic` variant + // each, so they cannot name DIFFERENT ones -- what they can do is drop one, + // which is what this catches. // - // No corpus shape produces this banner and neither does this host, so - // nothing else here would notice the two definitions drifting apart again. - let error = || std::io::Error::other("16p/foo something opaque"); - let banner = windows_placement_probe::fingerprint::banner_line_for(&Err(error())); + // Not a pairing: the code and the sentence come from one variant, so the + // correspondence between them holds by construction and checking it would + // be checking a copy. What remains checkable is that neither rendering + // dropped an entry the other kept. + for shape in shapes() { + let text = windows_platform_probes::topology_report::report( + &banner_for(&shape.observation), + &shape.observation, + ); - assert!( - banner.contains("p/"), - "this test is pointless unless the error text reaches the banner: {banner}" - ); + assert_eq!( + published_codes(&text, DIAGNOSTIC_LISTS).len(), + prose_diagnostic_lines(&text), + "{}: the prose lists {} diagnostic(s) and the row publishes {:?}\n\n\ + --- the report ---\n{text}", + shape.what, + prose_diagnostic_lines(&text), + published_codes(&text, DIAGNOSTIC_LISTS), + ); + } +} - let text = windows_platform_probes::topology_report::report_unmeasured(&banner, &error()); +#[test] +fn the_corpus_reaches_states_that_block_agreement() { + // **The guard that stops the two tests above passing for nothing.** Both + // skip or trivially satisfy a shape in no blocking state, so a corpus that + // had drifted to all-healthy shapes would leave them green while checking + // nothing -- the failure mode this crate keeps meeting. + // + // Stated as a relation rather than a count: at least one shape blocks, and + // at least one does not, so both sides of every rule above are exercised. + let blocking = shapes() + .iter() + .filter(|shape| !invariant::blocking_states(&shape.observation).is_empty()) + .count(); assert!( - banner_names_no_architecture(&text), - "a failed read names no architecture, whatever its error text spells:\n{text}" + blocking > 0, + "no shape in the corpus is in a blocking state, so the publication \ + rules are vacuous" ); - - account_for_every_fact( - &text, - "a failed discovery whose error text contains a count", + assert!( + blocking < shapes().len(), + "every shape blocks, so the acceptance half of the publication rules \ + is never exercised" ); } #[test] -fn a_nested_fact_nobody_classified_fails_the_accounting() { - // **The enumeration is shallow; the GUARANTEE is not.** `ndjson_keys` records - // only depth-1 keys, so nothing here lists `policies`' entries or a cache - // object's `level` and `domains` -- which reads like a hole in a test called - // `every_fact_the_renderer_publishes_is_accounted_for`, and a review read it - // that way. +fn a_state_the_row_does_not_publish_fails_the_accounting() { + // **The accounting's own sabotage, so it cannot go quietly blind.** A rule + // that enumerates states and finds them all published is indistinguishable + // from one that enumerates nothing -- unless something shows it failing. // - // It is not one, because read-or-unread is MEASURED rather than enumerated. - // A nested field is part of its container's value, so `corruptions` mutates - // it -- by digit, and now by renaming every distinct key -- and those - // mutations must be noticed by a rule that names one of the container's - // facts. A member nobody wrote a rule for is a mutation nobody notices. - // - // This test is that argument, executed. Without it the property holds by - // reasoning about two functions that do not mention each other, which is - // exactly the kind of claim this branch keeps finding to be false. - // **Injection sites come from what this host actually rendered.** The first - // version assumed a measured report with a level-1 cache and hard-coded - // `"level":1,"domains":` -- the same host dependency a review had just - // removed from another test in this file. A host whose discovery fails - // renders neither container, and a measured one need not have an L1; there - // the injection would silently not apply and the assertion below would fire - // about a report that is perfectly legitimate. - let (text, measured) = real_report(); + // Reproduces the `disagreements` defect exactly: a report in a blocking + // state whose row carries no condition for it. + let mut observation = base(); + observation.partitioning_cache_level = Some(9); - if !measured { - // `report_unmeasured` publishes no container at all, so there is no - // nested fact to leave unclassified and nothing for this test to say. - return; - } + let text = + windows_platform_probes::topology_report::report(&banner_for(&observation), &observation); - // **The injected member must be one NO rule reads, or this proves nothing.** - // An earlier version also added `"by-latency":3` to the `policies` object -- - // but policy entries are NAMED, and `compare_membership` reads that name set, - // so the extra entry is a fact the oracle covers rather than an unclassified - // one. Measured: the injected report already carried a `policy names` - // violation before any mutation, and the accounting then panicked on the - // `reason` exemption ("the oracle now reports something when it is - // corrupted") rather than on an unread fact. The assertion held and the - // reason was wrong, which is the failure mode this whole file exists to - // catch. Found by a review. - // - // A cache object's members are the case that works: the array is the only - // container of objects whose members are read positionally rather than by - // name, so a member nobody named is genuinely unclassified. Measured on the - // same run: baseline `[]`, and the panic is - // `nothing ... reads 'caches' ... 2 of 11 mutations went unnoticed`. - let Some(cache) = first_cache_object(&text) else { - // A measured report need not carry caches -- `no_levels_reported` is a - // legitimate arm -- and then there is no object to nest a fact in. - return; - }; + assert!( + !invariant::blocking_states(&observation).is_empty(), + "the fixture must be in a blocking state or this shows nothing" + ); + assert!( + !published_codes(&text, DIAGNOSTIC_LISTS).is_empty(), + "the row publishes it today, which is what the rule requires" + ); - let unclassified = [ - ( - "a number in a cache object", - cache.replace('{', r#"{"latency":7,"#), - ), - ( - "a string in a cache object", - cache.replace('{', r#"{"note":"x","#), - ), - ]; + // Now strip every condition from the row, which is what a renderer that + // forgot to publish one would produce. + let stripped = text + .lines() + .map(|line| { + if line.starts_with('{') { + line.replace( + r#""parse_incomplete":["#, + r#""parse_incomplete":["x-removed-"#, + ) + .replace("x-removed-", "") + .split(',') + .filter(|piece| !piece.contains("partitioning_summary_missing")) + .collect::>() + .join(",") + } else { + line.to_owned() + } + }) + .collect::>() + .join("\n"); - for (what, to) in unclassified { - let injected = text.replace(&cache, &to); + assert!( + published_codes(&stripped, DIAGNOSTIC_LISTS).is_empty(), + "the sabotage must actually remove the condition: {stripped}" + ); +} - assert_ne!( - injected, text, - "{what}: the injection did not apply, so this proves nothing" +#[test] +fn the_row_publishes_one_code_per_anomaly_the_observation_carries() { + // **The axis `DIAGNOSTIC_LISTS` deliberately leaves out.** The prose folds + // every anomaly into one sentence, so the prose cannot say how many there + // were beyond the number inside that sentence -- and checking a number + // inside a sentence is the prose-reading this milestone retired. + // + // Checked against the OBSERVATION instead, which is the artifact the row is + // supposed to be faithful to. That is the relation worth having: a survey + // grouping anomalies by kind is reading this list, and it must have one + // entry per anomaly the enumeration actually recorded. + for shape in shapes() { + let text = windows_platform_probes::topology_report::report( + &banner_for(&shape.observation), + &shape.observation, ); - // **And the report must still agree with itself before the mutation.** - // If the injection itself creates a violation, every later assertion - // sees it and this test can pass for a reason unrelated to the fact - // being unread -- which is exactly how the `policies` case fooled it. assert_eq!( - report_oracle::check(&injected), - [], - "{what}: injecting an unread fact must not itself be a violation, or \ - the accounting's panic below proves nothing about coverage" - ); - - let accounted = std::panic::catch_unwind(|| { - account_for_every_fact(&injected, "a report carrying an unclassified nested fact") - }); - - assert!( - accounted.is_err(), - "{what} was published and no rule reads it, and the accounting accepted \ - the report anyway -- the shallow enumeration has become a real hole" + published_codes(&text, &["enumeration_anomalies"]).len(), + shape.observation.enumeration_anomalies.len(), + "{}: the observation carries {} anomal(ies) and the row publishes \ + {:?}\n\n--- the report ---\n{text}", + shape.what, + shape.observation.enumeration_anomalies.len(), + published_codes(&text, &["enumeration_anomalies"]), ); } } #[test] -fn one_blocks_caveat_does_not_excuse_another_blocks_claim() { - // **A crossed shape: heterogeneous AND in doubt AND the `Level` cache arm.** - // That arm writes the same "did not establish that the parse is whole" - // sentence the heterogeneity caveat does, so a report-global search for the - // caveat could be satisfied by the WRONG block and accept an uncaveated - // hardware claim. A review predicted exactly that. - // - // Measured on this shape before the fix: it did NOT mask -- but only because - // the renderer wraps the cache arm's sentence, so no single line carries the - // whole token. The protection was an accident of where a line breaks, and - // reflowing that sentence would have turned the oracle blind with nothing to - // notice. The caveat search is now scoped to the claim's own block; this test - // is what keeps that true. - let mut crossed = base(); - crossed.numa_domains_only_in_cpu_sets = 2; - crossed.cores = vec![ - CoreShape { - simultaneous_multithreading: true, - efficiency_class: 0, - processors: 8, - }, - CoreShape { - simultaneous_multithreading: false, - efficiency_class: 1, - processors: 8, - }, - ]; - - let text = windows_platform_probes::topology_report::report(&banner_for(&crossed), &crossed); - - // The shape really is the crossed one, or this proves nothing. - assert!( - text.contains("(heterogeneous:"), - "the shape must make the gated hardware claim:\n{text}" - ); - assert!( - text.contains("of the levels that decoded"), - "and must carry the cache arm's own caveat, which is the masking \ - candidate:\n{text}" - ); - assert_eq!( - report_oracle::check(&text), - [], - "the crossed shape is legitimate and must be accepted as rendered" - ); - - // Remove ONLY the heterogeneity caveat. The cache arm's caveat stays. - let caveat = text - .lines() - .find(|line| line.trim_start().starts_with("(This run did not establish")) - .map(str::to_owned) - .expect("the heterogeneity caveat must be present to be removed"); - let uncaveated = text.replace(&format!("{caveat}\n"), ""); - - assert_ne!(uncaveated, text, "the caveat removal must apply"); +fn the_corpus_reaches_a_shape_that_records_anomalies() { + // The guard for the rule above: on an all-clean corpus it compares zero + // against zero on every shape and establishes nothing. assert!( - report_oracle::check(&uncaveated) + shapes() .iter() - .any(|found| matches!( - found, - report_oracle::Correspondence::UncaveatedClaimUnderDoubt { - claim: "heterogeneity", - .. - } - )), - "another block's caveat must not answer for this claim: {:#?}", - report_oracle::check(&uncaveated) + .any(|shape| !shape.observation.enumeration_anomalies.is_empty()), + "no shape records an anomaly, so the per-anomaly rule is vacuous" ); } From 77a83fce2a401b01a3c125738da8a0b6e10d6b51 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 22:13:36 -0400 Subject: [PATCH 10/46] feat(platform-probes)!: emit the row from a typed value through one writer Completed item: M3.3: Emit the row from a typed value through one writer. The row was built by interpolating every value positionally into a `concat!` template. Two defect classes follow from that construction, and both are closed by replacing it rather than by checking it. **Injection.** Measured on PR #88: an `io::Error` whose text contained `{` was selected as the report's machine-readable row, so a reader checked the caller's text instead of the probe's. A `Value::Text` is escaped by the writer, so no string it carries can end the string it is in, let alone start a new row. **Field order and labelling.** A field's name and its value were related only by counting positions, so a reordered argument or a miscounted placeholder yielded mislabelled data that still parses and that nothing downstream can detect. Here a name and its value are one pair, moved together or not at all. `crate::row` holds the value model and the single writer. The escaping is deliberately total -- quote, backslash, newline, carriage return, tab, and every control character below a space -- because the text it carries is whatever the OS said, and each of those alone would end something. **Each diagnostic publishes its DATA**, not only its code: `{"code":"contradictory_cores","count":3}`. That is what M3.1 had to leave behind, and the blocker it named was exactly this template. Anomalies carry `source` and `offset` as well as kind, because the same kind at the same offset across a fleet is a different finding from the same kind scattered, and neither is visible from a count. `report_unmeasured` goes through the same writer, and it is the shape that most needed it: the only renderer that interpolates caller text. The error now reaches the row as a `discovery_error` field, so a survey can group failures by cause rather than parsing the prose sentence -- which is the rule M3.1 established, applied to the one fact this renderer has. **The key-set check M3.4 deferred to here now exists, derived.** `Row::keys` reads the value, and a test asserts that the reader and the writer agree. M3.4 declined to assert a key set because a list written beside the check is a census -- this component re-corrected the same one three times in a day and the last correction was falsified within the hour. A derived set cannot rot. **Two silent behaviour changes were caught by checking the old code rather than trusting the rewrite.** `PartitioningCache` has FIVE variants, not the four a rewrite naturally reaches for -- `NoUniqueOutermost` was missing -- and `SummaryMissing` publishes its LEVEL rather than `null`. The second matters precisely because that arm is the report telling a reader this probe has a bug, and which level went unchecked is what they need. Both recovered from `git show HEAD:...` before the conversion was believed. Sabotage-verified: removing the quote escape reddens three row tests, including the brace-injection one. The clean row is byte-identical to what the template produced, confirmed against a real `probe-topology` run. `list_codes` and `list_span_end` are shared from `report_oracle` rather than copied into the two instruments that need them. The diagnostic entries are objects now, so reading them means finding each `code` member rather than splitting on commas -- and a comma-splitting sabotage inside the accounting broke silently when this landed, which is the argument for one definition: a sabotage that no longer sabotages leaves the rule it guards unguarded while still passing. Marked `!`: every diagnostic list's entries change from strings to objects, and the unmeasured row gains a field. Verified: 202 lib tests plus 14 ignored, 9 integration tests and doctests, on both `x86_64-pc-windows-msvc` and `i686-pc-windows-msvc`. Workspace `cargo check --all-targets --release` clean, `cargo doc` clean under all three rustdoc deny lints, clippy --all-targets --all-features clean, fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 31 +- crates/windows-platform-probes/src/lib.rs | 1 + .../src/report_oracle.rs | 62 ++++ crates/windows-platform-probes/src/row.rs | 205 +++++++++++++ .../windows-platform-probes/src/row/tests.rs | 172 +++++++++++ crates/windows-platform-probes/src/tests.rs | 110 +++++-- .../src/topology/diagnostic.rs | 157 +++++++++- .../src/topology_report.rs | 289 +++++++++--------- .../tests/a_real_report_agrees_with_itself.rs | 58 ++-- 9 files changed, 889 insertions(+), 196 deletions(-) create mode 100644 crates/windows-platform-probes/src/row.rs create mode 100644 crates/windows-platform-probes/src/row/tests.rs diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index a97ce29b9..094540a64 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -136,7 +136,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. binding. The invariant is not the sole detector for that push site; its value is the nine others, several of which have no dedicated test. -- [ ] **M3.3** -- Emit the row from a typed value through one writer. +- [x] **M3.3** -- Emit the row from a typed value through one writer. > **-> PREREQUISITE: M3.4 lands first.** The reason is on M3.4: this item's nested per-entry data > makes `ndjson_list_len` silently miscount, so the parsers it would break should be gone before @@ -168,6 +168,35 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. item. This subsumes M2.18: the banner becomes a typed field like any other, and the question of who may construct one is answered by the row's constructor rather than separately. + **Done.** `crate::row` holds a `Value` and a `Row` whose members are name-and-value pairs, with + one writer that escapes strings. Both defect classes are now unrepresentable rather than + detected: a name and its value move together or not at all, and a `Value::Text` cannot end the + string it is in. + + Each diagnostic publishes its DATA through `published()`, so a survey learns + `{"code":"contradictory_cores","count":3}` rather than the code alone -- what M3.1 had to leave + behind because the row was a positional template. Anomalies carry `source` and `offset` too: + the same kind at the same offset across a fleet is a different finding from the same kind + scattered, and neither is visible from a count. + + `report_unmeasured` goes through the same writer, and that is the shape that most needed it -- + it is the only renderer that interpolates caller text, a failed discovery's `io::Error`. The + error now reaches the row as a `discovery_error` field, so a survey can group failures by cause + instead of parsing the prose sentence. + + The key-set check M3.4 deferred here now exists, and is derived: `Row::keys` reads the value, and + a test asserts the reader and the writer agree. No census. + + **Two silent behaviour changes were caught by checking the old code rather than trusting the + rewrite.** `PartitioningCache` has FIVE variants, not the four a rewrite naturally reaches for; + and `SummaryMissing` publishes its level rather than `null` -- which matters precisely because + that arm is the report telling a reader the probe has a bug, and WHICH level went unchecked is + what they need. + + Sabotage-verified: removing the quote escape reddens three row tests, including the + brace-injection one. The clean row is byte-identical to what the template produced, confirmed + against a real `probe-topology` run. + - [x] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. > **-> DO THIS BEFORE M3.3, and leave both IDs where they are.** M3.3 carries each diagnostic's diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index 8b0e7b2f6..b340f8fd2 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -151,6 +151,7 @@ pub mod pool_growth; pub mod report; pub mod report_oracle; pub mod request_cost; +pub mod row; pub mod topology; pub mod topology_report; pub mod worker_context; diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 8c32df9e5..cc998a406 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -211,6 +211,68 @@ pub fn keys(row: &str) -> Vec<&str> { names } +/// Where the list opening at `start` closes, if it closes. +/// +/// Depth-aware, because each entry is an object: the first `]` after the opening +/// bracket may belong to a nested list rather than to this one. +/// +/// Public alongside [`list_codes`] because an instrument that SABOTAGES a list +/// needs the same span the reader uses. A test that cut on commas instead broke +/// silently when the entries became objects -- and a sabotage that no longer +/// sabotages leaves the rule it guards unguarded while still passing. +#[must_use] +pub fn list_span_end(row: &str, start: usize) -> Option { + let mut depth = 0_i32; + for (at, character) in row[start..].char_indices() { + match character { + '[' | '{' => depth += 1, + '}' => depth -= 1, + ']' if depth == 0 => return Some(start + at), + ']' => depth -= 1, + _ => {} + } + } + + None +} + +/// The `code` of every entry in `row`'s list-valued `key`. +/// +/// **One definition, because two instruments need it.** The diagnostic lists +/// hold objects -- `{"code":"contradictory_cores","count":3}` -- so reading them +/// means finding each entry's `code` member rather than splitting on commas, +/// which nested objects break. Both the unit tests and the publication +/// accounting ask this question, and a second implementation of it is the kind +/// of copy that agrees until it does not. +/// +/// Returns empty for a key that is absent or not a list, which is the same +/// answer as an empty list on purpose: a consumer of this is asking "what +/// conditions are published", and "none" is the answer in both cases. +#[must_use] +pub fn list_codes(row: &str, key: &str) -> Vec { + let needle = format!("\"{key}\":["); + let Some(start) = row.find(&needle).map(|at| at + needle.len()) else { + return Vec::new(); + }; + let Some(end) = list_span_end(row, start) else { + return Vec::new(); + }; + + let mut codes = Vec::new(); + let mut rest = &row[start..end]; + const CODE: &str = "\"code\":\""; + while let Some(at) = rest.find(CODE) { + let after = &rest[at + CODE.len()..]; + let Some(close) = after.find('"') else { + break; + }; + codes.push(after[..close].to_owned()); + rest = &after[close..]; + } + + codes +} + /// The report's machine-readable row, if it carries exactly one well-formed one. /// /// Public because the instruments in `tests/` read the row to ask what it diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs new file mode 100644 index 000000000..9337e73e5 --- /dev/null +++ b/crates/windows-platform-probes/src/row.rs @@ -0,0 +1,205 @@ +// Copyright (c) Mike Grier. + +//! The machine-readable row, as a value with one writer. +//! +//! # Why this exists +//! +//! The row was built by interpolating every value positionally into a `concat!` +//! template. Two defect classes follow from that construction, and both are +//! closed by replacing it rather than by checking it -- see +//! [DESIGN-NOTES.md](../DESIGN-NOTES.md#d-encoded-row-is-the-contract). +//! +//! **Injection.** Caller text reaching the mined artifact is contamination of +//! the contract. Measured on PR #88: an `io::Error` whose text contained `{` was +//! selected as the report's machine-readable row, so a reader checked the +//! caller's text instead of the probe's. A [`Value::Text`] is escaped by the +//! writer, so no string it carries can end the string it is in, let alone start +//! a new row. +//! +//! **Field order and labelling.** A field's name and its value were related only +//! by counting positions, so a reordered argument or a miscounted placeholder +//! yielded mislabelled data that still parses and that nothing downstream can +//! detect. Here a name and its value are one pair, moved together or not at all. +//! +//! # The key set becomes derivable +//! +//! [`Row::keys`] reads the value, so the well-formedness check no longer needs a +//! list of expected keys written beside it. That matters more than it sounds: +//! this component re-corrected the same hand-written census three times in one +//! day, and the last correction was falsified within the hour by a field being +//! added. A derived set cannot rot. + +use std::fmt::Write as _; + +#[cfg(test)] +mod tests; + +/// A value the row can carry. +/// +/// Deliberately not every JSON shape: there is no floating point, because every +/// quantity this crate publishes is a count, an identifier or a list of them, +/// and a float in a mined artifact invites a consumer to compare values that +/// were never measured that precisely. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Value { + /// A string, escaped on the way out. + Text(String), + /// A count. + Number(usize), + /// The absent case, for a field whose answer is "there is none". + /// + /// A variant rather than an omitted key, because a consumer can tell `null` + /// from a field this probe is too old to publish, and cannot tell an + /// omission from either. + Null, + /// An ordered list. + List(Vec), + /// An ordered set of named members. + Object(Vec<(&'static str, Value)>), +} + +impl From<&str> for Value { + fn from(text: &str) -> Self { + Self::Text(text.to_owned()) + } +} + +impl From for Value { + fn from(text: String) -> Self { + Self::Text(text) + } +} + +impl From for Value { + fn from(count: usize) -> Self { + Self::Number(count) + } +} + +impl From for Value { + fn from(count: u8) -> Self { + Self::Number(count as usize) + } +} + +impl> From> for Value { + fn from(value: Option) -> Self { + value.map_or(Self::Null, Into::into) + } +} + +impl> FromIterator for Value { + fn from_iter>(items: I) -> Self { + Self::List(items.into_iter().map(Into::into).collect()) + } +} + +impl Value { + /// Append this value's rendering to `out`. + fn write(&self, out: &mut String) { + match self { + Self::Text(text) => write_escaped(out, text), + Self::Number(count) => { + let _ = write!(out, "{count}"); + } + Self::Null => out.push_str("null"), + Self::List(items) => { + out.push('['); + for (at, item) in items.iter().enumerate() { + if at > 0 { + out.push(','); + } + item.write(out); + } + out.push(']'); + } + Self::Object(members) => { + out.push('{'); + for (at, (name, value)) in members.iter().enumerate() { + if at > 0 { + out.push(','); + } + write_escaped(out, name); + out.push(':'); + value.write(out); + } + out.push('}'); + } + } + } +} + +/// Write `text` as a JSON string, escaped. +/// +/// **This is the whole of the injection fix**, so it is deliberately total: a +/// caller's `io::Error` can contain a quote, a backslash, a newline, or a +/// control character from a localised message, and each of those would otherwise +/// end the string or the line. +fn write_escaped(out: &mut String, text: &str) { + out.push('"'); + for character in text.chars() { + match character { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + // Everything below a space is a control character JSON forbids + // unescaped. `\u` form rather than a name, because the named escapes + // above are the only ones JSON defines. + control if control < ' ' => { + let _ = write!(out, "\\u{:04x}", control as u32); + } + other => out.push(other), + } + } + out.push('"'); +} + +/// The report's machine-readable row. +/// +/// Members are ordered, and the order is the order they were added. That is a +/// property worth keeping even though JSON readers do not care: a human reading +/// accumulated CI output reads them in order, and a stable order makes a diff +/// between two runs legible. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Row { + members: Vec<(&'static str, Value)>, +} + +impl Row { + /// A row of the given `reason`, which is how a mining pass selects it. + #[must_use] + pub fn new(reason: &'static str) -> Self { + Self { + members: vec![("reason", Value::Text(reason.to_owned()))], + } + } + + /// Add a member. + /// + /// Takes the name and the value together, which is the point: they cannot be + /// reordered apart, and there is no position to miscount. + #[must_use] + pub fn with(mut self, name: &'static str, value: impl Into) -> Self { + self.members.push((name, value.into())); + self + } + + /// Every key this row carries, in order. + /// + /// Derived rather than declared, so a check over the key set cannot drift + /// from what is published. + #[must_use] + pub fn keys(&self) -> Vec<&'static str> { + self.members.iter().map(|(name, _)| *name).collect() + } + + /// The row, rendered as one line of JSON. + #[must_use] + pub fn render(&self) -> String { + let mut out = String::new(); + Value::Object(self.members.clone()).write(&mut out); + out + } +} diff --git a/crates/windows-platform-probes/src/row/tests.rs b/crates/windows-platform-probes/src/row/tests.rs new file mode 100644 index 000000000..260c6766f --- /dev/null +++ b/crates/windows-platform-probes/src/row/tests.rs @@ -0,0 +1,172 @@ +// Copyright (c) Mike Grier. + +//! Tests for the row's value model and its writer. + +use super::{Row, Value}; + +#[test] +fn a_row_renders_its_members_in_the_order_they_were_added() { + let row = Row::new("x-probe-topology") + .with("arch", "x86_64") + .with("processors", 16_usize); + + assert_eq!( + row.render(), + r#"{"reason":"x-probe-topology","arch":"x86_64","processors":16}"# + ); +} + +#[test] +fn the_key_set_is_derived_from_the_value() { + // The property that lets the well-formedness check stop carrying a census. + let row = Row::new("x-probe-topology") + .with("arch", "x86_64") + .with("cores", 8_usize); + + assert_eq!(row.keys(), vec!["reason", "arch", "cores"]); +} + +#[test] +fn a_quote_in_caller_text_cannot_end_the_string_it_is_in() { + // **The injection fix, on the value that carries caller text.** A failed + // discovery interpolates an `io::Error`, and an OS message is free to + // contain a quote. + let row = Row::new("x-probe-topology").with("error", r#"he said "no" and left"#); + + assert_eq!( + row.render(), + r#"{"reason":"x-probe-topology","error":"he said \"no\" and left"}"# + ); +} + +#[test] +fn a_brace_in_caller_text_cannot_start_a_second_row() { + // Measured on PR #88: an `io::Error` containing `{` was selected as the + // report's machine-readable row, so a reader checked the caller's text + // instead of the probe's. A brace inside a string is inert -- this asserts + // the rendering keeps it there. + let row = Row::new("x-probe-topology").with("error", r#"failed at {"reason":"fake"}"#); + let rendered = row.render(); + + assert!( + rendered.lines().count() == 1, + "one line, so there is no second row to select: {rendered}" + ); + assert_eq!( + rendered, + r#"{"reason":"x-probe-topology","error":"failed at {\"reason\":\"fake\"}"}"# + ); +} + +#[test] +fn a_newline_in_caller_text_cannot_end_the_row() { + // The row is one LINE, and a mining pass splits on lines. An unescaped + // newline would put the rest of an error message on a line of its own, + // where it is neither the row nor prose. + let row = Row::new("x-probe-topology").with("error", "first\nsecond\r\nthird"); + let rendered = row.render(); + + assert_eq!(rendered.lines().count(), 1, "{rendered}"); + assert_eq!( + rendered, + r#"{"reason":"x-probe-topology","error":"first\nsecond\r\nthird"}"# + ); +} + +#[test] +fn a_backslash_is_escaped_so_it_cannot_escape_the_quote_after_it() { + // The subtle one: a message ending in a backslash -- a Windows path, say -- + // would otherwise escape the closing quote and swallow the rest of the row. + let row = Row::new("x-probe-topology").with("path", r"C:\temp\"); + + assert_eq!( + row.render(), + r#"{"reason":"x-probe-topology","path":"C:\\temp\\"}"# + ); +} + +#[test] +fn a_control_character_is_escaped_to_its_json_form() { + // A localised OS message can carry one, and JSON forbids them unescaped. + let row = Row::new("x-probe-topology").with("error", "bell\u{7}null\u{0}"); + + assert_eq!( + row.render(), + r#"{"reason":"x-probe-topology","error":"bell\u0007null\u0000"}"# + ); +} + +#[test] +fn a_tab_uses_its_named_escape_rather_than_the_numeric_one() { + let row = Row::new("x").with("t", "a\tb"); + + assert_eq!(row.render(), r#"{"reason":"x","t":"a\tb"}"#); +} + +#[test] +fn the_absent_case_renders_as_null_rather_than_being_omitted() { + // A consumer can tell `null` from a field this probe is too old to publish, + // and cannot tell an omission from either. + let row = Row::new("x").with("highest_numa_node", Option::::None); + + assert_eq!(row.render(), r#"{"reason":"x","highest_numa_node":null}"#); +} + +#[test] +fn a_present_option_renders_as_its_value() { + let row = Row::new("x").with("highest_numa_node", Some(2_usize)); + + assert_eq!(row.render(), r#"{"reason":"x","highest_numa_node":2}"#); +} + +#[test] +fn an_empty_list_renders_as_an_empty_list() { + // Not omitted, for the same reason `null` is not: "no conditions" and "this + // probe does not publish conditions" are different answers. + let row = Row::new("x").with("parse_incomplete", Value::List(Vec::new())); + + assert_eq!(row.render(), r#"{"reason":"x","parse_incomplete":[]}"#); +} + +#[test] +fn a_list_of_objects_renders_each_member_in_order() { + let row = Row::new("x").with( + "caches", + Value::List(vec![ + Value::Object(vec![ + ("level", Value::Number(1)), + ("domains", 8_usize.into()), + ]), + Value::Object(vec![ + ("level", Value::Number(3)), + ("domains", 1_usize.into()), + ]), + ]), + ); + + assert_eq!( + row.render(), + r#"{"reason":"x","caches":[{"level":1,"domains":8},{"level":3,"domains":1}]}"# + ); +} + +#[test] +fn a_list_collects_from_an_iterator_of_anything_a_value_accepts() { + let row = Row::new("x").with( + "efficiency_classes", + [0_u8, 1].into_iter().collect::(), + ); + + assert_eq!(row.render(), r#"{"reason":"x","efficiency_classes":[0,1]}"#); +} + +#[test] +fn a_key_is_escaped_too() { + // Keys are `&'static str` minted in this crate, so none needs escaping + // today -- but the writer has one path for strings, so a key that ever did + // is handled rather than being a hole waiting for the first policy name + // with a quote in it. + let row = Row::new("x").with("odd\"name", 1_usize); + + assert_eq!(row.render(), r#"{"reason":"x","odd\"name":1}"#); +} diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 25e78c34f..6e3d4a07e 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4533,27 +4533,11 @@ fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { /// what a survey receives is the point: an assertion against the struct would /// hold even if the writer published nothing at all. fn row_codes(text: &str, key: &str) -> Vec { - let row = text - .lines() - .find(|line| line.starts_with('{')) - .unwrap_or_else(|| panic!("no machine-readable row in:\n{text}")); - let needle = format!("\"{key}\":["); - let start = row - .find(&needle) - .unwrap_or_else(|| panic!("no `{key}` list in the row:\n{row}")) - + needle.len(); - let end = start - + row[start..] - .find(']') - .unwrap_or_else(|| panic!("unterminated `{key}` list in the row:\n{row}")); - - row[start..end] - .split(',') - .map(|piece| piece.trim().trim_matches('"').to_owned()) - .filter(|piece| !piece.is_empty()) - .collect() -} + let row = crate::report_oracle::row(text) + .unwrap_or_else(|| panic!("no single well-formed row in:\n{text}")); + crate::report_oracle::list_codes(row, key) +} #[test] fn the_row_names_the_probes_own_bug_when_it_detects_one() { // **The defect this whole milestone came from, stated as a test.** The @@ -4704,3 +4688,89 @@ fn a_report_with_nothing_to_report_lists_no_disagreements() { assert!(text.contains(r#""disagreements":[]"#), "{text}"); } + +#[test] +fn a_discovery_error_full_of_json_cannot_manufacture_a_second_row() { + // **The injection defect, end to end through the renderer.** Measured on + // PR #88: an `io::Error` whose text contained `{` was selected as the + // report's machine-readable row, so a reader checked the caller's text + // instead of the probe's. Two containments now answer it -- the prose + // flattening that stops the text occupying a line, and the row's writer + // that escapes it into a string value. + // + // The error is chosen to be as hostile as an OS message can be: a brace, a + // quote, a backslash and a newline, each of which alone would end something. + let hostile = + "failed at {\"reason\":\"x-probe-topology\",\"cross_check\":\"agree\"}\nand C:\\temp\\"; + let text = crate::topology_report::report_unmeasured(BANNER, &std::io::Error::other(hostile)); + + let rows: Vec<&str> = text.lines().filter(|line| line.starts_with('{')).collect(); + assert_eq!( + rows.len(), + 1, + "the caller's text must not be selectable as a row: {text}" + ); + assert!( + rows[0].contains(r#""cross_check":"not_measured""#), + "and the ONE row is the probe's, not the caller's: {}", + rows[0] + ); + crate::report_oracle::assert_corresponds(&text); +} + +#[test] +fn a_discovery_error_reaches_the_row_as_a_field() { + // A survey counting failures wants to group them by cause, and the prose + // sentence is not something a mining pass should be parsing. + let text = crate::topology_report::report_unmeasured( + BANNER, + &std::io::Error::other("the device is not ready"), + ); + + assert!( + text.contains(r#""discovery_error":"the device is not ready""#), + "{text}" + ); +} + +#[test] +fn the_rendered_row_carries_exactly_the_keys_the_value_declares() { + // **What M3.4 deferred to here.** The well-formedness check could not assert + // a key set without a list written beside it, and a hand-written list is a + // census -- this component re-corrected the same one three times in a day, + // and the last correction was falsified within the hour by a field being + // added. `Row::keys` derives it, so the two cannot drift. + let row = crate::row::Row::new("x-probe-topology") + .with("arch", "x86_64") + .with("cross_check", "agree"); + + assert_eq!( + crate::report_oracle::keys(&row.render()), + row.keys(), + "the writer publishes exactly what the value declares" + ); +} + +#[test] +fn the_topology_rows_keys_are_what_the_renderer_declares() { + // The same property on the real row, which is the one a survey reads. + // Asserted as agreement between the reader and the writer rather than + // against a list here, for the reason above. + let text = crate::topology_report::report(BANNER, &clean_observation()); + let row = crate::report_oracle::row(&text).expect("one well-formed row"); + let published = crate::report_oracle::keys(row); + + assert_eq!(published.first().copied(), Some("reason")); + assert!( + published.contains(&"disagreements") && published.contains(&"parse_incomplete"), + "{published:?}" + ); + assert_eq!( + published.len(), + published + .iter() + .collect::>() + .len(), + "no key is published twice: {published:?}" + ); +} diff --git a/crates/windows-platform-probes/src/topology/diagnostic.rs b/crates/windows-platform-probes/src/topology/diagnostic.rs index 5990429e7..14f201e2b 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic.rs @@ -36,7 +36,25 @@ use std::fmt; -use windows_topology_sys::{AnomalyKind, EnumerationAnomaly}; +use windows_topology_sys::{AnomalyKind, EnumerationAnomaly, Source}; + +use crate::row::Value; + +/// A diagnostic as the row publishes it: its code, and the values it carries. +/// +/// **The data, not only the code.** M3.1 published the code alone, so a survey +/// learned `contradictory_cores` without learning that three cores contradicted +/// themselves. The variants already carry those values, for `Display`; what +/// stopped M3.1 publishing them is that the row was a positional template where +/// a nested object had to be hand-assembled. +/// +/// `code` is always first, so a reader scanning accumulated output sees the +/// identity before the detail. +fn entry(code: &'static str, fields: Vec<(&'static str, Value)>) -> Value { + let mut members = vec![("code", Value::Text(code.to_owned()))]; + members.extend(fields); + Value::Object(members) +} /// A counter comparison that was made and did not match. /// @@ -85,6 +103,32 @@ impl Disagreement { } } +impl Disagreement { + /// This disagreement as the row publishes it. + #[must_use] + pub fn published(&self) -> Value { + let fields = match self { + Self::OnlineProcessors { parsed, counter } => vec![ + ("parsed", Value::Number(*parsed)), + ("counter", Value::Number(*counter as usize)), + ], + Self::ProcessorGroups { parsed, counter } => vec![ + ("parsed", Value::Number(*parsed)), + ("counter", Value::Number(*counter as usize)), + ], + Self::HighestNumaNode { parsed, counter } => vec![ + ( + "parsed", + parsed.map_or(Value::Null, |node| Value::Number(node as usize)), + ), + ("counter", Value::Number(*counter as usize)), + ], + }; + + entry(self.code(), fields) + } +} + impl fmt::Display for Disagreement { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -147,6 +191,18 @@ impl NotCompared { } } +impl NotCompared { + /// This entry as the row publishes it. + /// + /// Every variant is a bare condition with no values of its own, so each + /// publishes its code and nothing else -- which is the honest rendering + /// rather than an object padded to look like the others. + #[must_use] + pub fn published(&self) -> Value { + entry(self.code(), Vec::new()) + } +} + impl fmt::Display for NotCompared { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let text = match self { @@ -331,6 +387,56 @@ impl ParseIncomplete { } } +impl ParseIncomplete { + /// This entry as the row publishes it, with the values its variant carries. + #[must_use] + pub fn published(&self) -> Value { + let count = |value: &usize| vec![("count", Value::Number(*value))]; + let fields = match self { + Self::EnumerationAnomalies { count: n } + | Self::NumaDomainsOnlyInCpuSets { count: n } + | Self::ContradictoryCores { count: n } + | Self::UnnumberedCacheLevels { count: n } + | Self::UnreportedRelations { count: n } + | Self::DescribedRelations { count: n } + | Self::CoresOnlyInCpuSets { count: n } + | Self::OverlappingWalkRelations { count: n } + | Self::ProcessorAttributeConflicts { count: n } + | Self::NumaDomainsWithConflictingLabels { count: n } + | Self::NumaDomainsUnreported { count: n } => count(n), + Self::NoCacheLevels + | Self::NoPackages + | Self::NoCores + | Self::NotMeasured + | Self::CoherenceNotCollected => Vec::new(), + Self::CacheLevelsWithoutPartitions { levels } => { + vec![("levels", levels.iter().copied().collect())] + } + Self::MeasuredButCountsAbsent { absent } => { + vec![("absent", absent.iter().copied().collect())] + } + Self::PartitioningSummaryMissing { level } => { + vec![("level", Value::Number(*level as usize))] + } + Self::RelationsWithoutProcessors { cores, packages } => vec![ + ("cores", Value::Number(*cores)), + ("packages", Value::Number(*packages)), + ], + Self::EnumerationsDisagreed { + attempts, + walk_only, + cpu_sets_only, + } => vec![ + ("attempts", Value::Number(*attempts as usize)), + ("walk_only", Value::Number(*walk_only)), + ("cpu_sets_only", Value::Number(*cpu_sets_only)), + ], + }; + + entry(self.code(), fields) + } +} + impl fmt::Display for ParseIncomplete { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -472,3 +578,52 @@ pub fn anomaly_code(anomaly: &EnumerationAnomaly) -> &'static str { _ => "unclassified", } } + +/// An anomaly as the row publishes it. +/// +/// Carries WHERE as well as what: the enumeration it was reading and the byte +/// offset it stopped at. A survey grouping by kind across a fleet wants both -- +/// the same kind at the same offset on many hosts is a different finding from +/// the same kind scattered, and neither is visible from a count. +/// +/// `AnomalyKind` is `#[non_exhaustive]`, so a kind added upstream lands in +/// `unclassified` and publishes no fields rather than a guess at which ones it +/// has. That is the honest rendering: the code says the vocabulary is older than +/// the crate, and inventing fields for it would say more than is known. +#[must_use] +pub fn published_anomaly(anomaly: &EnumerationAnomaly) -> Value { + let source = match anomaly.source { + Source::RelationshipWalk => "relationship_walk", + Source::CpuSets => "cpu_sets", + _ => "unclassified", + }; + + let mut fields = vec![ + ("source", Value::Text(source.to_owned())), + ("offset", Value::Number(anomaly.offset)), + ]; + + fields.extend(match anomaly.kind { + AnomalyKind::Undersized { declared, minimum } => vec![ + ("declared", Value::Number(declared)), + ("minimum", Value::Number(minimum)), + ], + AnomalyKind::OverrunsBuffer { + declared, + remaining, + } => vec![ + ("declared", Value::Number(declared)), + ("remaining", Value::Number(remaining)), + ], + AnomalyKind::TrailingBytes { remaining } => { + vec![("remaining", Value::Number(remaining))] + } + AnomalyKind::TruncatedArray { declared, decoded } => vec![ + ("declared", Value::Number(declared)), + ("decoded", Value::Number(decoded)), + ], + _ => Vec::new(), + }); + + entry(anomaly_code(anomaly), fields) +} diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index eea88af8a..921446c77 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -19,8 +19,11 @@ use std::io; use windows_placement_probe::fingerprint::{Fingerprint, banner_line_for}; -use crate::topology::diagnostic::anomaly_code; -use crate::topology::{Observation, PartitioningCache, Verdict}; +use crate::row::{Row, Value}; +use crate::topology::diagnostic::published_anomaly; +use crate::topology::{ + Disagreement, NotCompared, Observation, ParseIncomplete, PartitioningCache, Verdict, +}; /// The banner and title both reports open with. /// @@ -252,10 +255,24 @@ pub fn report_unmeasured(banner: &str, error: &io::Error) -> String { out, "subject must say so instead of printing a misleading shape.)" ); + // **Through the same writer, and this is the shape that most needs it.** + // The only caller text in any report reaches THIS renderer -- a failed + // discovery's `io::Error`, whose message is whatever the OS said. Publishing + // it as a `Value::Text` is what makes a brace in that message inert rather + // than the start of a second row, which is the contamination measured on + // PR #88. + // + // The error is published as a field rather than only printed, because a + // survey counting failures wants to group them by cause, and the prose + // sentence above is not something a mining pass should be parsing. let _ = writeln!( out, - r#"{{"reason":"x-probe-topology","arch":"{}","cross_check":"not_measured"}}"#, - std::env::consts::ARCH + "{}", + Row::new("x-probe-topology") + .with("arch", std::env::consts::ARCH) + .with("cross_check", "not_measured") + .with("discovery_error", error.to_string()) + .render() ); // Bound here too, for the reason given on `report` below. This renderer @@ -665,146 +682,138 @@ pub fn report(banner: &str, observation: &Observation) -> String { // anything sizing itself by cache boundary, on a row already certified. // Hence the `outermost_partitioning_cache` field beside it, which a // consumer must read rather than inferring from the level being absent. - let cache_json: Vec = observation - .caches - .iter() - .map(|c| format!(r#"{{"level":{},"domains":{}}}"#, c.level, c.domains())) - .collect(); - let policy_json: Vec = observation - .domain_counts() - .into_iter() - .map(|(name, count)| format!(r#""{name}":{count}"#)) - .collect(); - // **The conditions, not how many there were.** These three published - // `.len()`, so the row said a run was in doubt without saying why: a survey - // reading `"parse_incomplete":1` could not tell `partitioning_summary_missing` - // -- this probe detecting a bug in ITSELF -- from `contradictory_cores` or - // `not_measured`. Four categorically different facts, one cardinality, and - // only the prose separated them. - // - // Exactly the correction `efficiency_classes` already carries a few lines - // below, for exactly the reason given there: the list is what the name - // promises, and a count is still available from its length. The - // `windows-topology-sys recorded N enumeration anomal...` sentence the prose - // prints keeps that count; it does not need the row to restate it. + // **One writer, one value, no positions.** The row was built by + // interpolating every value positionally into a `concat!` template, where a + // field's name and its value were related only by counting -- so a reordered + // argument or a miscounted placeholder yielded mislabelled data that still + // parses. Here a name and its value are one pair. See `crate::row`. // - // The codes are the contract and the sentences are not -- see - // `topology::diagnostic`. That is what lets the prose be reworded for a - // reader without breaking a mining pass. - let quoted = |codes: Vec<&'static str>| { - codes - .into_iter() - .map(|code| format!(r#""{code}""#)) - .collect::>() - .join(",") - }; - let disagreements_json = quoted(check.disagreements.iter().map(|e| e.code()).collect()); - let not_compared_json = quoted(check.not_compared.iter().map(|e| e.code()).collect()); - let parse_incomplete_json = quoted(check.parse_incomplete.iter().map(|e| e.code()).collect()); - let anomalies_json = quoted( - observation - .enumeration_anomalies - .iter() - .map(anomaly_code) - .collect(), - ); - let _ = writeln!( - out, - concat!( - r#"{{"reason":"x-probe-topology","arch":"{}","processors":{},"groups":{},"#, - r#""packages":{},"numa_domains":{},"numa_domains_without_processors":{},"cores":{},"#, - r#""efficiency_classes":[{}],"caches":[{}],"outermost_partitioning_cache_level":{},"#, - r#""outermost_partitioning_cache":"{}","#, - r#""policies":{{{}}},"cross_check":"{}","disagreements":[{}],"not_compared":[{}],"#, - r#""parse_incomplete":[{}],"#, - r#""enumeration_anomalies":[{}],"numa_domains_only_in_cpu_sets":{}}}"# - ), - std::env::consts::ARCH, - observation.online_processors, - observation.groups, - observation.packages, - observation.numa_domains, - observation.numa_domains_without_processors, - observation.cores.len(), + // The diagnostics publish their DATA now, not only their code: a survey + // learns `contradictory_cores` AND that three cores contradicted themselves. + // What stopped M3.1 doing that was this template. + let row = Row::new("x-probe-topology") + .with("arch", std::env::consts::ARCH) + .with("processors", observation.online_processors) + .with("groups", observation.groups) + .with("packages", observation.packages) + .with("numa_domains", observation.numa_domains) + .with( + "numa_domains_without_processors", + observation.numa_domains_without_processors, + ) + .with("cores", observation.cores.len()) // The CLASSES, not how many there are. A plural name over a count is - // ambiguous in the one way that matters here: on a single-class host - // this emitted `"efficiency_classes":1`, which reads exactly like a - // machine whose one class is class *1* -- while the prose two lines - // above printed `efficiency classes: [0]`. Same fact, same report, two - // renderings a consumer cannot reconcile. The list is what the name - // promises, agrees with the prose, and carries strictly more: a fleet - // survey can still get the count from its length, and can now also see - // WHICH classes a host reported. - classes - .iter() - .map(u8::to_string) - .collect::>() - .join(","), - cache_json.join(","), - // The level the prose names, read per variant rather than through - // `outermost_partitioning_cache`, whose `None` covers the - // summary-missing case too. Routing through it emitted - // `"outermost_partitioning_cache_level":null` beside - // `"outermost_partitioning_cache":"summary_missing"` while the prose - // printed the number -- one fact, two renderings, no way to reconcile - // them. `domain_counts` was taken off the same accessor for the same - // reason; this consumer was not swept with it. - match observation.partitioning_cache() { - PartitioningCache::Level(cache) => cache.level.to_string(), - PartitioningCache::SummaryMissing(level) => level.to_string(), - PartitioningCache::NoLevelsReported - | PartitioningCache::NoLevelPartitions - | PartitioningCache::NoUniqueOutermost => "null".to_string(), - }, - // The level alone said `null` for every absent case alike, on a line - // the verdict had already certified as "agree" -- an incomparable + // ambiguous in the one way that matters: on a single-class host this + // emitted `"efficiency_classes":1`, which reads exactly like a machine + // whose one class is class *1* -- while the prose printed + // `efficiency classes: [0]`. Same fact, two renderings a consumer cannot + // reconcile. A survey can still get the count from the length. + .with( + "efficiency_classes", + classes.iter().copied().collect::(), + ) + .with( + "caches", + observation + .caches + .iter() + .map(|cache| { + Value::Object(vec![ + ("level", Value::from(cache.level)), + ("domains", Value::from(cache.domains())), + ]) + }) + .collect::(), + ) + // The level alone said `null` for every absent case alike, on a line the + // verdict had already certified as `agree` -- an incomparable // partitioning touches nothing `cross_check` consults. A query counting // nulls as "machines no cache level partitions" then folded in machines - // where a level DOES partition, which is the opposite conclusion for - // anything sizing itself by cache boundary. Always a string, so a - // consumer filters on `== "none"` rather than on the absence of a - // number. - match observation.partitioning_cache() { - PartitioningCache::Level(_) => "level", - PartitioningCache::NoLevelsReported => "no_levels_reported", - PartitioningCache::NoLevelPartitions => "none", - PartitioningCache::NoUniqueOutermost => "not_unique", - PartitioningCache::SummaryMissing(_) => "summary_missing", - }, - policy_json.join(","), - // A tri-state rather than a boolean, for the reason the prose above - // gives: a log-mining pass over accumulated CI output must be able to - // tell "all three counters agreed" from "two agreed and the third was - // never compared". `cross_check_ok:true` said the same thing for both. - match check.verdict() { - Verdict::Agree => "agree", - Verdict::Disagree => "disagree", - Verdict::Incomplete => "incomplete", - }, - // **The disagreements, which the row did not carry at all.** Before - // this, a survey could tell a run had disagreed -- `cross_check` says - // so -- but not WHICH counter disagreed, while the prose listed each one - // with both readings. That is the same shape as the defect this - // milestone came from, in the list nobody had noticed was missing: the - // vocabulary was built for all three lists and wired for two. - // - // It survived because the fact-accounting instrument enumerates the - // ROW's keys, so a fact the row omits entirely is outside what it can - // ask about. It catches a key nothing reads; it cannot catch a prose - // fact with no key. - disagreements_json, - not_compared_json, - // Separate from `not_compared`, because a mining pass that finds - // `"cross_check":"incomplete"` needs to know whether this probe failed - // to read a counter or the parse itself was short or disputed -- the - // first is a gap in the measurement, the second a fact about the - // machine worth going and looking at. The two lists beside it say which - // kind, without a consumer having to know what `cross_check` currently - // pushes for -- and now say which CONDITION, not merely how many. - parse_incomplete_json, - anomalies_json, - observation.numa_domains_only_in_cpu_sets, - ); + // where a level DOES partition, the opposite conclusion for anything + // sizing itself by cache boundary. Hence the discriminator beside it, + // which a consumer must read rather than inferring from the absence. + // **The level a SummaryMissing arm names is published too**, not + // `null`: the two renderings of WHICH level went unchecked are what a + // reader needs when the report is telling them the probe has a bug. + .with( + "outermost_partitioning_cache_level", + match observation.partitioning_cache() { + PartitioningCache::Level(cache) => Value::from(cache.level), + PartitioningCache::SummaryMissing(level) => Value::from(level), + PartitioningCache::NoLevelsReported + | PartitioningCache::NoLevelPartitions + | PartitioningCache::NoUniqueOutermost => Value::Null, + }, + ) + .with( + "outermost_partitioning_cache", + match observation.partitioning_cache() { + PartitioningCache::Level(_) => "level", + PartitioningCache::NoLevelsReported => "no_levels_reported", + PartitioningCache::NoLevelPartitions => "none", + PartitioningCache::NoUniqueOutermost => "not_unique", + PartitioningCache::SummaryMissing(_) => "summary_missing", + }, + ) + .with( + "policies", + Value::Object( + observation + .domain_counts() + .into_iter() + .map(|(name, count)| (name, Value::from(count))) + .collect(), + ), + ) + // A tri-state rather than a boolean: a mining pass must be able to tell + // "all three counters agreed" from "two agreed and the third was never + // compared". `cross_check_ok:true` said the same thing for both. + .with( + "cross_check", + match check.verdict() { + Verdict::Agree => "agree", + Verdict::Disagree => "disagree", + Verdict::Incomplete => "incomplete", + }, + ) + .with( + "disagreements", + check + .disagreements + .iter() + .map(Disagreement::published) + .collect::(), + ) + .with( + "not_compared", + check + .not_compared + .iter() + .map(NotCompared::published) + .collect::(), + ) + .with( + "parse_incomplete", + check + .parse_incomplete + .iter() + .map(ParseIncomplete::published) + .collect::(), + ) + .with( + "enumeration_anomalies", + observation + .enumeration_anomalies + .iter() + .map(published_anomaly) + .collect::(), + ) + .with( + "numa_domains_only_in_cpu_sets", + observation.numa_domains_only_in_cpu_sets, + ); + + let _ = writeln!(out, "{}", row.render()); // **Bound here rather than called from each test, which is the difference // between an oracle and three more tests.** A test added beside the others diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 2383a75c7..c83319868 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -403,26 +403,10 @@ fn published_codes(text: &str, keys: &[&str]) -> Vec { panic!("no single well-formed row in:\n{text}"); }; - let mut codes = Vec::new(); - for key in keys { - let needle = format!("\"{key}\":["); - let Some(start) = row.find(&needle).map(|at| at + needle.len()) else { - continue; - }; - let Some(end) = row[start..].find(']').map(|at| start + at) else { - continue; - }; - codes.extend( - row[start..end] - .split(',') - .map(|piece| piece.trim().trim_matches('"').to_owned()) - .filter(|piece| !piece.is_empty()), - ); - } - - codes + keys.iter() + .flat_map(|key| report_oracle::list_codes(row, key)) + .collect() } - /// How many diagnostic entries the PROSE lists. /// /// The renderer tags each kind under DISAGREE and renders both as a bare `- ` @@ -557,31 +541,37 @@ fn a_state_the_row_does_not_publish_fails_the_accounting() { "the row publishes it today, which is what the rule requires" ); - // Now strip every condition from the row, which is what a renderer that - // forgot to publish one would produce. + // Now empty every diagnostic list in the row, which is what a renderer that + // forgot to publish one would produce. Done by replacing each list's whole + // contents rather than by editing entries: the entries are objects now, so + // splitting on commas cuts them in half -- which is how this sabotage broke + // when M3.3 landed, and is worth the note because a sabotage that no longer + // sabotages leaves the rule it guards unguarded while still passing. let stripped = text .lines() .map(|line| { - if line.starts_with('{') { - line.replace( - r#""parse_incomplete":["#, - r#""parse_incomplete":["x-removed-"#, - ) - .replace("x-removed-", "") - .split(',') - .filter(|piece| !piece.contains("partitioning_summary_missing")) - .collect::>() - .join(",") - } else { - line.to_owned() + if !line.starts_with('{') { + return line.to_owned(); + } + let mut out = line.to_owned(); + for key in DIAGNOSTIC_LISTS { + let needle = format!("\"{key}\":["); + let Some(start) = out.find(&needle).map(|at| at + needle.len()) else { + continue; + }; + let Some(end) = report_oracle::list_span_end(&out, start) else { + continue; + }; + out.replace_range(start..end, ""); } + out }) .collect::>() .join("\n"); assert!( published_codes(&stripped, DIAGNOSTIC_LISTS).is_empty(), - "the sabotage must actually remove the condition: {stripped}" + "the sabotage must actually remove the conditions: {stripped}" ); } From ba786cecddba42d33b5104a9ee3e55b1dc2f0abe Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:10:36 -0400 Subject: [PATCH 11/46] refactor(platform-probes): remove the last prose parsing from the test matrix M3.5 left one site where the test matrix obtained structured data by reading sentences: a rule that filtered rendered lines by prefix, counted them, and compared that number against the row's condition count. A wider sweep found it had a unit-test twin in `src/tests.rs` that the first pass missed. Both are replaced by the same claim stated against `cross_check`: the row's condition codes must EQUAL the cross-check's, in order. Strictly stronger. A count catches only a dropped entry; this catches a drop, a reorder and a substitution. It also covers all three lists, which the prose count could not -- under INCOMPLETE the renderer gives `not_compared` and `parse_incomplete` the same bare `- ` prefix, so only their total was recoverable from prose at all. Be clear about what it is, since the replacement is easy to over-read: the row is BUILT from these lists, so this is the writer checked against its input, not an independent reading of the machine. That is the check worth having -- the writer is the one thing no amount of typing upstream can check for itself -- but it is narrower than "the report is correct" and the comments say so. **The ordering half was vacuous, and the guard is what found it.** Reversing the row's `parse_incomplete` order reddened nothing: every corpus shape varied one dimension, so each landed at most one entry per list, and a one-element list has no order to get wrong. The first version of the guard summed the three lists and passed while the sabotage still did nothing -- one entry in each of two lists is two conditions and no order to get wrong. Corrected to measure the largest SINGLE list, and a `several conditions at once, in one list` shape added to the corpus. The reorder now reddens `the_row_lists_exactly_the_conditions_the_cross_check_found`. That sequence is the point of the guard: a corpus cannot report the shape it does not reach, so the rule looked strong for as long as nothing asked it to prove it. Swept for the claim rather than asserting it. What still touches rendered text: selecting the row line, and positional containment -- the banner is the first line, the banner is one line, there is exactly one row-shaped line. None reads prose for its content, and no test parses prose into a value. The 58 `contains(...)` assertions on prose remain and are deliberate: they test the prose renderer on its own terms, which the decision permits. They extract nothing. Verified: 202 lib tests plus 14 ignored, 9 integration tests and doctests, on both `x86_64-pc-windows-msvc` and `i686-pc-windows-msvc`. clippy --all-targets --all-features clean, fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 22 ++++ crates/windows-platform-probes/src/tests.rs | 77 +++++++++--- .../tests/a_real_report_agrees_with_itself.rs | 118 +++++++++++++----- 3 files changed, 172 insertions(+), 45 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 094540a64..709eabd7c 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -294,6 +294,28 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. Both publication rules carry a corpus guard, because both skip a shape in no blocking state and a drifted all-healthy corpus would leave them green while checking nothing. + **The last prose parsing in the matrix is gone.** M3.5 left one site: a rule that filtered + rendered lines by prefix, counted them, and compared that number against the row -- the only place + left where the test matrix obtained structured data by reading sentences. It had a unit-test twin + in `src/tests.rs` that the first sweep missed and a second, wider sweep found. + + Both are replaced by the same claim against `cross_check`: the row's codes must EQUAL the + cross-check's, in order. Strictly stronger -- a count catches only a dropped entry, this catches a + drop, a reorder and a substitution -- and it never reads a sentence. It also covers all three + lists, which the prose count could not: under INCOMPLETE the renderer gives `not_compared` and + `parse_incomplete` the same bare `- ` prefix, so only their total was recoverable from prose. + + **The ordering half was vacuous, and the guard is what found it.** Reversing the row's + `parse_incomplete` order reddened nothing: every corpus shape varied one dimension, so each landed + at most one entry per list, and a one-element list has no order to get wrong. A first version of + the guard summed the three lists and passed while the sabotage still did nothing -- one entry in + each of two lists is two conditions and no order. Corrected to measure the largest SINGLE list, + and a `several conditions at once, in one list` shape added. The reorder now reddens. + + What remains that touches rendered text at all: selecting the row line, and asserting positional + containment -- the banner is the first line, the banner is one line, there is exactly one row. + None reads prose for its content. + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates the facts a report publishes and measures, by mutation, which are read. The instrument is sound and diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 6e3d4a07e..e1bdc41f0 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4532,6 +4532,38 @@ fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { /// Reads the rendered artifact rather than the `CrossCheck` behind it, because /// what a survey receives is the point: an assertion against the struct would /// hold even if the writer published nothing at all. +/// The codes of a diagnostic list, for comparing the row against what the +/// cross-check found. +/// +/// Generic over the three list types because each has its own `code`, and a +/// helper per list would be three copies of one idea. +fn codes_of(entries: &[T]) -> Vec { + entries.iter().map(|entry| entry.code().to_owned()).collect() +} + +/// The three diagnostic vocabularies, so `codes_of` can take any of them. +trait HasCode { + fn code(&self) -> &'static str; +} + +impl HasCode for crate::topology::Disagreement { + fn code(&self) -> &'static str { + Self::code(self) + } +} + +impl HasCode for crate::topology::NotCompared { + fn code(&self) -> &'static str { + Self::code(self) + } +} + +impl HasCode for crate::topology::ParseIncomplete { + fn code(&self) -> &'static str { + Self::code(self) + } +} + fn row_codes(text: &str, key: &str) -> Vec { let row = crate::report_oracle::row(text) .unwrap_or_else(|| panic!("no single well-formed row in:\n{text}")); @@ -4595,30 +4627,45 @@ fn the_row_lists_exactly_the_codes_of_the_conditions_the_check_found() { } #[test] -fn the_row_lists_one_code_per_line_the_prose_lists() { +fn the_row_lists_a_condition_for_every_kind_the_check_found() { // **The rule M3.1 establishes: a renderer may not tell a reader something - // the row cannot tell a survey.** Asserted as a count rather than a pairing - // on purpose -- the code and the sentence come from one `diagnostic` - // variant, so they cannot name different conditions, and what is left to - // check is that neither rendering drops an entry the other kept. + // the row cannot tell a survey.** + // + // This counted PROSE LINES and compared that number against the row -- the + // last place in the matrix that obtained structured data by reading + // sentences. Replaced by the same claim against the cross-check, which is + // stronger (it catches a reorder or a substitution, not only a drop) and + // never reads a sentence. + // + // Covers all three lists at once, which the prose count could not: under + // INCOMPLETE the renderer gives `not_compared` and `parse_incomplete` the + // same bare `- ` prefix, so only their total was recoverable from prose. let mut observation = clean_observation(); observation.caches = Vec::new(); observation.numa_domains_unreported = 2; + observation.raw_group_count = 0; let text = crate::topology_report::report(BANNER, &observation); - let listed = text - .lines() - .filter(|line| line.starts_with(" - ") || line.starts_with(" (parse incomplete) ")) - .count(); + let check = observation.cross_check(); - assert_eq!( - row_codes(&text, "parse_incomplete").len(), - listed, - "the prose lists {listed} conditions, so the row must publish that \ - many: {text}" + for (key, expected) in [ + ("disagreements", codes_of(&check.disagreements)), + ("not_compared", codes_of(&check.not_compared)), + ("parse_incomplete", codes_of(&check.parse_incomplete)), + ] { + assert_eq!( + row_codes(&text, key), + expected, + "{key}: the row must publish what the check found, in order: {text}" + ); + } + + assert!( + !check.not_compared.is_empty() && check.parse_incomplete.len() >= 2, + "the fixture must fill more than one list, and one of them more than \ + once, or neither the coverage nor the ordering is exercised: {check:?}" ); } - #[test] fn an_anomaly_reaches_the_row_as_its_kind() { // Anomalies are published per-anomaly, so a survey can group by WHAT failed diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index c83319868..95ae849b6 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -348,6 +348,22 @@ fn shapes() -> Vec { anomalies_while_disagreeing, ); + // **Several conditions in ONE list, which nothing else here reaches.** Every + // other shape varies one dimension, so each lands at most one entry per + // list -- and a one-element list has no order to get wrong. That made the + // ordering half of `the_row_lists_exactly_the_conditions_the_cross_check_found` + // vacuous: reversing the row's `parse_incomplete` reddened nothing at all. + // + // Found by the guard in `the_corpus_reaches_states_that_block_agreement`, + // which is there precisely because a corpus cannot report the shape it does + // not reach. + let mut several = base(); + several.caches = Vec::new(); + several.cores_only_in_cpu_sets = 1; + several.numa_domains_unreported = 2; + several.processor_attribute_conflicts = 1; + push("several conditions at once, in one list", several); + shapes } @@ -407,20 +423,6 @@ fn published_codes(text: &str, keys: &[&str]) -> Vec { .flat_map(|key| report_oracle::list_codes(row, key)) .collect() } -/// How many diagnostic entries the PROSE lists. -/// -/// The renderer tags each kind under DISAGREE and renders both as a bare `- ` -/// under INCOMPLETE, so this counts what a reader would count on either. -fn prose_diagnostic_lines(text: &str) -> usize { - text.lines() - .filter(|line| { - line.starts_with(" - ") - || line.starts_with(" (not compared) ") - || line.starts_with(" (parse incomplete) ") - }) - .count() -} - #[test] fn every_state_that_blocks_agreement_reaches_the_row() { // **This is the rule M3.1 established, given an instrument at last.** A @@ -464,34 +466,57 @@ fn every_state_that_blocks_agreement_reaches_the_row() { } #[test] -fn the_row_lists_a_condition_for_every_diagnostic_the_prose_lists() { - // The same rule as a count, over every shape the corpus reaches. The prose - // and the row render the same conditions through one `diagnostic` variant - // each, so they cannot name DIFFERENT ones -- what they can do is drop one, - // which is what this catches. +fn the_row_lists_exactly_the_conditions_the_cross_check_found() { + // **The rule that replaced a prose count, and the last prose parsing in the + // matrix went with it.** This compared the row's condition count against a + // count of prose lines -- which meant filtering rendered text by line prefix + // and turning it into a number, the one remaining place the test matrix + // obtained structured data by reading prose. + // + // What replaces it is strictly stronger and never reads a sentence: the + // row's codes must EQUAL the cross-check's codes, in order. A count could + // only catch a dropped entry; this catches a dropped one, a reordered one, + // and a substituted one. // - // Not a pairing: the code and the sentence come from one variant, so the - // correspondence between them holds by construction and checking it would - // be checking a copy. What remains checkable is that neither rendering - // dropped an entry the other kept. + // Be clear about what it is: the row is BUILT from these lists, so this is + // the writer being checked against its input, not an independent reading. + // That is exactly the check worth having here -- the writer is the one thing + // no amount of typing upstream can check for itself -- but it is narrower + // than "the report is correct" and should not be read as that. for shape in shapes() { let text = windows_platform_probes::topology_report::report( &banner_for(&shape.observation), &shape.observation, ); + let check = shape.observation.cross_check(); + let expected: Vec = check + .disagreements + .iter() + .map(|entry| entry.code().to_owned()) + .chain( + check + .not_compared + .iter() + .map(|entry| entry.code().to_owned()), + ) + .chain( + check + .parse_incomplete + .iter() + .map(|entry| entry.code().to_owned()), + ) + .collect(); + assert_eq!( - published_codes(&text, DIAGNOSTIC_LISTS).len(), - prose_diagnostic_lines(&text), - "{}: the prose lists {} diagnostic(s) and the row publishes {:?}\n\n\ - --- the report ---\n{text}", - shape.what, - prose_diagnostic_lines(&text), published_codes(&text, DIAGNOSTIC_LISTS), + expected, + "{}: the row must publish every condition the cross-check found, in \ + order\n\n--- the report ---\n{text}", + shape.what, ); } } - #[test] fn the_corpus_reaches_states_that_block_agreement() { // **The guard that stops the two tests above passing for nothing.** Both @@ -516,6 +541,39 @@ fn the_corpus_reaches_states_that_block_agreement() { "every shape blocks, so the acceptance half of the publication rules \ is never exercised" ); + + // **ORDER is only a claim where there is more than one entry to order.** + // `the_row_lists_exactly_the_conditions_the_cross_check_found` compares the + // row's codes against the cross-check's as a SEQUENCE, which is what makes + // it stronger than the prose count it replaced -- but a corpus whose shapes + // each carry at most one condition can never tell a sequence from a set. + // + // Measured, and this is why the guard exists: reversing the row's + // `parse_incomplete` order reddened nothing until a multi-condition shape + // was in the corpus. + // Within ONE list, not summed across the three. Summing was the first + // version of this guard and it passed while the sabotage still reddened + // nothing: a shape carrying one `not_compared` and one `parse_incomplete` + // has two conditions and no order to get wrong, because reversing a + // one-element list is the identity. + let most = shapes() + .iter() + .map(|shape| { + let check = shape.observation.cross_check(); + check + .disagreements + .len() + .max(check.not_compared.len()) + .max(check.parse_incomplete.len()) + }) + .max() + .unwrap_or_default(); + + assert!( + most >= 2, + "no shape carries two conditions in ONE list, so the ordering half of \ + the publication rule is vacuous -- it cannot tell a sequence from a set" + ); } #[test] From 1741fbfeac43ff73c1464541cd69b9a2ab416010 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:15:43 -0400 Subject: [PATCH 12/46] docs(platform-probes): split DESIGN-NOTES into Tier 1 and Tier 2 Completed item: M3.6: Split DESIGN-NOTES.md into Tier 1 and Tier 2. DESIGN-NOTES.md was 92,467 bytes -- XL on the repository's byte scale, where the default answer is to split unless the document is indivisible. It is not: it carried current decisions and a large block of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture. The fracture taken is the correspondence-oracle investigation: the two defects a pull-request review found after twenty-eight rounds of per-artifact review and a zero-surviving-mutant sweep, why each instrument was structurally incapable of seeing them, the sparse-matrix-and-oracle split that followed, and the oracle's own design, failure modes and mutation evidence. It is Tier 2 on both tests. It records how a decision was reached rather than stating one, and the decision it reached has since been superseded by #d-encoded-row-is-the-contract. Moving it also resolved latent drift rather than only relocating bytes. The block cites the `Correspondence` enum, the fact-accounting instrument and the prose correspondence rules -- none of which survived M3.4 and M3.5. As history those sentences are accurate; sitting in Tier 1 they described deleted code as though it were current. **The result is still XL, and that is stated rather than implied away.** DESIGN-NOTES.md is 67,807 bytes, over the 64 KiB threshold. The split was made at the one unambiguous fracture rather than trimmed until it hit a number: a cut sized to a byte budget yields two documents that are each individually incomprehensible, which is worse than one large coherent document. Why it stays over: the remaining bulk is neither current decisions nor rationale, but FINDINGS -- measurements about Windows that are this crate's actual product. They do not belong in a rationale file, and filing them as decisions is what keeps Tier 1 large. Whether they want a tier of their own is a structural choice about this component's documentation scheme, so it is raised as an open question on the checklist rather than taken unilaterally. Pure relocation, verified byte-for-byte against the pre-split file: 439 lines, identical. No reflow, no rewording, no other work in this commit -- the two headings missing a blank line above them were inherited and are left for a follow-up, because a split that also tidies is a split whose blame cannot be traced. The two moved anchors are kept in Tier 1 beside a pointer, so an existing link to `DESIGN-NOTES.md#d-correspondence-failures` still lands somewhere that says where the content went. Every in-repo reference was repointed at the content itself: the superseding decision's two links, the design-session record's two, and the crate's PLANS.md row. Split-Source: crates/windows-platform-probes/DESIGN-NOTES.md Split-Into: crates/windows-platform-probes/DESIGN-RATIONALE.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 24 +- .../windows-platform-probes/DESIGN-NOTES.md | 459 +----------------- .../DESIGN-RATIONALE.md | 456 +++++++++++++++++ crates/windows-platform-probes/PLANS.md | 2 +- ...-2026-09-12-what-the-oracle-should-read.md | 4 +- 5 files changed, 505 insertions(+), 440 deletions(-) create mode 100644 crates/windows-platform-probes/DESIGN-RATIONALE.md diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 709eabd7c..eee5f1187 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -326,7 +326,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on its own terms rather than against the row. -- [ ] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. +- [x] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is to split unless the module is indivisible. It is not -- it carries current decisions and a large @@ -337,6 +337,28 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. stating what was decided and what forced it. The decision added by this milestone is written to be split that way already, so it is the worked example rather than the hard case. + **Done, and the result is still XL -- say so rather than imply otherwise.** 25,942 bytes moved; + DESIGN-NOTES.md went 92,467 -> 67,807, which is over the 64 KiB threshold still. The split was + made at the one unambiguous Tier 2 fracture rather than trimmed to hit a number. + + The fracture: the correspondence-oracle investigation. It is Tier 2 on both tests -- a record of + how a decision was reached rather than a statement of one, AND a decision since superseded by + [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract). Moving it also + resolved latent drift: it cites `Correspondence`, the fact-accounting instrument and the prose + rules, none of which survived M3.4 and M3.5. As history those sentences are accurate; as Tier 1 + they described deleted code. + + Pure relocation, verified byte-for-byte against the pre-split file (439 lines, identical). The + two moved anchors are kept in Tier 1 beside a pointer, so existing links land somewhere that says + where the content went, and every in-repo reference was repointed at the content. + +> **-> OPEN QUESTION for the engineer:** the remaining bulk of DESIGN-NOTES.md is neither current +> decisions nor rationale -- it is FINDINGS, measurements about Windows that are this crate's actual +> product (the completion-port fork, the thread-agnosticism probe, the x64 comparison, the long-path +> pair, the topology cross-check). They do not belong in a rationale file, and filing them as +> decisions is what keeps Tier 1 XL. Whether they want a tier of their own is a structural choice +> about this component's documentation scheme, so it is raised rather than taken. + ## M4 -- Carried over from M2: the items M3 gates These were written under M2 and are blocked on M3 above: each one targets the prose-against-row diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 7460d5343..2162a4a6f 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1025,445 +1025,32 @@ it: it closes over the whole run including both banner reads, where `measure` closes only over the counters. Neither subsumes the other, and no work is scheduled by this decision. -## The defects that survived were correspondence failures, and no instrument here could see them +## The correspondence-oracle investigation, and what it concluded + -**The diagnosis here is refined by [The encoded row is the contract; the prose is -not](#d-encoded-row-is-the-contract).** What each instrument could not see is -unchanged and is still the reason this component has an oracle at all. What this -section got wrong is WHERE the two defects lived: both were defects in the -ENCODED ROW, not in the relation between two renderings of a consistent state. - -This probe was reviewed twenty-eight times before it opened as a pull request, -by two independent readers per round on different models, with `cargo-mutants` -reporting **zero surviving mutants** on both of its modules. A review on the -pull request then found, in code none of that had touched, a state where the -renderer printed - -``` -BUG IN THIS PROBE: the topology crate named L3 as the outermost -partitioning cache and this survey carries no summary for it. Nothing -below about cache partitioning can be trusted. -``` - -while `cross_check` had no branch for that state at all, so `verdict()` could -return `Agree` for the same run and print `=> agree` two paragraphs below. A -second finding in the same review had the same shape: one fact rendered twice -in one report -- `efficiency classes: [0]` in prose, `"efficiency_classes":1` -in the NDJSON -- in two shapes a consumer cannot reconcile, where the numeral -happens to read as a plausible class *label*. - -Neither is a bug inside a function. Every function involved was correct on its -own terms, and each had been read repeatedly and found so. The defect lived in -the **relation between two artifacts**, and that is a place none of the -instruments in use could look. - -### Why each instrument was structurally incapable, not merely unlucky - -**Mutation testing cannot find absent code.** `cargo-mutants` perturbs what is -written and asks whether a test notices. A missing branch has no mutants, so -the missing `SummaryMissing` check did not lower the score -- it was invisible -to it. The 180/0 result was true and said nothing about the gap. A perfect -mutation score is compatible with an entirely missing feature, and this -component is the proof. - -The same run also shows the weaker half of what a mutation score means. A test -existed asserting `"efficiency_classes":2`, so every mutant of that line died. -It was pinning the wrong shape faithfully. **Mutation testing measures whether -behavior is pinned by tests; it is silent on whether the pinned behavior is -right.** Both halves were over-read here for many rounds as though they were -evidence of correctness. - -**Exhaustiveness checking protects `match` expressions, not concepts.** -`PartitioningCache` exists precisely to force a decision -- its own doc says a -renderer or serialiser "cannot emit the absent case without having decided -which absent case it is" -- and it worked, in the two consumers that wrote a -`match`. It bought nothing in the two that did not: `domain_counts` reached the -same information through `outermost_partitioning_cache`, a second accessor -returning `Option`, which launders five states into two; and `cross_check` -never asked. A type can only compel a consumer that consults it. - -**Per-artifact review finds per-artifact defects.** Two readers checking each -function against its own documentation will confirm both sides of a -contradiction, because each side is locally true. Worse, the readers were -answering questions posed in a prompt, and across rounds that prompt -accumulated focus areas and "already verified, do not re-litigate" facts. The -shared prompt correlated the readers far more strongly than their differing -models decorrelated them; the instrument was being shaped to agree with its -author. Removing that framing in the final round is what got a reader to trace -`simultaneous_multithreading` out of this crate into `windows-topology-sys` and -check it against the Win32 `LTP_PC_SMT` contract. - -The single sentence that covers all three: **every instrument in use verified -properties of things that exist.** Tests assert existing behavior, mutation -perturbs existing code, reviewers check written claims. A correspondence -failure is a property of a *pair*, and an absent branch is not a thing at all. - -### Integration-level analysis was absent, which is where these live - -At the time of the pull request the crate had one integration test, asserting -that a probe writes something to stdout. Of twenty-five `report()` calls in the -suite, **none rendered from a real host's `measure()`** -- every one used a -synthetic `Observation` built by hand. A hand-built fixture can only contain -states its author already imagined, and each assertion checked one local fact -about it. Nothing anywhere rendered the artifact a consumer actually reads and -asked whether it was self-consistent. - -### What to do instead: a sparse matrix to explore with, an oracle to keep - -The obvious response -- tabulate every state against every consumer and fill -the grid -- is wrong, and was proposed and rejected during this analysis. Such -a table grows combinatorially, most of its cells are meaningless, and a version -of it committed beside the code would be a second copy of the code's structure -that nothing verifies. It would rot exactly as every restatement in this -component rotted, and a stale "all cells covered" table is more dangerous than -no table. - -The division that does work: - -- **The matrix is a transient, exploratory instrument.** Draw it for one type - at one boundary to find out which correlations exist. It is expected to be - **sparse**; most cells are empty and discovering that is cheap. Correlations - cannot be derived -- which is why twenty-eight rounds of reading produced - none -- so populating it is exploration, not specification. -- **An oracle is the durable artifact.** Only cells that turn out to mean - something graduate into it. It stays small because discovery, not - enumeration, fills it. - -`windows-file-watcher`'s `ContractChecker` is this repository's worked example -of the oracle half: a shared executable definition of the rules, owned by the -crate that owns the contract, that the producing crate's own tests and every -consumer's test doubles all bind to. It already existed while this probe was -being written, and was not reached for. - -Every correlation admitted here is one the report already renders twice, with -nothing relating the two -- a property of the artifact rather than of anyone's -intuition about it. The ones this decision was written against are: - -1. an alarm in the report implies the verdict is not `agree`; -2. a fact rendered twice must agree across its renderings; -3. an uncaveated hardware claim implies `!parse_in_doubt`; -4. the banner names the same machine the body describes. - -That list is the seed, not the census: the admission RULE is what governs, and -the authoritative set is the `Correspondence` enum in -[src/report_oracle.rs](src/report_oracle.rs). An earlier version of this -paragraph said "three correlations" and listed the first three while the enum -already had the fourth -- the count was wrong when written and would have -rotted again at the next addition, so it is stated as a rule here instead. - -The replacement rule was then itself overstated, as "known to be real because it -was violated" -- which excludes the correspondences found by the M2.4 matrix, -where no defect had occurred and walking every NDJSON field against the prose is -what showed the fact rendered twice with nothing comparing it. Both routes are -admissible; what is not is inventing a correspondence between things the report -does not actually render twice. Corrected the same day it was written, after a -review noticed it contradicted `check_structured_pairs`' own history. - -What makes an oracle different from more tests is where it is invoked: if every -test renders *through* it, every existing call site inherits the checks and so -does every future one. A test added beside them checks one case; an oracle -checks every case anyone ever writes. (Also stated without a number on purpose --- this said "all twenty-five existing call sites" and there are now 35.) - -**Record the vacuous findings too.** "We examined whether X and Y must -correspond, and they need not" is a result, and it is the half that normally -evaporates -- without it the next person re-explores the same empty cells. - -An oracle is a forcing function for correlations already discovered. It will -not find a new one. The discipline that makes it compound is that each newly -found cross-artifact contradiction adds an invariant to the oracle rather than -a one-off test. - -Whether this generalises to `Coherence`, `BracketOutcome`, `Verdict` and the -sibling probes is **an open question, deliberately not answered here.** The work -this decision implies is queued as M2 in [CHECKLIST.md](CHECKLIST.md); this -section schedules nothing on its own. - -## The oracle exists, and what it deliberately refuses to know +**Moved to Tier 2: [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md).** The anchors +above are kept here so every existing link still lands somewhere that says where +the content went. - +What it covers: the two defects a pull-request review found after twenty-eight +rounds of per-artifact review and a zero-surviving-mutant sweep; why each +instrument in use was structurally incapable of seeing them; the sparse-matrix +and oracle split that followed; and the oracle's own design, failure modes and +mutation evidence. + +It is Tier 2 rather than Tier 1 for two reasons. It is a record of how a decision +was reached rather than a statement of one -- and the decision it reached has +since been superseded by +[The encoded row is the contract; the prose is not](#d-encoded-row-is-the-contract). +The code it describes is also gone: the prose-reading oracle, its correspondence +enum and its fact-accounting instrument were retired by M3.4 and M3.5, so several +of its sentences name types that no longer exist. + +What survives into Tier 1 is the conclusion the two defects actually support, +which is the decision below. -**The artifact-reading rule below is superseded by [The encoded row is the -contract; the prose is not](#d-encoded-row-is-the-contract).** The rest of this -section -- what the oracle admits, the acceptance half, the failure mode that -looks like success -- still describes what is in the tree and still holds. - -M2.1 built it: [src/report_oracle.rs](src/report_oracle.rs), admitting only -correlations the report already renders twice. The defect that forced -it is the section above. - -**It reads the rendered artifact, never the state behind it.** Checking state -would miss precisely this defect class -- in the original finding the state was -consistent and the two *renderings* of it were not. - -That last sentence is the superseded one, and it is wrong about its own -evidence. Re-checked against the code: the alarm has no NDJSON key, and -`cross_check` does -- so the original finding was a run whose ENCODED ROW said -`agree` while the probe had detected its own bug, and published nothing about -that bug. The state was not consistent; the row was wrong. See -[#d-encoded-row-is-the-contract](#d-encoded-row-is-the-contract). - -**It relates two things already visible in the report, and re-derives nothing.** -A second implementation of the rendering rules would be a check of the copy -rather than of the contract, and would drift the moment either moved. So the -alarm rule compares an alarm line against a verdict line, the double-rendering -rule compares prose against NDJSON, and the gating rule compares a claim against -the report's own published evidence of doubt. - -That last one is the interesting boundary. `CrossCheck::parse_in_doubt` is -`!disagreements.is_empty() || !parse_incomplete.is_empty()`, and the NDJSON -publishes `parse_incomplete` as a **list of conditions** rather than the -predicate -- so the oracle reads whether that list is empty, together with the -`disagree` verdict, and those are the two visible shadows of that definition. The -coupling is deliberate, and confirming it still holds is what M2.2's sabotage -check is for when the call sites are bound. - -(This said "as a **count**", which M3.1 made false when the three diagnostic -fields began publishing their conditions. The shape of the argument is -unchanged -- the row still renders a shadow of the predicate rather than the -predicate -- but the shadow is now a list, and an emptiness test rather than a -comparison against `0`.) - -**Half the tests assert acceptance**, following -[../windows-file-watcher/src/contract.rs](../windows-file-watcher/src/contract.rs)'s -`ContractChecker`: an alarm beside a non-agreeing verdict is legal and is what -the fix produced, a caveated claim under doubt is legal and is what the renderer -emits on every heterogeneous host with a short parse, and a prose-only report is -silence rather than violation. Over-constraining is the same defect as -under-specifying and fails in the more expensive direction, because noise trains -a reader to ignore the instrument. - -### The failure mode that would look exactly like success - -An oracle whose prose labels do not match the renderer reads nothing, finds -nothing, and passes everything. So the labels were confirmed against a real -`probe-topology` run, and a test corrupts each double-rendered value in turn and -requires a violation -- if a label ever drifts, that test fails rather than the -oracle going quietly blind. - -**The first attempt at that injection silently did nothing**, and is worth -recording because it nearly produced the opposite conclusion. The anchor used -was `cross-check:`, which does not occur -- the real text is `cross-check -against independently read Win32 counters:` -- so the "defective" report was -identical to the clean one, the oracle correctly reported no violation, and the -reading was almost "the oracle is blind". A sabotage that fails to apply is -indistinguishable from an instrument that fails to fire, unless the injection -asserts it changed something. It now does. - -**The mirror-image hazard: a RESTORE that fails to rebuild.** Sabotage work in -this crate is a loop -- break it, run it, put it back, run it again -- and the -put-it-back step has its own way of lying. On Windows, PowerShell's `Copy-Item` -preserves the source file's `LastWriteTime`, so restoring a file from a backup -taken earlier gives it an mtime OLDER than the artifacts built from the -sabotaged version. Cargo fingerprints by mtime, decides nothing has changed, and -reruns the previous binary. Measured here: a restored, correct oracle reported -the fixed defect as still present, and the reading was almost "the fix does not -work" -- the conclusion was only avoided by printing the intermediate values and -finding that the function returned the right answer while the test insisted it -did not. - -After restoring a file by copy, set its timestamp forward -(`(Get-ChildItem ).LastWriteTime = Get-Date`) or rewrite it through a -read-then-write, which stamps it as a matter of course. The general rule is the -same one as above, pointed the other way: **a green result proves nothing until -you know the code you are testing is the code you just wrote.** - -### The real-host test, and the guard that stops it passing for nothing - -[tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) -composes the report the way `probe-topology` does and applies the oracle -explicitly. - -**Why it has to exist.** The oracle's unit tests pin it against fixtures, and a -fixture is a report somebody wrote down -- so a fixture-bound oracle checks -correspondences over states its author already imagined, and the defect it -exists for was a state nobody had imagined. More narrowly, a fixture cannot -notice the *renderer* drifting away from the prose labels the oracle reads: -both sides would still agree with each other. Only the real artifact disagrees. - -Some unit tests in this crate do call `measure()` and so do read this host. -What none of them does is run the **oracle** over a report rendered from that -reading, which is the gap this test closes. On CI it runs across the hosted -runner fleet, a survey of shapes no fixture anticipates. - -**It asserts nothing about this machine, deliberately.** A test expecting a -processor count, a cache level or a verdict would fail on the next runner shape -rather than on a defect, and would have to be loosened until it asserted -nothing. What it checks is that whatever this host produced, the report's parts -agree with each other -- a property every host must satisfy, including one whose -topology cannot be read at all. - -#### The primary assertion can pass having checked nothing - -On a host whose report the oracle cannot parse, every lookup returns `None`, -every comparison is skipped, and -`a_report_rendered_from_this_host_agrees_with_itself` passes having checked -exactly zero correspondences. That is why the second test corrupts each -double-rendered fact in a report **this host really produced** and requires the -oracle to report a violation **naming that fact** -- and asserts first that the -corruption changed the text at all, for the reason recorded above. -Eight facts rather than one, because corrupting a single field would leave the -others unguarded: the renderer could drift away from the oracle's other prose -labels and the test would still pass on the strength of the one that remained. - -**"Naming that fact" is load-bearing, and took three attempts to get right.** -The guard first required only that the violation list was non-empty. That was -strengthened to require a `ProseAndNdjsonDisagree`, with a comment correctly -observing that corrupting the NDJSON `processors` count also trips the -cross-check counter rule -- and then not acting on the observation, because that -counter rule emits precisely that variant. So the strengthened guard still -passed while the reader it was written to protect was blind. - -Measured, by blinding one `DOUBLE_RENDERED` prose label at a time: under the -variant-only assertion, `processors` and `groups` both stayed **green**, masked -by their counter rules, while `packages` went red because nothing else reads it. -Two of the four facts the guard names as required were unchecked by the guard -whose entire purpose is to establish that they are checked. Matching on the -`fact` string closes it, because no neighbouring rule can supply another rule's -fact name. - -The general lesson, which this crate has now paid for four times: **a guard -against vacuity is itself a claim, and is subject to the same discipline as any -other.** Each of the three earlier versions was written to fix the previous -one's vacuity and introduced the next-narrower version of it. The only thing -that has ever settled the question is sabotaging the mechanism the guard is -supposed to protect and watching the guard go red -- never reading the guard and -judging it sufficient. - -### The fact set is derived, because the gaps were never in the rules - -Six unread double-renderings were found on this branch by six different -reviewers, and none by the oracle's own coverage. Each individual gap was real, -and fixing each one by hand was correct -- but the pattern is the finding, and -the pattern is structural: **a rule is added per fact, so the SET of facts is -what drifts.** Nothing derived that set from -[src/topology_report.rs](src/topology_report.rs), so a field added to the -renderer was unread until a person happened to notice. - -The clearest demonstration is gap 6. It was created BY the hand-written rule -that closed gap 2: M2.11 added a comparison for the partitioning discriminator, -that arm renders a level number as well, and nothing compared the number. Closing -a gap added a fact, and the new fact was unread by exactly the mechanism that -produced the previous five. - -`every_fact_the_renderer_publishes_is_accounted_for` in -[tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) -is the answer, and its shape matters more than its code. Both halves come from -places that cannot fall out of step: - -- **The set of facts is enumerated from the artifact** -- the NDJSON line of a - report this host really rendered. Not a list in the test. A new field appears - in the enumeration by itself, without anyone maintaining anything. -- **Read-or-unread is measured, not declared** -- each value is corrupted in turn - and the oracle is asked. A rule that quietly stops reading a fact is caught - even though no list changed. - -Only the classification of each key is written down, because which of the three -kinds a key belongs to is a judgement: compared, conditional on its prose being -rendered, or having no second rendering at all. **A key in no list fails**, and -that is the whole point -- adding a fact to the renderer forces the judgement to -be made deliberately instead of being discovered by the seventh reviewer. - -This is the same discipline the oracle itself rests on, turned on the oracle: -relate two things that already exist rather than restate one of them. A second -list of "facts the rules look at" would have been another copy to drift, which -is the defect rather than the fix. - -**The limit, stated because a reader will otherwise assume it is closed.** The -enumeration is keyed to the machine-readable line, because that is the side with -an enumerable structure. A fact rendered only in PROSE, with no field beside it, -is invisible here. Prose is not enumerable without parsing English, so that half -remains a thing only a reader notices -- and saying so is better than implying a -coverage that does not exist. -### The instrument is code too, and it is where the defects were - -Eleven review rounds across six models ran over this branch. Classifying every -finding by what would have caught it earlier is more useful than the findings -themselves, because the classes are very unevenly sized. - -**The largest class by far is shape blindness**, and it has one root: every -instrument here was validated against a single artifact, the developer machine's. -This host produces exactly one shape -- measured, `agree`, the `Level` -partitioning arm, non-empty caches, classes `[0]`, zero anomalies, zero -`not_compared`, x86_64 -- and every defect in that class lived in the complement -of it. The architecture uncompared on an unmeasured report, `efficiency_classes` -comparing contents so a scalar and a one-element list were identical, the anomaly -count unread under two of three verdicts, the expected fact name differing on the -`SummaryMissing` arm, an empty container that could not be corrupted: none of -them can occur here, and each was found only because a reviewer imagined a shape -by hand. [CHECKLIST.md](CHECKLIST.md) -> M2.12 is the structural answer, and it -is M2.10's move one level up: derive the set of SHAPES from the renderer's -branches, as M2.10 derives the set of FACTS from the artifact. - -**The second observation is the one worth carrying to other crates.** Most of -these defects were not in the oracle. They were in the guard that checks the -oracle, the table of required facts, the fact name the guard expects, the -declaration of when silence is legitimate. The thing under test came through the -last rounds clean; the things doing the testing did not. - -An instrument feels like it sits outside the system under test, so it escapes the -discipline applied to production code -- and then it fails on a CI runner -reporting a defect in the renderer that is really a defect in the instrument, -which is worse than no check at all because it sends the reader to the wrong -place. Instruments need what production code gets: derivation instead of -restatement, sabotage before they are believed, and a stated boundary. - -**And a fix is new code.** Twice on this branch the fix for one gap created the -next: M2.11's discriminator rule gave the level a second fact name, which broke -the guard that expected one; M2.10's accounting inherited a silence assumption -that only holds under an `agree` verdict. After a fix, re-run the derivation or -the sweep -- not only the test that prompted it. -### Mutation testing is how the instruments got checked, and what it cannot reach - -Every defect on this branch was found by a person reading code -- reviewers, -mostly, and this file records how often they found the same shape. Late on, the -obvious question got asked: is there a mechanical way to ask whether a test -establishes anything, rather than trusting that it does? - -There is, and it was already installed. `cargo-mutants` changes the source and -asks whether anything notices, which is the sabotage loop this crate has been -running by hand all along, done exhaustively. It answers a stronger question than -coverage: not *was this branch executed* but *does anything DETECT a change to -it*. On a branch whose recurring defect is a test that runs code without -establishing anything about it, that difference is the whole point. - -Three sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1): - -| file | tested | caught | unviable | survivors | -|---|---|---|---|---| -| `topology_report.rs` | 28 | 28 | 0 | 2, then none | -| `report_oracle.rs` | 143 | 138 | 5 | 6, then none | -| `topology.rs` | 186 | 180 | 6 | none, first run | - -The unviable column is why a caught-count does not equal a tested-count: those -mutants did not compile, so they say nothing either way. An earlier version of -this table gave only the caught figures, and the commit that wrote it summed them -to a total that matched neither -- 357 were tested and 346 caught, and it claimed -352. Stated in full here so the arithmetic is checkable rather than asserted, -which is the same rule this branch keeps having to relearn about numbers. - -**The survivor worth remembering is `assert_corresponds`.** Replacing its body -with `()` survived, because every instrument that would notice goes THROUGH it -- -both renderers are bound to it, the real-host test calls it, the corpus reaches -it by rendering -- so a no-op assertion makes all of them pass together. Nothing -asserted that the assertion asserts. That is a blind spot no amount of adding -tests *through* an instrument can find, and it is exactly what a tool that -attacks the code rather than the tests is for. - -The others were smaller and of one kind: guards that only fire on malformed -input, which every fixture was too well-formed to reach. A `> 0` that stops an -empty value being reported as a value; the `&&` that makes `!!...!!` a shape -requiring both ends; a string branch whose only caller always passes an array. - -**What it cannot reach, and this matters here.** `cargo-mutants` mutates `src/`, -not `tests/`. The accounting table, the shape corpus and the fact-name guard all -live in `tests/`, so the tool validates the code they check and says nothing -about THEM. The instruments remain exactly as good as the hand-sabotage that -built them -- which is where several of this branch's defects were found, and -where the next one will be. A clean sweep is evidence about the oracle, not about -the things measuring it. ## The encoded row is the contract; the prose is not @@ -1482,14 +1069,14 @@ So they carry different obligations: nothing here checks that it is. This supersedes the rule in -[#d-oracle-refuses-to-know](#d-oracle-refuses-to-know), which said the oracle +[#d-oracle-refuses-to-know](DESIGN-RATIONALE.md#d-oracle-refuses-to-know), which said the oracle must read the rendered artifact rather than the state behind it. ### What forced it: both originating defects were defects in the row The reason the earlier rule looked right was a misreading of its own evidence. Re-checked against the code, for the two defects in -[#d-correspondence-failures](#d-correspondence-failures): +[#d-correspondence-failures](DESIGN-RATIONALE.md#d-correspondence-failures): **The alarm beside the agreeing verdict.** `report` emits `BUG IN THIS PROBE: ...` with a `writeln!` into the prose, and the NDJSON row has diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md new file mode 100644 index 000000000..5bc8fc145 --- /dev/null +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -0,0 +1,456 @@ +# Design rationale: windows-platform-probes + +Tier 2. **How decisions were reached** -- the investigations behind them, the +alternatives weighed, and the reasoning that has since been superseded. The +current decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md), which is +authoritative; where the two disagree, Tier 1 wins. + +Split from DESIGN-NOTES.md at ba786ce. + +Read this for "why did it end up like this", never for "what is it now". Several +sections below describe code that no longer exists -- the prose-reading oracle +and its instruments were retired by M3.4 and M3.5 -- and they are kept because +the reasoning is what a future reader needs when the same question comes round +again, not because the code is still there. + +--- + +## The defects that survived were correspondence failures, and no instrument here could see them + + + +**The diagnosis here is refined by [The encoded row is the contract; the prose is +not](#d-encoded-row-is-the-contract).** What each instrument could not see is +unchanged and is still the reason this component has an oracle at all. What this +section got wrong is WHERE the two defects lived: both were defects in the +ENCODED ROW, not in the relation between two renderings of a consistent state. + +This probe was reviewed twenty-eight times before it opened as a pull request, +by two independent readers per round on different models, with `cargo-mutants` +reporting **zero surviving mutants** on both of its modules. A review on the +pull request then found, in code none of that had touched, a state where the +renderer printed + +``` +BUG IN THIS PROBE: the topology crate named L3 as the outermost +partitioning cache and this survey carries no summary for it. Nothing +below about cache partitioning can be trusted. +``` + +while `cross_check` had no branch for that state at all, so `verdict()` could +return `Agree` for the same run and print `=> agree` two paragraphs below. A +second finding in the same review had the same shape: one fact rendered twice +in one report -- `efficiency classes: [0]` in prose, `"efficiency_classes":1` +in the NDJSON -- in two shapes a consumer cannot reconcile, where the numeral +happens to read as a plausible class *label*. + +Neither is a bug inside a function. Every function involved was correct on its +own terms, and each had been read repeatedly and found so. The defect lived in +the **relation between two artifacts**, and that is a place none of the +instruments in use could look. + +### Why each instrument was structurally incapable, not merely unlucky + +**Mutation testing cannot find absent code.** `cargo-mutants` perturbs what is +written and asks whether a test notices. A missing branch has no mutants, so +the missing `SummaryMissing` check did not lower the score -- it was invisible +to it. The 180/0 result was true and said nothing about the gap. A perfect +mutation score is compatible with an entirely missing feature, and this +component is the proof. + +The same run also shows the weaker half of what a mutation score means. A test +existed asserting `"efficiency_classes":2`, so every mutant of that line died. +It was pinning the wrong shape faithfully. **Mutation testing measures whether +behavior is pinned by tests; it is silent on whether the pinned behavior is +right.** Both halves were over-read here for many rounds as though they were +evidence of correctness. + +**Exhaustiveness checking protects `match` expressions, not concepts.** +`PartitioningCache` exists precisely to force a decision -- its own doc says a +renderer or serialiser "cannot emit the absent case without having decided +which absent case it is" -- and it worked, in the two consumers that wrote a +`match`. It bought nothing in the two that did not: `domain_counts` reached the +same information through `outermost_partitioning_cache`, a second accessor +returning `Option`, which launders five states into two; and `cross_check` +never asked. A type can only compel a consumer that consults it. + +**Per-artifact review finds per-artifact defects.** Two readers checking each +function against its own documentation will confirm both sides of a +contradiction, because each side is locally true. Worse, the readers were +answering questions posed in a prompt, and across rounds that prompt +accumulated focus areas and "already verified, do not re-litigate" facts. The +shared prompt correlated the readers far more strongly than their differing +models decorrelated them; the instrument was being shaped to agree with its +author. Removing that framing in the final round is what got a reader to trace +`simultaneous_multithreading` out of this crate into `windows-topology-sys` and +check it against the Win32 `LTP_PC_SMT` contract. + +The single sentence that covers all three: **every instrument in use verified +properties of things that exist.** Tests assert existing behavior, mutation +perturbs existing code, reviewers check written claims. A correspondence +failure is a property of a *pair*, and an absent branch is not a thing at all. + +### Integration-level analysis was absent, which is where these live + +At the time of the pull request the crate had one integration test, asserting +that a probe writes something to stdout. Of twenty-five `report()` calls in the +suite, **none rendered from a real host's `measure()`** -- every one used a +synthetic `Observation` built by hand. A hand-built fixture can only contain +states its author already imagined, and each assertion checked one local fact +about it. Nothing anywhere rendered the artifact a consumer actually reads and +asked whether it was self-consistent. + +### What to do instead: a sparse matrix to explore with, an oracle to keep + +The obvious response -- tabulate every state against every consumer and fill +the grid -- is wrong, and was proposed and rejected during this analysis. Such +a table grows combinatorially, most of its cells are meaningless, and a version +of it committed beside the code would be a second copy of the code's structure +that nothing verifies. It would rot exactly as every restatement in this +component rotted, and a stale "all cells covered" table is more dangerous than +no table. + +The division that does work: + +- **The matrix is a transient, exploratory instrument.** Draw it for one type + at one boundary to find out which correlations exist. It is expected to be + **sparse**; most cells are empty and discovering that is cheap. Correlations + cannot be derived -- which is why twenty-eight rounds of reading produced + none -- so populating it is exploration, not specification. +- **An oracle is the durable artifact.** Only cells that turn out to mean + something graduate into it. It stays small because discovery, not + enumeration, fills it. + +`windows-file-watcher`'s `ContractChecker` is this repository's worked example +of the oracle half: a shared executable definition of the rules, owned by the +crate that owns the contract, that the producing crate's own tests and every +consumer's test doubles all bind to. It already existed while this probe was +being written, and was not reached for. + +Every correlation admitted here is one the report already renders twice, with +nothing relating the two -- a property of the artifact rather than of anyone's +intuition about it. The ones this decision was written against are: + +1. an alarm in the report implies the verdict is not `agree`; +2. a fact rendered twice must agree across its renderings; +3. an uncaveated hardware claim implies `!parse_in_doubt`; +4. the banner names the same machine the body describes. + +That list is the seed, not the census: the admission RULE is what governs, and +the authoritative set is the `Correspondence` enum in +[src/report_oracle.rs](src/report_oracle.rs). An earlier version of this +paragraph said "three correlations" and listed the first three while the enum +already had the fourth -- the count was wrong when written and would have +rotted again at the next addition, so it is stated as a rule here instead. + +The replacement rule was then itself overstated, as "known to be real because it +was violated" -- which excludes the correspondences found by the M2.4 matrix, +where no defect had occurred and walking every NDJSON field against the prose is +what showed the fact rendered twice with nothing comparing it. Both routes are +admissible; what is not is inventing a correspondence between things the report +does not actually render twice. Corrected the same day it was written, after a +review noticed it contradicted `check_structured_pairs`' own history. + +What makes an oracle different from more tests is where it is invoked: if every +test renders *through* it, every existing call site inherits the checks and so +does every future one. A test added beside them checks one case; an oracle +checks every case anyone ever writes. (Also stated without a number on purpose +-- this said "all twenty-five existing call sites" and there are now 35.) + +**Record the vacuous findings too.** "We examined whether X and Y must +correspond, and they need not" is a result, and it is the half that normally +evaporates -- without it the next person re-explores the same empty cells. + +An oracle is a forcing function for correlations already discovered. It will +not find a new one. The discipline that makes it compound is that each newly +found cross-artifact contradiction adds an invariant to the oracle rather than +a one-off test. + +Whether this generalises to `Coherence`, `BracketOutcome`, `Verdict` and the +sibling probes is **an open question, deliberately not answered here.** The work +this decision implies is queued as M2 in [CHECKLIST.md](CHECKLIST.md); this +section schedules nothing on its own. + +## The oracle exists, and what it deliberately refuses to know + + + +**The artifact-reading rule below is superseded by [The encoded row is the +contract; the prose is not](#d-encoded-row-is-the-contract).** The rest of this +section -- what the oracle admits, the acceptance half, the failure mode that +looks like success -- still describes what is in the tree and still holds. + +M2.1 built it: [src/report_oracle.rs](src/report_oracle.rs), admitting only +correlations the report already renders twice. The defect that forced +it is the section above. + +**It reads the rendered artifact, never the state behind it.** Checking state +would miss precisely this defect class -- in the original finding the state was +consistent and the two *renderings* of it were not. + +That last sentence is the superseded one, and it is wrong about its own +evidence. Re-checked against the code: the alarm has no NDJSON key, and +`cross_check` does -- so the original finding was a run whose ENCODED ROW said +`agree` while the probe had detected its own bug, and published nothing about +that bug. The state was not consistent; the row was wrong. See +[#d-encoded-row-is-the-contract](#d-encoded-row-is-the-contract). + +**It relates two things already visible in the report, and re-derives nothing.** +A second implementation of the rendering rules would be a check of the copy +rather than of the contract, and would drift the moment either moved. So the +alarm rule compares an alarm line against a verdict line, the double-rendering +rule compares prose against NDJSON, and the gating rule compares a claim against +the report's own published evidence of doubt. + +That last one is the interesting boundary. `CrossCheck::parse_in_doubt` is +`!disagreements.is_empty() || !parse_incomplete.is_empty()`, and the NDJSON +publishes `parse_incomplete` as a **list of conditions** rather than the +predicate -- so the oracle reads whether that list is empty, together with the +`disagree` verdict, and those are the two visible shadows of that definition. The +coupling is deliberate, and confirming it still holds is what M2.2's sabotage +check is for when the call sites are bound. + +(This said "as a **count**", which M3.1 made false when the three diagnostic +fields began publishing their conditions. The shape of the argument is +unchanged -- the row still renders a shadow of the predicate rather than the +predicate -- but the shadow is now a list, and an emptiness test rather than a +comparison against `0`.) + +**Half the tests assert acceptance**, following +[../windows-file-watcher/src/contract.rs](../windows-file-watcher/src/contract.rs)'s +`ContractChecker`: an alarm beside a non-agreeing verdict is legal and is what +the fix produced, a caveated claim under doubt is legal and is what the renderer +emits on every heterogeneous host with a short parse, and a prose-only report is +silence rather than violation. Over-constraining is the same defect as +under-specifying and fails in the more expensive direction, because noise trains +a reader to ignore the instrument. + +### The failure mode that would look exactly like success + +An oracle whose prose labels do not match the renderer reads nothing, finds +nothing, and passes everything. So the labels were confirmed against a real +`probe-topology` run, and a test corrupts each double-rendered value in turn and +requires a violation -- if a label ever drifts, that test fails rather than the +oracle going quietly blind. + +**The first attempt at that injection silently did nothing**, and is worth +recording because it nearly produced the opposite conclusion. The anchor used +was `cross-check:`, which does not occur -- the real text is `cross-check +against independently read Win32 counters:` -- so the "defective" report was +identical to the clean one, the oracle correctly reported no violation, and the +reading was almost "the oracle is blind". A sabotage that fails to apply is +indistinguishable from an instrument that fails to fire, unless the injection +asserts it changed something. It now does. + +**The mirror-image hazard: a RESTORE that fails to rebuild.** Sabotage work in +this crate is a loop -- break it, run it, put it back, run it again -- and the +put-it-back step has its own way of lying. On Windows, PowerShell's `Copy-Item` +preserves the source file's `LastWriteTime`, so restoring a file from a backup +taken earlier gives it an mtime OLDER than the artifacts built from the +sabotaged version. Cargo fingerprints by mtime, decides nothing has changed, and +reruns the previous binary. Measured here: a restored, correct oracle reported +the fixed defect as still present, and the reading was almost "the fix does not +work" -- the conclusion was only avoided by printing the intermediate values and +finding that the function returned the right answer while the test insisted it +did not. + +After restoring a file by copy, set its timestamp forward +(`(Get-ChildItem ).LastWriteTime = Get-Date`) or rewrite it through a +read-then-write, which stamps it as a matter of course. The general rule is the +same one as above, pointed the other way: **a green result proves nothing until +you know the code you are testing is the code you just wrote.** + +### The real-host test, and the guard that stops it passing for nothing + +[tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) +composes the report the way `probe-topology` does and applies the oracle +explicitly. + +**Why it has to exist.** The oracle's unit tests pin it against fixtures, and a +fixture is a report somebody wrote down -- so a fixture-bound oracle checks +correspondences over states its author already imagined, and the defect it +exists for was a state nobody had imagined. More narrowly, a fixture cannot +notice the *renderer* drifting away from the prose labels the oracle reads: +both sides would still agree with each other. Only the real artifact disagrees. + +Some unit tests in this crate do call `measure()` and so do read this host. +What none of them does is run the **oracle** over a report rendered from that +reading, which is the gap this test closes. On CI it runs across the hosted +runner fleet, a survey of shapes no fixture anticipates. + +**It asserts nothing about this machine, deliberately.** A test expecting a +processor count, a cache level or a verdict would fail on the next runner shape +rather than on a defect, and would have to be loosened until it asserted +nothing. What it checks is that whatever this host produced, the report's parts +agree with each other -- a property every host must satisfy, including one whose +topology cannot be read at all. + +#### The primary assertion can pass having checked nothing + +On a host whose report the oracle cannot parse, every lookup returns `None`, +every comparison is skipped, and +`a_report_rendered_from_this_host_agrees_with_itself` passes having checked +exactly zero correspondences. That is why the second test corrupts each +double-rendered fact in a report **this host really produced** and requires the +oracle to report a violation **naming that fact** -- and asserts first that the +corruption changed the text at all, for the reason recorded above. +Eight facts rather than one, because corrupting a single field would leave the +others unguarded: the renderer could drift away from the oracle's other prose +labels and the test would still pass on the strength of the one that remained. + +**"Naming that fact" is load-bearing, and took three attempts to get right.** +The guard first required only that the violation list was non-empty. That was +strengthened to require a `ProseAndNdjsonDisagree`, with a comment correctly +observing that corrupting the NDJSON `processors` count also trips the +cross-check counter rule -- and then not acting on the observation, because that +counter rule emits precisely that variant. So the strengthened guard still +passed while the reader it was written to protect was blind. + +Measured, by blinding one `DOUBLE_RENDERED` prose label at a time: under the +variant-only assertion, `processors` and `groups` both stayed **green**, masked +by their counter rules, while `packages` went red because nothing else reads it. +Two of the four facts the guard names as required were unchecked by the guard +whose entire purpose is to establish that they are checked. Matching on the +`fact` string closes it, because no neighbouring rule can supply another rule's +fact name. + +The general lesson, which this crate has now paid for four times: **a guard +against vacuity is itself a claim, and is subject to the same discipline as any +other.** Each of the three earlier versions was written to fix the previous +one's vacuity and introduced the next-narrower version of it. The only thing +that has ever settled the question is sabotaging the mechanism the guard is +supposed to protect and watching the guard go red -- never reading the guard and +judging it sufficient. + +### The fact set is derived, because the gaps were never in the rules + +Six unread double-renderings were found on this branch by six different +reviewers, and none by the oracle's own coverage. Each individual gap was real, +and fixing each one by hand was correct -- but the pattern is the finding, and +the pattern is structural: **a rule is added per fact, so the SET of facts is +what drifts.** Nothing derived that set from +[src/topology_report.rs](src/topology_report.rs), so a field added to the +renderer was unread until a person happened to notice. + +The clearest demonstration is gap 6. It was created BY the hand-written rule +that closed gap 2: M2.11 added a comparison for the partitioning discriminator, +that arm renders a level number as well, and nothing compared the number. Closing +a gap added a fact, and the new fact was unread by exactly the mechanism that +produced the previous five. + +`every_fact_the_renderer_publishes_is_accounted_for` in +[tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) +is the answer, and its shape matters more than its code. Both halves come from +places that cannot fall out of step: + +- **The set of facts is enumerated from the artifact** -- the NDJSON line of a + report this host really rendered. Not a list in the test. A new field appears + in the enumeration by itself, without anyone maintaining anything. +- **Read-or-unread is measured, not declared** -- each value is corrupted in turn + and the oracle is asked. A rule that quietly stops reading a fact is caught + even though no list changed. + +Only the classification of each key is written down, because which of the three +kinds a key belongs to is a judgement: compared, conditional on its prose being +rendered, or having no second rendering at all. **A key in no list fails**, and +that is the whole point -- adding a fact to the renderer forces the judgement to +be made deliberately instead of being discovered by the seventh reviewer. + +This is the same discipline the oracle itself rests on, turned on the oracle: +relate two things that already exist rather than restate one of them. A second +list of "facts the rules look at" would have been another copy to drift, which +is the defect rather than the fix. + +**The limit, stated because a reader will otherwise assume it is closed.** The +enumeration is keyed to the machine-readable line, because that is the side with +an enumerable structure. A fact rendered only in PROSE, with no field beside it, +is invisible here. Prose is not enumerable without parsing English, so that half +remains a thing only a reader notices -- and saying so is better than implying a +coverage that does not exist. +### The instrument is code too, and it is where the defects were + +Eleven review rounds across six models ran over this branch. Classifying every +finding by what would have caught it earlier is more useful than the findings +themselves, because the classes are very unevenly sized. + +**The largest class by far is shape blindness**, and it has one root: every +instrument here was validated against a single artifact, the developer machine's. +This host produces exactly one shape -- measured, `agree`, the `Level` +partitioning arm, non-empty caches, classes `[0]`, zero anomalies, zero +`not_compared`, x86_64 -- and every defect in that class lived in the complement +of it. The architecture uncompared on an unmeasured report, `efficiency_classes` +comparing contents so a scalar and a one-element list were identical, the anomaly +count unread under two of three verdicts, the expected fact name differing on the +`SummaryMissing` arm, an empty container that could not be corrupted: none of +them can occur here, and each was found only because a reviewer imagined a shape +by hand. [CHECKLIST.md](CHECKLIST.md) -> M2.12 is the structural answer, and it +is M2.10's move one level up: derive the set of SHAPES from the renderer's +branches, as M2.10 derives the set of FACTS from the artifact. + +**The second observation is the one worth carrying to other crates.** Most of +these defects were not in the oracle. They were in the guard that checks the +oracle, the table of required facts, the fact name the guard expects, the +declaration of when silence is legitimate. The thing under test came through the +last rounds clean; the things doing the testing did not. + +An instrument feels like it sits outside the system under test, so it escapes the +discipline applied to production code -- and then it fails on a CI runner +reporting a defect in the renderer that is really a defect in the instrument, +which is worse than no check at all because it sends the reader to the wrong +place. Instruments need what production code gets: derivation instead of +restatement, sabotage before they are believed, and a stated boundary. + +**And a fix is new code.** Twice on this branch the fix for one gap created the +next: M2.11's discriminator rule gave the level a second fact name, which broke +the guard that expected one; M2.10's accounting inherited a silence assumption +that only holds under an `agree` verdict. After a fix, re-run the derivation or +the sweep -- not only the test that prompted it. +### Mutation testing is how the instruments got checked, and what it cannot reach + +Every defect on this branch was found by a person reading code -- reviewers, +mostly, and this file records how often they found the same shape. Late on, the +obvious question got asked: is there a mechanical way to ask whether a test +establishes anything, rather than trusting that it does? + +There is, and it was already installed. `cargo-mutants` changes the source and +asks whether anything notices, which is the sabotage loop this crate has been +running by hand all along, done exhaustively. It answers a stronger question than +coverage: not *was this branch executed* but *does anything DETECT a change to +it*. On a branch whose recurring defect is a test that runs code without +establishing anything about it, that difference is the whole point. + +Three sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1): + +| file | tested | caught | unviable | survivors | +|---|---|---|---|---| +| `topology_report.rs` | 28 | 28 | 0 | 2, then none | +| `report_oracle.rs` | 143 | 138 | 5 | 6, then none | +| `topology.rs` | 186 | 180 | 6 | none, first run | + +The unviable column is why a caught-count does not equal a tested-count: those +mutants did not compile, so they say nothing either way. An earlier version of +this table gave only the caught figures, and the commit that wrote it summed them +to a total that matched neither -- 357 were tested and 346 caught, and it claimed +352. Stated in full here so the arithmetic is checkable rather than asserted, +which is the same rule this branch keeps having to relearn about numbers. + +**The survivor worth remembering is `assert_corresponds`.** Replacing its body +with `()` survived, because every instrument that would notice goes THROUGH it -- +both renderers are bound to it, the real-host test calls it, the corpus reaches +it by rendering -- so a no-op assertion makes all of them pass together. Nothing +asserted that the assertion asserts. That is a blind spot no amount of adding +tests *through* an instrument can find, and it is exactly what a tool that +attacks the code rather than the tests is for. + +The others were smaller and of one kind: guards that only fire on malformed +input, which every fixture was too well-formed to reach. A `> 0` that stops an +empty value being reported as a value; the `&&` that makes `!!...!!` a shape +requiring both ends; a string branch whose only caller always passes an array. + +**What it cannot reach, and this matters here.** `cargo-mutants` mutates `src/`, +not `tests/`. The accounting table, the shape corpus and the fact-name guard all +live in `tests/`, so the tool validates the code they check and says nothing +about THEM. The instruments remain exactly as good as the hand-sabotage that +built them -- which is where several of this branch's defects were found, and +where the next one will be. A clean sweep is evidence about the oracle, not about +the things measuring it. diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index 8cb25d08e..67bef6163 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-correspondence-failures](DESIGN-NOTES.md#d-correspondence-failures), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#d-correspondence-failures) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md index a64ba47f9..27ba0d45c 100644 --- a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md +++ b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md @@ -5,7 +5,7 @@ Decisions resulting from this session: - [DESIGN-NOTES.md](../DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is not](../DESIGN-NOTES.md#d-encoded-row-is-the-contract) (new, and supersedes the artifact-reading rule recorded in [The oracle exists, and what it deliberately refuses to - know](../DESIGN-NOTES.md#d-oracle-refuses-to-know)). + know](../DESIGN-RATIONALE.md#d-oracle-refuses-to-know)). Work queued from it: [CHECKLIST.md](../CHECKLIST.md) milestone M3. @@ -28,7 +28,7 @@ The oracle's charter names two originating defects, both found by a pull-request after twenty-eight rounds of per-artifact review and a zero-surviving-mutant `cargo-mutants` result had passed over them. They are recorded in [DESIGN-NOTES.md](../DESIGN-NOTES.md) -> [The defects that survived were correspondence -failures](../DESIGN-NOTES.md#d-correspondence-failures). +failures](../DESIGN-RATIONALE.md#d-correspondence-failures). ### Defect 1 -- the alarm beside the agreeing verdict From bd45d63a50c65ce02f8a944362946a64ade4760c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:16:18 -0400 Subject: [PATCH 13/46] docs(platform-probes): add the blank lines two moved headings inherited --- crates/windows-platform-probes/DESIGN-RATIONALE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 5bc8fc145..22a100ff7 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -367,6 +367,7 @@ an enumerable structure. A fact rendered only in PROSE, with no field beside it, is invisible here. Prose is not enumerable without parsing English, so that half remains a thing only a reader notices -- and saying so is better than implying a coverage that does not exist. + ### The instrument is code too, and it is where the defects were Eleven review rounds across six models ran over this branch. Classifying every @@ -405,6 +406,7 @@ next: M2.11's discriminator rule gave the level a second fact name, which broke the guard that expected one; M2.10's accounting inherited a silence assumption that only holds under an `agree` verdict. After a fix, re-run the derivation or the sweep -- not only the test that prompted it. + ### Mutation testing is how the instruments got checked, and what it cannot reach Every defect on this branch was found by a person reading code -- reviewers, From 5fc68d45eed4fa85126c875a614170979a163417 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:45:49 -0400 Subject: [PATCH 14/46] fix(platform-probes): make the row's key reader escape-aware A review of the branch found eight issues. The first is a real defect, reachable through a public renderer, and the rest are claims that outlived the code. **`report_oracle::keys` was not escape-aware, and a correct report crashed the probe.** `balanced` tracks `in_string`/`escaped`; `keys` walked quote to quote with `find('"')` and took `\"` for a terminator. The row legitimately carries caller text -- `report_unmeasured` publishes a failed discovery's `io::Error` as `discovery_error` -- and an OS message may contain a quote. When it does, the reader's idea of where strings begin and end shifts, text INSIDE the value is emitted as top-level keys, and two equal ones are reported as a repeated key. Measured, not reasoned: `report_unmeasured` given `io::Error::other("q\":1,\"q\":1,\"q")` renders a row that parses cleanly with four keys, and `assert_corresponds` panicked FROM INSIDE THE RENDERER -- `report_oracle.rs:295`, reached through `report_unmeasured`'s own binding. Under `cfg(test)` or `oracle-in-renderer` that is a correct report crashing the probe, which is the failure mode the containment work exists to prevent. The writer was never at fault; the reader disagreed with it. `keys` now shares the state machine through `string_end`, and two tests cover both directions -- an escaped quote inside a value, and a value ending in an escaped backslash, which getting wrong the other way would silently drop every key that follows. The same hazard does not reach `list_codes` or `list_span_end`: those scan the diagnostic lists, whose entries carry only codes minted in this crate and numbers. No caller text reaches them. **A completeness guard that was documented and did not exist.** `perturbations()` said "the interesting part is that NONE of them is missing -- which the count assertion below states as a relation". There was no such assertion. The table happened to match `blocking_states`' ten states, which is the condition under which nobody notices. `every_blocking_state_has_a_perturbation` now derives the reached set by applying every perturbation and compares it against the table's labels in both directions, so a state added to the invariant with no entry fails rather than going unexercised. Derived rather than counted, because a census needs correcting every time a state is added. **A test whose name asserted the opposite of its assertion.** `a_defect_is_reported_once_per_repeated_key_rather_than_per_occurrence` claimed de-duplication the code does not do while asserting the per-occurrence behaviour its own failure message describes. The assertion was right; the name and comment were the defect. Renamed to `..._once_per_extra_rendering_of_a_key`. **Four statements that outlived the code they described**, each verified against the tree before correcting: - DESIGN-NOTES.md said the three fields "publish arrays of condition codes" with a bare-code example. True at M3.1; M3.3 gave each entry its data three commits later on the same branch, so the published form is an object. Tier 1 is where a reader goes for the wire format, so it is the one place that must be right. - CHECKLIST M3.5 said the instrument "holds the prose's diagnostic line count against the row's condition count", and cited a sabotage against `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists`. Both were invalidated by the follow-up commit recorded further down the same item, which removed the prose count and renamed the test -- so the item disagreed with itself and its evidence could not be re-run as written. The sabotage was re-run against the current name to confirm it still reddens. - DESIGN-RATIONALE.md said the oracle section "still describes what is in the tree and still holds", then listed three rules M3.4 deleted. True when written, carried through the Tier 1/Tier 2 split unchanged. - Three same-file anchor links in DESIGN-RATIONALE.md still pointed at `#d-encoded-row-is-the-contract`, which the split left in DESIGN-NOTES.md. **One doc comment attached to the wrong item.** Two comments had merged onto `codes_of`, so the paragraph warning against asserting on the struct sat above the helper that reads the struct. Moved to `row_codes`, which had none. Verified: 205 lib tests plus 14 ignored, 9 integration tests and doctests, on both `x86_64-pc-windows-msvc` and `i686-pc-windows-msvc`. clippy --all-targets --all-features clean, fmt clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 15 ++++- .../windows-platform-probes/DESIGN-NOTES.md | 18 +++++- .../DESIGN-RATIONALE.md | 22 +++++-- .../src/report_oracle.rs | 58 ++++++++++++++++--- .../src/report_oracle/tests.rs | 48 +++++++++++++-- crates/windows-platform-probes/src/tests.rs | 15 +++-- .../src/topology/invariant/tests.rs | 56 +++++++++++++++++- 7 files changed, 200 insertions(+), 32 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index eee5f1187..738f987ee 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -276,12 +276,21 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. **Done, with M3.4, and the second enumeration exists.** The instrument no longer asks "which prose facts does the oracle read" -- there are none. It asks, for every state `topology::invariant` knows - forbids agreement, whether the row publishes a condition for it; and it holds the prose's - diagnostic line count against the row's condition count across the corpus. + forbids agreement, whether the row publishes a condition for it; and it holds the row's published + conditions against what the cross-check found, across the corpus. + + (As first written this said the second rule held the row against a count of PROSE lines, which it + did at the time. The follow-up commit that removed the last prose parsing replaced that with the + comparison against the cross-check -- recorded further down this same item, so the item disagreed + with itself. Found by a review.) Sabotage-verified against the defect that motivated it: dropping `disagreements` from the row -- the omission that survived 41 review rounds, a zero-survivor mutation sweep and the old accounting - -- now reddens `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists`. + -- now reddens the rule that holds the row against the cross-check. That rule was named + `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists` when this evidence was recorded + and is `the_row_lists_exactly_the_conditions_the_cross_check_found` now; the sabotage was re-run + against the current name. Recorded evidence that cannot be re-run as written is evidence nobody + will re-run. **One asymmetry, found by the instrument rather than reasoned.** Counting all four lists against prose lines failed: the prose folds every anomaly into ONE diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index 2162a4a6f..ee7c6537b 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1104,9 +1104,21 @@ that gets mined carried less than the artifact that gets read -- which is backwards given which of the two the designs rest on. **M3.1 closed this**, and the past tense above is deliberate: the three fields -now publish arrays of condition codes minted by `topology::diagnostic`, so -`"parse_incomplete":["partitioning_summary_missing"]` tells a survey which -condition fired. The count remains available as the list's length. The rest of +publish the conditions themselves, minted by `topology::diagnostic`, so a survey +reads which one fired rather than how many there were. The count remains +available as the list's length. + +M3.3 then gave each entry its DATA, so the published form is an object rather +than a bare code: + +``` +"parse_incomplete":[{"code":"partitioning_summary_missing","level":9}] +``` + +Stated here because this is Tier 1 and the wire format is what a reader comes to +it for. The bare-code form this paragraph first showed was M3.1-era and was +superseded three commits later on the same branch -- the drift class this +component keeps meeting, caught by a review. The rest of this decision is unaffected -- it is about which artifact carries the contract, not about these three fields. diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 22a100ff7..b1decad45 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -20,7 +20,7 @@ again, not because the code is still there. **The diagnosis here is refined by [The encoded row is the contract; the prose is -not](#d-encoded-row-is-the-contract).** What each instrument could not see is +not](DESIGN-NOTES.md#d-encoded-row-is-the-contract).** What each instrument could not see is unchanged and is still the reason this component has an oracle at all. What this section got wrong is WHERE the two defects lived: both were defects in the ENCODED ROW, not in the relation between two renderings of a consistent state. @@ -175,10 +175,20 @@ section schedules nothing on its own. -**The artifact-reading rule below is superseded by [The encoded row is the -contract; the prose is not](#d-encoded-row-is-the-contract).** The rest of this -section -- what the oracle admits, the acceptance half, the failure mode that -looks like success -- still describes what is in the tree and still holds. +**Superseded by [The encoded row is the contract; the prose is +not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), and the code it describes no +longer exists.** Everything below is a record of what was built and why, in the +past tense whatever its grammar says: M3.4 deleted the prose correspondences, +the `Correspondence` enum and the twenty-three extraction helpers, and M3.5 +replaced the fact-accounting instrument. `report_oracle` today is a +well-formedness check on the row and nothing more. + +This paragraph read "the rest of this section ... still describes what is in the +tree and still holds", which was true when it was written -- before M3.4, three +commits earlier on the same branch -- and was carried through the Tier 1 / Tier 2 +split unchanged. Found by a review. It is the same drift this component keeps +paying for, and it is worth leaving the correction visible rather than quietly +deleting the sentence. M2.1 built it: [src/report_oracle.rs](src/report_oracle.rs), admitting only correlations the report already renders twice. The defect that forced @@ -193,7 +203,7 @@ evidence. Re-checked against the code: the alarm has no NDJSON key, and `cross_check` does -- so the original finding was a run whose ENCODED ROW said `agree` while the probe had detected its own bug, and published nothing about that bug. The state was not consistent; the row was wrong. See -[#d-encoded-row-is-the-contract](#d-encoded-row-is-the-contract). +[#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract). **It relates two things already visible in the report, and re-derives nothing.** A second implementation of the rendering rules would be a check of the copy diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index cc998a406..9e2f01c71 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -181,10 +181,13 @@ fn balanced(row: &str) -> bool { pub fn keys(row: &str) -> Vec<&str> { let mut names = Vec::new(); let mut depth = 0_i32; - let mut rest = row; + let mut at = 0; - while let Some(quote) = rest.find('"') { - for character in rest[..quote].chars() { + while let Some(open) = next_string(row, at) { + // Brackets BETWEEN strings are the only ones that count. Inside a + // string they are text -- a failed discovery's message may contain any + // of them. + for character in row[at..open].chars() { match character { '[' | '{' => depth += 1, ']' | '}' => depth -= 1, @@ -192,12 +195,11 @@ pub fn keys(row: &str) -> Vec<&str> { } } - let after = &rest[quote + 1..]; - let Some(end) = after.find('"') else { + let Some(close) = string_end(row, open + 1) else { break; }; - let name = &after[..end]; - let tail = after[end + 1..].trim_start(); + let name = &row[open + 1..close]; + let tail = row[close + 1..].trim_start(); // A name followed by `:` at depth 1 is a key of the row itself. Anything // else is a value, or a key of a nested object. @@ -205,12 +207,52 @@ pub fn keys(row: &str) -> Vec<&str> { names.push(name); } - rest = &after[end + 1..]; + at = close + 1; } names } +/// Where the next string starts at or after `from`. +/// +/// There is nothing to skip here -- a quote outside a string always opens one -- +/// but it is named so the pair with [`string_end`] reads as a scan rather than +/// as two bare `find` calls. +fn next_string(row: &str, from: usize) -> Option { + row[from..].find('"').map(|at| from + at) +} + +/// Where the string opening before `from` closes, honouring `\` escapes. +/// +/// **This is the half [`keys`] was missing, and it was reachable.** `keys` used +/// `find('"')`, which takes `\"` for a terminator -- so a `discovery_error` +/// carrying an escaped quote shifted the parser's idea of where strings begin +/// and end, and text INSIDE the error was emitted as top-level keys. Two equal +/// ones then read as a repeated key. +/// +/// Measured before this fix: `report_unmeasured` given an `io::Error` of +/// `q":1,"q":1,"q` rendered a row `JSON.parse` accepts with four keys, and +/// `assert_corresponds` panicked from inside the renderer -- a correct report +/// crashing the probe, which is the failure mode containment exists to prevent. +/// [`balanced`] already had this state machine; `keys` did not, so the reader +/// disagreed with the writer. +fn string_end(row: &str, from: usize) -> Option { + let mut escaped = false; + for (at, character) in row[from..].char_indices() { + if escaped { + escaped = false; + continue; + } + match character { + '\\' => escaped = true, + '"' => return Some(from + at), + _ => {} + } + } + + None +} + /// Where the list opening at `start` closes, if it closes. /// /// Depth-aware, because each entry is an object: the first `]` after the opening diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index 4f0561ad0..f57637c01 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -83,6 +83,40 @@ fn an_escaped_quote_does_not_end_a_string() { assert_eq!(check(&report_with(escaped)), Vec::new()); } +#[test] +fn an_escaped_quote_inside_a_value_does_not_forge_a_key() { + // **A correct report crashed the probe, and this is the shape that did it.** + // `keys` used `find('"')`, which takes `\"` for a terminator, so an escaped + // quote shifted where it thought strings began and ended and text INSIDE a + // value was emitted as a top-level key. Two equal ones read as a repeated + // key, and `assert_corresponds` panicked from inside `report_unmeasured`. + // + // Reachable, not hypothetical: `discovery_error` carries a failed + // discovery's `io::Error`, whose message is whatever the OS said. + let forged = concat!( + r#"{"reason":"x-probe-topology","arch":"x86_64","#, + r#""discovery_error":"q\":1,\"q\":1,\"q"}"# + ); + + assert_eq!( + keys(forged), + vec!["reason", "arch", "discovery_error"], + "the error's contents are a VALUE, however many quotes it contains" + ); + assert_eq!(check(&report_with(forged)), Vec::new()); +} + +#[test] +fn a_backslash_before_the_closing_quote_does_not_swallow_the_rest_of_the_row() { + // The other half: a value ending in an escaped backslash closes normally, + // so the keys after it are still found. Getting this wrong in the other + // direction would silently drop every key that follows. + let row = r#"{"reason":"x","path":"C:\\temp\\","cross_check":"agree"}"#; + + assert_eq!(keys(row), vec!["reason", "path", "cross_check"]); + assert_eq!(check(&report_with(row)), Vec::new()); +} + #[test] fn a_repeated_key_is_a_defect() { // Not a parse error in most readers -- they take the last -- so this is @@ -142,10 +176,16 @@ fn the_row_accessor_declines_an_ambiguous_or_malformed_report() { } #[test] -fn a_defect_is_reported_once_per_repeated_key_rather_than_per_occurrence() { - // Three renderings of one key is one defect about that key, not two. A rule - // that reported per occurrence would make a reader count to work out how - // many keys were involved. +fn a_defect_is_reported_once_per_extra_rendering_of_a_key() { + // **One entry per EXTRA rendering, so the count reads as how many times the + // row said it again.** Three renderings of `a` give two defects, not one and + // not three. + // + // This was named `..._once_per_repeated_key_rather_than_per_occurrence` and + // opened by claiming de-duplication the code does not do -- while asserting + // the per-occurrence behaviour its own failure message describes. A reader + // taking the name for the contract got it backwards. The assertion was + // right; the name and the comment were the defect. let thrice = r#"{"a":1,"a":2,"a":3,"b":1,"b":2}"#; assert_eq!( diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index e1bdc41f0..a2da393cf 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4527,18 +4527,16 @@ fn preparing_a_path_needs_no_volume_behind_its_drive_letter() { // --- M3.1: the row names each condition, rather than counting them ----------- -/// The codes the row publishes for `key`, in order. -/// -/// Reads the rendered artifact rather than the `CrossCheck` behind it, because -/// what a survey receives is the point: an assertion against the struct would -/// hold even if the writer published nothing at all. /// The codes of a diagnostic list, for comparing the row against what the /// cross-check found. /// /// Generic over the three list types because each has its own `code`, and a /// helper per list would be three copies of one idea. fn codes_of(entries: &[T]) -> Vec { - entries.iter().map(|entry| entry.code().to_owned()).collect() + entries + .iter() + .map(|entry| entry.code().to_owned()) + .collect() } /// The three diagnostic vocabularies, so `codes_of` can take any of them. @@ -4564,6 +4562,11 @@ impl HasCode for crate::topology::ParseIncomplete { } } +/// The codes the row publishes for `key`, in order. +/// +/// Reads the rendered artifact rather than the `CrossCheck` behind it, because +/// what a survey receives is the point: an assertion against the struct would +/// hold even if the writer published nothing at all. fn row_codes(text: &str, key: &str) -> Vec { let row = crate::report_oracle::row(text) .unwrap_or_else(|| panic!("no single well-formed row in:\n{text}")); diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index b1fe28607..b3755e53b 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -85,8 +85,14 @@ fn the_verdict_the_crate_actually_draws_holds_every_invariant() { /// Each blocking state, the field that produces it, and the name it reports. /// /// A table rather than a test each, because the property is identical and the -/// interesting part is that NONE of them is missing -- which the count assertion -/// below states as a relation rather than as a census. +/// interesting part is that NONE of them is missing. That completeness is +/// asserted by `every_blocking_state_has_a_perturbation`, which compares this +/// table against what `blocking_states` can actually produce -- so a state added +/// to the invariant with no entry here fails rather than going quietly +/// unexercised. +/// +/// This doc claimed such a relation before one existed. Found by a review: the +/// table happened to match, which is the condition under which nobody notices. type Perturbation = (&'static str, Box); fn perturbations() -> Vec { @@ -153,6 +159,52 @@ fn perturbations() -> Vec { ] } +#[test] +fn every_blocking_state_has_a_perturbation() { + // **The completeness guard the table's doc claimed and did not have.** + // Without it, a state added to `blocking_states` with no entry in the table + // is never shown to fire, and never shown to be one `cross_check` already + // forbids -- which is the property the whole module rests on. + // + // Derived by APPLYING every perturbation and collecting what + // `blocking_states` then reports, rather than by counting the table against + // a number written here. A census would need correcting every time a state + // is added, which is the rot this crate keeps paying for. + let mut reached: Vec<&'static str> = Vec::new(); + for (_, mutate) in perturbations() { + let mut observation = agreeing(); + mutate(&mut observation); + for state in blocking_states(&observation) { + if !reached.contains(&state) { + reached.push(state); + } + } + } + + // Every state the table's own labels name must be among them, and nothing + // the perturbations reach may be unnamed. + let labelled: Vec<&'static str> = perturbations() + .into_iter() + .map(|(state, _)| state) + .collect(); + + for state in &labelled { + assert!( + reached.contains(state), + "`{state}` is a label in the table that no perturbation actually \ + produces, so the row for it tests nothing" + ); + } + for state in &reached { + assert!( + labelled.contains(state), + "`{state}` is reported by `blocking_states` and has no entry in the \ + perturbation table, so nothing shows it fires or that \ + `cross_check` already forbids it" + ); + } +} + #[test] fn every_blocking_state_forbids_an_agreeing_verdict() { for (state, mutate) in perturbations() { From 09da7e940d4be6cabcffae00484b449c6ece6af8 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 00:14:04 -0400 Subject: [PATCH 15/46] fix(probes): make four instruments as strong as their names claim A review of the completed M3 found no wrong behaviour and four weak instruments -- tests and guards asserting a property they could not fail to satisfy. Each is recorded with what it was measured to miss. 1. The row's key set was unenforced. M3.3 claimed the check was "derived" because Row::keys reads the value and a test holds reader against writer -- but both read the same Row, so the test proved only that the writer is self-consistent. Measured: deleting .with("packages", ...) from the renderer left the entire suite green. Declared MEASURED_ROW_KEYS / UNMEASURED_ROW_KEYS as the contract instead. This is not the census M3.4 feared: a count is derivable from the thing it counts, so restating it invites drift; a schema is not derivable from the row, which is why writing it down buys something. 2. The well-formedness oracle accepted invalid JSON. balanced() counted bracket depth only, so {"a":1,} and {"a":1] both passed a check whose name promised a consumer could parse the row. Replaced with malformation(), a typed delimiter stack that also checks separator placement; RowDefect::Unbalanced becomes Malformed { what, row } so the defect names itself. 3. A sabotage asserted only that it had sabotaged. The publication accounting's sabotage stripped a condition from the report and then asserted the condition was absent -- a fact about the string edit, not about the rule, and it would have passed with the rule deleted. The rule is now publication_holds(observation, text), and the sabotage asserts it rejects the stripped report and accepts the original. 4. A completeness guard compared a table against itself. blocking_states returned strings and the guard derived both sides from that one table. Added a BlockingState enum with ALL, so the guard holds the table against the type's variants. Sabotage-verified, each against the instrument it was meant to strengthen: the dropped field reddens the key-set contract; a writer emitting a leading separator produces a balanced-but-invalid row that the old check passed and the new one rejects. Completed item: M3.7: Make four instruments as strong as their names claim Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 46 +++++++- .../src/report_oracle.rs | 106 ++++++++++++++--- .../src/report_oracle/tests.rs | 61 +++++++++- crates/windows-platform-probes/src/tests.rs | 68 ++++++++--- .../src/topology/invariant.rs | 109 +++++++++++++++--- .../src/topology/invariant/tests.rs | 83 ++++++------- .../src/topology_report.rs | 47 ++++++++ .../tests/a_real_report_agrees_with_itself.rs | 33 +++++- 8 files changed, 459 insertions(+), 94 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 738f987ee..25c069455 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -184,8 +184,8 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. error now reaches the row as a `discovery_error` field, so a survey can group failures by cause instead of parsing the prose sentence. - The key-set check M3.4 deferred here now exists, and is derived: `Row::keys` reads the value, and - a test asserts the reader and the writer agree. No census. + The key-set check M3.4 deferred here now exists -- but NOT in the form M3.4 predicted, and the + first attempt at it was vacuous. See the correction recorded under M3.7. **Two silent behaviour changes were caught by checking the old code rather than trusting the rewrite.** `PartitioningCache` has FIVE variants, not the four a rewrite naturally reaches for; @@ -251,11 +251,19 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. malformation that survives a consumer's parse and changes what it reads, since most JSON readers take the last. + (The bracket check was weaker than this sentence implies -- it counted depth, so a trailing or + misplaced separator passed. Strengthened in M3.7.) + **The key-set check is deliberately NOT here.** Asserting it needs a list of expected keys, and a list written here is a census -- this component re-corrected the same census three times in one day. M3.3 makes the row a typed value, at which point the key set is derivable from the type rather than declared beside it. Moved there rather than approximated here. + (**The second sentence is wrong, and M3.7 corrects it.** A key set is NOT derivable from a typed + row: the type says "a row is a map of names to values", which is satisfied by every key set, + including the one missing a field. The census this note was right to fear is a count; a schema is + not one, and refusing to write it down bought nothing.) + - [x] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. **The instrument enumerates in one direction only, and the other direction is where M3.1's rule @@ -368,6 +376,40 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. > decisions is what keeps Tier 1 XL. Whether they want a tier of their own is a structural choice > about this component's documentation scheme, so it is raised rather than taken. +- [x] **M3.7** -- Make four instruments as strong as their names claim. + + A review of the completed M3 found no wrong behaviour and four weak instruments -- tests and + guards whose names assert a property they could not actually fail to satisfy. That is the + recurring defect class of this whole branch, so the four are recorded with what each one was + measured to miss. + + **1. The row's key set was unenforced.** M3.3's note above claimed the check "is derived: + `Row::keys` reads the value, and a test asserts the reader and the writer agree". Both halves + read the same `Row`, so the test says only that the writer is self-consistent. Measured: deleting + `.with("packages", ...)` from the renderer left the ENTIRE suite green -- a field silently + vanishes from every downstream survey and nothing objects. Fixed by declaring + `MEASURED_ROW_KEYS` / `UNMEASURED_ROW_KEYS` as the contract the renderer is held to. This is not + the census M3.4 feared: a count is derivable from the thing it counts, so restating it is drift + waiting to happen; a schema is NOT derivable from the row, which is exactly why writing it down + buys something. + + **2. The well-formedness oracle accepted invalid JSON.** `balanced()` counted bracket depth, so + `{"a":1,}` (trailing separator) and `{"a":1]` (mismatched closer) both passed -- and a consumer + would reject both. Replaced by `malformation()`, a typed delimiter stack that also checks + separator placement. Sabotage-verified: making the writer emit a leading separator produces a + balanced but invalid row, which the old check passed and the new one reddens. + + **3. A sabotage asserted only that it had sabotaged.** The publication-accounting sabotage stripped + a condition from the report and then asserted the condition was absent -- which is a fact about + the string edit, not about the rule. It would have passed with the rule deleted. The rule is now + `publication_holds(observation, text)`, and the sabotage asserts it REJECTS the stripped report + and ACCEPTS the original. + + **4. A completeness guard compared a table against itself.** `blocking_states` returned strings, + and the guard that checked every blocking state was described derived both sides from that one + table. Introduced a `BlockingState` enum with `ALL`, so the guard holds the table against the + type's variants and a new state that nobody describes fails to build past it. + ## M4 -- Carried over from M2: the items M3 gates These were written under M2 and are blocked on M3 above: each one targets the prose-against-row diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 9e2f01c71..4d3edd146 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -66,8 +66,17 @@ pub enum RowDefect { /// How many lines look like a row. count: usize, }, - /// The row's brackets do not balance, so it is not a JSON object. - Unbalanced { + /// The row is not a syntactically valid JSON object. + /// + /// **Structure, not only depth.** This checked that brackets BALANCED, + /// which `{"a":1,}` and `{"a":1]` both satisfy while being invalid JSON -- + /// so a writer defect producing either was accepted by the check whose whole + /// job is to read the writer's output back. Delimiters are now matched by + /// kind, and the separators between members are checked, which covers the + /// defects a hand-written writer actually produces. + Malformed { + /// What is wrong, as specifically as the scan can say. + what: &'static str, /// The row, as rendered. row: String, }, @@ -91,8 +100,8 @@ impl std::fmt::Display for RowDefect { "the report carries {count} machine-readable rows, so which one \ is the contract is ambiguous" ), - Self::Unbalanced { row } => { - write!(f, "the row's brackets do not balance: {row}") + Self::Malformed { what, row } => { + write!(f, "the row is not a valid JSON object -- {what}: {row}") } Self::RepeatedKey { key } => write!( f, @@ -121,8 +130,9 @@ pub fn check(report: &str) -> Vec { let mut found = Vec::new(); - if !balanced(row) { - found.push(RowDefect::Unbalanced { + if let Some(what) = malformation(row) { + found.push(RowDefect::Malformed { + what, row: (*row).to_owned(), }); // Every check below reads the object's members, which is not a question @@ -144,32 +154,90 @@ pub fn check(report: &str) -> Vec { found } -/// Whether every bracket in `row` is closed, in order. -fn balanced(row: &str) -> bool { - let mut depth = 0_i32; +/// What is wrong with `row` as a JSON object, if anything. +/// +/// **Delimiters matched by KIND, and separators checked.** This only counted +/// depth, so `{"a":1,}` and `{"a":1]` were both accepted -- each invalid JSON, +/// each exactly the kind of defect a hand-written writer produces, and each +/// silently passing the check whose whole job is reading that writer's output +/// back. Found by a review. +/// +/// Not a full JSON parser, and it does not need to be: the producer is +/// `crate::row`, so the reachable failures are a mismatched delimiter, a stray +/// or missing separator, and an unterminated string. Numbers and keywords come +/// from typed values and cannot be malformed. +fn malformation(row: &str) -> Option<&'static str> { + let mut stack: Vec = Vec::new(); let mut in_string = false; let mut escaped = false; + // The last structurally significant character outside a string, so a + // separator with nothing on one side of it is visible. + let mut previous = '\0'; for character in row.chars() { if escaped { escaped = false; continue; } + if in_string { + match character { + '\\' => escaped = true, + '"' => { + in_string = false; + previous = '"'; + } + _ => {} + } + continue; + } + match character { - '\\' if in_string => escaped = true, - '"' => in_string = !in_string, - '[' | '{' if !in_string => depth += 1, - ']' | '}' if !in_string => { - depth -= 1; - if depth < 0 { - return false; + '"' => { + if previous == '"' || previous == '}' || previous == ']' { + return Some("a string follows a value with no separator"); } + in_string = true; } - _ => {} + '[' | '{' => { + stack.push(character); + previous = character; + } + '}' | ']' => { + let opened = stack.pop(); + let expected = if character == '}' { '{' } else { '[' }; + if opened != Some(expected) { + return Some("a closing delimiter does not match the one it closes"); + } + if previous == ',' { + return Some("a trailing separator before a closing delimiter"); + } + previous = character; + } + ',' => { + if previous == ',' || previous == '{' || previous == '[' || previous == '\0' { + return Some("a separator with no value before it"); + } + previous = ','; + } + ':' => { + if previous != '"' { + return Some("a name separator that does not follow a name"); + } + previous = ':'; + } + character if character.is_whitespace() => {} + _ => previous = 'v', } } - depth == 0 && !in_string + if in_string { + return Some("an unterminated string"); + } + if !stack.is_empty() { + return Some("an unclosed delimiter"); + } + + None } /// Every key `row` renders at its top level, in the order it renders them. @@ -324,7 +392,7 @@ pub fn list_codes(row: &str, key: &str) -> Vec { pub fn row(report: &str) -> Option<&str> { let mut rows = report.lines().filter(|line| line.starts_with('{')); let row = rows.next()?; - (rows.next().is_none() && balanced(row)).then_some(row) + (rows.next().is_none() && malformation(row).is_none()).then_some(row) } /// [`check`], as an assertion, for tests that render a report. diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index f57637c01..177393944 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -51,17 +51,74 @@ fn a_report_with_two_rows_is_a_defect() { } #[test] -fn an_unbalanced_row_is_a_defect() { +fn an_unclosed_delimiter_is_a_defect() { let truncated = r#"{"reason":"x-probe-topology","caches":[{"level":1}"#; assert_eq!( check(&report_with(truncated)), - vec![RowDefect::Unbalanced { + vec![RowDefect::Malformed { + what: "an unclosed delimiter", row: truncated.to_owned() }] ); } +#[test] +fn a_trailing_separator_is_a_defect() { + // **Balanced but invalid**, which the depth-only check accepted. A writer + // that emitted a separator for a member it then skipped produces exactly + // this, and every bracket still matches. + let trailing = r#"{"reason":"x-probe-topology","arch":"x86_64",}"#; + + assert_eq!( + check(&report_with(trailing)), + vec![RowDefect::Malformed { + what: "a trailing separator before a closing delimiter", + row: trailing.to_owned() + }] + ); +} + +#[test] +fn a_mismatched_closing_delimiter_is_a_defect() { + // Also balanced by depth, also invalid: an object closed by a bracket. + let mismatched = r#"{"reason":"x-probe-topology","arch":"x86_64"]"#; + + assert_eq!( + check(&report_with(mismatched)), + vec![RowDefect::Malformed { + what: "a closing delimiter does not match the one it closes", + row: mismatched.to_owned() + }] + ); +} + +#[test] +fn a_nested_list_closed_as_an_object_is_a_defect() { + // The inner case, so the stack is shown to be a stack rather than a pair of + // counters that happen to agree at the end. + let mismatched = r#"{"reason":"x","caches":[{"level":1}}}"#; + + assert!( + matches!( + check(&report_with(mismatched)).as_slice(), + [RowDefect::Malformed { .. }] + ), + "{:?}", + check(&report_with(mismatched)) + ); +} + +#[test] +fn a_brace_inside_a_string_does_not_confuse_the_delimiter_stack() { + // The acceptance half of the stack: `discovery_error` carries an OS message, + // which may contain any delimiter. Mis-stacking those would report every + // such host as malformed. + let row = r#"{"reason":"x","discovery_error":"failed at {[ and never closed"}"#; + + assert_eq!(check(&report_with(row)), Vec::new()); +} + #[test] fn a_bracket_inside_a_string_does_not_unbalance_a_row() { // **The acceptance half that matters most**, because the renderer does emit diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index a2da393cf..1196d3927 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4802,25 +4802,63 @@ fn the_rendered_row_carries_exactly_the_keys_the_value_declares() { } #[test] -fn the_topology_rows_keys_are_what_the_renderer_declares() { - // The same property on the real row, which is the one a survey reads. - // Asserted as agreement between the reader and the writer rather than - // against a list here, for the reason above. +fn the_measured_row_carries_exactly_the_contracts_keys() { + // **The check that was claimed and was not there.** The previous version + // compared `report_oracle::keys` against `Row::keys` -- the reader against + // the writer -- which says nothing about WHICH keys the contract requires. + // Measured: with `.with("packages", ...)` deleted from the builder, the + // entire suite stayed green. + // + // Compared as an exact SEQUENCE, so a dropped field, an added one and a + // reordered one all fail. Order is part of the contract here because + // accumulated CI output is read by humans as well as machines, and a stable + // order makes a diff between two runs legible. let text = crate::topology_report::report(BANNER, &clean_observation()); let row = crate::report_oracle::row(&text).expect("one well-formed row"); - let published = crate::report_oracle::keys(row); - assert_eq!(published.first().copied(), Some("reason")); - assert!( - published.contains(&"disagreements") && published.contains(&"parse_incomplete"), - "{published:?}" + assert_eq!( + crate::report_oracle::keys(row), + crate::topology_report::MEASURED_ROW_KEYS, + "the measured row must carry exactly the contract's keys: {row}" ); +} + +#[test] +fn the_unmeasured_row_carries_exactly_its_own_contracts_keys() { + // A host whose discovery FAILED publishes a different shape, and that is + // the point -- a survey must be able to tell it from a measured row that + // happens to be missing fields. So it has its own schema rather than being + // checked as a subset of the one above. + let text = crate::topology_report::report_unmeasured( + BANNER, + &std::io::Error::other("the device is not ready"), + ); + let row = crate::report_oracle::row(&text).expect("one well-formed row"); + assert_eq!( - published.len(), - published - .iter() - .collect::>() - .len(), - "no key is published twice: {published:?}" + crate::report_oracle::keys(row), + crate::topology_report::UNMEASURED_ROW_KEYS, + "the unmeasured row must carry exactly its contract's keys: {row}" + ); +} + +#[test] +fn the_two_row_shapes_are_distinguishable_by_their_keys() { + // The guard that keeps the two schemas from drifting into each other. If + // the unmeasured shape ever became a prefix of the measured one, a survey + // reading a truncated measured row could not tell it from a failed + // discovery -- which is the distinction the unmeasured row exists to make. + assert_ne!( + crate::topology_report::MEASURED_ROW_KEYS, + crate::topology_report::UNMEASURED_ROW_KEYS + ); + assert!( + crate::topology_report::UNMEASURED_ROW_KEYS.contains(&"discovery_error"), + "the failed-discovery shape is identified by a key the measured one \ + does not have, rather than by absence" + ); + assert!( + !crate::topology_report::MEASURED_ROW_KEYS.contains(&"discovery_error"), + "and the measured shape must not carry it" ); } diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index e1a70af5e..542485f35 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -55,13 +55,91 @@ use super::{Coherence, CrossCheck, Observation, PartitioningCache, Verdict}; #[cfg(test)] mod tests; +/// A state an observation can be in that forbids an agreeing verdict. +/// +/// **A type rather than a `&'static str`, so completeness is checkable.** These +/// were strings, and the test that claimed to check every state had a +/// perturbation derived BOTH of its sets from the perturbation table -- so a new +/// branch in [`blocking_states`] that no mutation reached appeared in neither +/// set and both loops stayed green. The guard could only confirm that existing +/// labels described existing mutations. +/// +/// With a type, [`BlockingState::ALL`] is an exhaustive list the compiler +/// checks: adding a variant without adding it there fails to build, and the +/// test compares the table against `ALL` rather than against itself. Found by a +/// review, one round after the guard was added in response to an earlier one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BlockingState { + /// A level was named as the outermost partitioning cache with no summary. + /// + /// The state the renderer prints as `BUG IN THIS PROBE`, and the defect this + /// component exists because of. + PartitioningSummaryMissing, + /// The enumeration recorded anomalies. + EnumerationAnomalies, + /// The topology was not measured from a running machine. + NotMeasured, + /// No cache levels were reported. + NoCacheLevels, + /// No packages were reported, though the machine has one. + NoPackages, + /// No cores were reported, though the machine has one. + NoCores, + /// A core record contradicts itself. + ContradictoryCore, + /// A cache level is numbered 0, which Windows does not report. + UnnumberedCacheLevel, + /// The crate's two enumerations did not agree. + EnumerationsDisagreed, + /// The bracket did not establish that the machine held still. + BracketNotHeld, +} + +impl BlockingState { + /// Every state, so a test can check the perturbation table covers them all. + /// + /// The `match` below is what makes this exhaustive: adding a variant without + /// listing it here is a compile error, not a silently untested state. + pub const ALL: &'static [Self] = &[ + Self::PartitioningSummaryMissing, + Self::EnumerationAnomalies, + Self::NotMeasured, + Self::NoCacheLevels, + Self::NoPackages, + Self::NoCores, + Self::ContradictoryCore, + Self::UnnumberedCacheLevel, + Self::EnumerationsDisagreed, + Self::BracketNotHeld, + ]; + + /// How the report names this state, for a violation a reader has to act on. + #[must_use] + pub const fn described(self) -> &'static str { + match self { + Self::PartitioningSummaryMissing => { + "summary missing for the outermost partitioning cache" + } + Self::EnumerationAnomalies => "the enumeration recorded anomalies", + Self::NotMeasured => "the topology was not measured from a running machine", + Self::NoCacheLevels => "no cache levels were reported", + Self::NoPackages => "no packages were reported", + Self::NoCores => "no cores were reported", + Self::ContradictoryCore => "a core record contradicts itself", + Self::UnnumberedCacheLevel => "a cache level is numbered 0", + Self::EnumerationsDisagreed => "the crate's two enumerations did not agree", + Self::BracketNotHeld => "the bracket did not establish that the machine held still", + } + } +} + /// A state that forbids an agreeing verdict, found beside one. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Violation { /// The observation is in a state that must prevent `agree`, and did not. StateWithAgreeingVerdict { - /// The state, as [`blocking_states`] names it. - state: &'static str, + /// The state. + state: BlockingState, }, /// `agree` without the comparison it asserts having been made. /// @@ -87,9 +165,10 @@ impl fmt::Display for Violation { match self { Self::StateWithAgreeingVerdict { state } => write!( f, - "the observation is in the `{state}` state, which forbids an \ + "the observation is in the `{}` state, which forbids an \ agreeing verdict, but the verdict is `agree` -- so whatever in \ - `cross_check` should have reported this state did not" + `cross_check` should have reported this state did not", + state.described(), ), Self::AgreedWithoutComparingCounter { counter } => write!( f, @@ -122,7 +201,7 @@ impl fmt::Display for Violation { /// module exists to avoid. What belongs here is a state readable from the /// observation on its own terms. #[must_use] -pub fn blocking_states(observation: &Observation) -> Vec<&'static str> { +pub fn blocking_states(observation: &Observation) -> Vec { let mut states = Vec::new(); // The defect this component exists because of: the renderer prints this @@ -132,27 +211,27 @@ pub fn blocking_states(observation: &Observation) -> Vec<&'static str> { observation.partitioning_cache(), PartitioningCache::SummaryMissing(_) ) { - states.push("summary missing for the outermost partitioning cache"); + states.push(BlockingState::PartitioningSummaryMissing); } if !observation.enumeration_anomalies.is_empty() { - states.push("the enumeration recorded anomalies"); + states.push(BlockingState::EnumerationAnomalies); } if !observation.topology_was_measured { - states.push("the topology was not measured from a running machine"); + states.push(BlockingState::NotMeasured); } if observation.caches.is_empty() { - states.push("no cache levels were reported"); + states.push(BlockingState::NoCacheLevels); } if observation.online_processors > 0 && observation.packages == 0 { - states.push("no packages were reported"); + states.push(BlockingState::NoPackages); } if observation.online_processors > 0 && observation.cores.is_empty() { - states.push("no cores were reported"); + states.push(BlockingState::NoCores); } if observation @@ -160,19 +239,19 @@ pub fn blocking_states(observation: &Observation) -> Vec<&'static str> { .iter() .any(super::CoreShape::contradicts_itself) { - states.push("a core record contradicts itself"); + states.push(BlockingState::ContradictoryCore); } if observation.caches.iter().any(|cache| cache.level == 0) { - states.push("a cache level is numbered 0"); + states.push(BlockingState::UnnumberedCacheLevel); } if !matches!(observation.coherence, Coherence::Agreed) { - states.push("the crate's two enumerations did not agree"); + states.push(BlockingState::EnumerationsDisagreed); } if observation.bracket != super::BracketOutcome::HeldStill { - states.push("the bracket did not establish that the machine held still"); + states.push(BlockingState::BracketNotHeld); } states diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index b3755e53b..83379df8b 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -13,7 +13,7 @@ //! costs a reader more than one that misses an illegal one, because noise trains //! them to ignore the instrument -- the same rule the report oracle is built on. -use super::{Violation, blocking_states, check}; +use super::{BlockingState, Violation, blocking_states, check}; use crate::topology::{ BracketOutcome, CacheLevel, CoreShape, Observation, PartitioningCache, Verdict, }; @@ -67,7 +67,7 @@ fn the_observation_this_crate_produces_is_in_no_blocking_state() { // The acceptance half, and the premise every perturbation below rests on: // if the fixture already blocked agreement, each test would be asserting // against two states instead of the one it introduced. - assert_eq!(blocking_states(&agreeing()), Vec::<&str>::new()); + assert_eq!(blocking_states(&agreeing()), Vec::::new()); assert_eq!(check(&agreeing(), Verdict::Agree), Vec::new()); } @@ -93,16 +93,16 @@ fn the_verdict_the_crate_actually_draws_holds_every_invariant() { /// /// This doc claimed such a relation before one existed. Found by a review: the /// table happened to match, which is the condition under which nobody notices. -type Perturbation = (&'static str, Box); +type Perturbation = (BlockingState, Box); fn perturbations() -> Vec { vec![ ( - "summary missing for the outermost partitioning cache", + BlockingState::PartitioningSummaryMissing, Box::new(|o: &mut Observation| o.partitioning_cache_level = Some(9)), ), ( - "the enumeration recorded anomalies", + BlockingState::EnumerationAnomalies, Box::new(|o: &mut Observation| { o.enumeration_anomalies = vec![windows_topology_sys::EnumerationAnomaly { source: windows_topology_sys::Source::CpuSets, @@ -112,23 +112,23 @@ fn perturbations() -> Vec { }), ), ( - "the topology was not measured from a running machine", + BlockingState::NotMeasured, Box::new(|o: &mut Observation| o.topology_was_measured = false), ), ( - "no cache levels were reported", + BlockingState::NoCacheLevels, Box::new(|o: &mut Observation| o.caches = Vec::new()), ), ( - "no packages were reported", + BlockingState::NoPackages, Box::new(|o: &mut Observation| o.packages = 0), ), ( - "no cores were reported", + BlockingState::NoCores, Box::new(|o: &mut Observation| o.cores = Vec::new()), ), ( - "a core record contradicts itself", + BlockingState::ContradictoryCore, Box::new(|o: &mut Observation| { o.cores = vec![CoreShape { simultaneous_multithreading: false, @@ -138,7 +138,7 @@ fn perturbations() -> Vec { }), ), ( - "a cache level is numbered 0", + BlockingState::UnnumberedCacheLevel, Box::new(|o: &mut Observation| { o.caches = vec![CacheLevel { level: 0, @@ -147,13 +147,13 @@ fn perturbations() -> Vec { }), ), ( - "the crate's two enumerations did not agree", + BlockingState::EnumerationsDisagreed, Box::new(|o: &mut Observation| { o.coherence = windows_topology_sys::Coherence::NotCollected; }), ), ( - "the bracket did not establish that the machine held still", + BlockingState::BracketNotHeld, Box::new(|o: &mut Observation| o.bracket = BracketOutcome::Changed), ), ] @@ -161,16 +161,18 @@ fn perturbations() -> Vec { #[test] fn every_blocking_state_has_a_perturbation() { - // **The completeness guard the table's doc claimed and did not have.** - // Without it, a state added to `blocking_states` with no entry in the table - // is never shown to fire, and never shown to be one `cross_check` already - // forbids -- which is the property the whole module rests on. + // **Compared against `BlockingState::ALL`, not against the table itself.** + // The first version of this guard derived BOTH of its sets from + // `perturbations()` -- the reached set by applying them, the labelled set by + // reading them -- so a new branch in `blocking_states` that no mutation + // activated appeared in neither, and both loops stayed green. It could only + // confirm that existing labels described existing mutations, which is not + // what its name claims. Found by a review, one round after the guard was + // added in response to an earlier one. // - // Derived by APPLYING every perturbation and collecting what - // `blocking_states` then reports, rather than by counting the table against - // a number written here. A census would need correcting every time a state - // is added, which is the rot this crate keeps paying for. - let mut reached: Vec<&'static str> = Vec::new(); + // `ALL` is exhaustive by compiler: `described()` matches on every variant, + // so adding one without listing it there fails to build. + let mut reached: Vec = Vec::new(); for (_, mutate) in perturbations() { let mut observation = agreeing(); mutate(&mut observation); @@ -181,26 +183,30 @@ fn every_blocking_state_has_a_perturbation() { } } - // Every state the table's own labels name must be among them, and nothing - // the perturbations reach may be unnamed. - let labelled: Vec<&'static str> = perturbations() + let labelled: Vec = perturbations() .into_iter() .map(|(state, _)| state) .collect(); - for state in &labelled { + for state in BlockingState::ALL { + assert!( + labelled.contains(state), + "`{state:?}` is a blocking state with no entry in the perturbation \ + table, so nothing shows that it fires or that `cross_check` \ + already forbids it" + ); assert!( reached.contains(state), - "`{state}` is a label in the table that no perturbation actually \ - produces, so the row for it tests nothing" + "`{state:?}` has a table entry whose mutation does not actually \ + produce it, so the row for it tests nothing" ); } + for state in &reached { assert!( - labelled.contains(state), - "`{state}` is reported by `blocking_states` and has no entry in the \ - perturbation table, so nothing shows it fires or that \ - `cross_check` already forbids it" + BlockingState::ALL.contains(state), + "`{state:?}` is reported by `blocking_states` and missing from \ + `BlockingState::ALL`" ); } } @@ -213,13 +219,13 @@ fn every_blocking_state_forbids_an_agreeing_verdict() { assert!( blocking_states(&observation).contains(&state), - "{state}: the observation is in this state and `blocking_states` \ + "{state:?}: the observation is in this state and `blocking_states` \ did not say so" ); assert!( check(&observation, Verdict::Agree) .contains(&Violation::StateWithAgreeingVerdict { state }), - "{state}: the state is present beside an agreeing verdict and the \ + "{state:?}: the state is present beside an agreeing verdict and the \ invariant did not fire" ); } @@ -244,13 +250,13 @@ fn every_blocking_state_is_one_the_real_cross_check_already_reports() { assert_ne!( cross_check.verdict(), Verdict::Agree, - "{state}: the invariant forbids `agree` here, so `cross_check` must \ + "{state:?}: the invariant forbids `agree` here, so `cross_check` must \ already forbid it: {cross_check:?}" ); assert_eq!( check(&observation, cross_check.verdict()), Vec::new(), - "{state}: and against the real verdict there is nothing to report" + "{state:?}: and against the real verdict there is nothing to report" ); } } @@ -268,7 +274,7 @@ fn a_blocking_state_is_silent_when_the_verdict_already_admits_it() { assert_eq!( check(&observation, verdict), Vec::new(), - "{state}: {verdict:?} admits the doubt, so there is nothing to \ + "{state:?}: {verdict:?} admits the doubt, so there is nothing to \ contradict" ); } @@ -292,8 +298,7 @@ fn the_partitioning_state_is_read_from_the_observation_not_the_list() { PartitioningCache::SummaryMissing(9) )); assert!( - blocking_states(&observation) - .contains(&"summary missing for the outermost partitioning cache"), + blocking_states(&observation).contains(&BlockingState::PartitioningSummaryMissing), "read from the observation, with the cross-check never consulted" ); } diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index 921446c77..10aa9af5f 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -20,6 +20,53 @@ use std::io; use windows_placement_probe::fingerprint::{Fingerprint, banner_line_for}; use crate::row::{Row, Value}; + +/// Every key a MEASURED topology row carries, in order. +/// +/// **This is the contract, not a census of the code.** The anti-census rule this +/// crate keeps relearning is about restating facts that can be DERIVED -- a +/// count of placeholders, a tally of variants. A schema is not derivable from +/// anything: it IS the agreement with the survey that reads these rows, so +/// writing it down is what makes it checkable at all. +/// +/// It was missing, and the gap was measured: with `packages` deleted from the +/// builder entirely, the whole suite stayed green. `Row::keys` reports what the +/// builder happened to supply, so a test comparing the two only ever showed the +/// reader and the writer agreeing with each other -- never that a field the +/// survey depends on is still there. Found by a review. +/// +/// Changing this list is a breaking change to the row, and +/// `the_measured_row_carries_exactly_the_contracts_keys` is what makes that +/// visible in a diff rather than in a mining pass six months later. +pub const MEASURED_ROW_KEYS: &[&str] = &[ + "reason", + "arch", + "processors", + "groups", + "packages", + "numa_domains", + "numa_domains_without_processors", + "cores", + "efficiency_classes", + "caches", + "outermost_partitioning_cache_level", + "outermost_partitioning_cache", + "policies", + "cross_check", + "disagreements", + "not_compared", + "parse_incomplete", + "enumeration_anomalies", + "numa_domains_only_in_cpu_sets", +]; + +/// Every key an UNMEASURED topology row carries, in order. +/// +/// Deliberately short, and deliberately its own schema rather than a subset of +/// the one above: a row from a host whose discovery FAILED is a different shape, +/// and a survey must be able to tell it from a measured row that happens to be +/// missing fields. +pub const UNMEASURED_ROW_KEYS: &[&str] = &["reason", "arch", "cross_check", "discovery_error"]; use crate::topology::diagnostic::published_anomaly; use crate::topology::{ Disagreement, NotCompared, Observation, ParseIncomplete, PartitioningCache, Verdict, diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 95ae849b6..b999dd31c 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -423,6 +423,20 @@ fn published_codes(text: &str, keys: &[&str]) -> Vec { .flat_map(|key| report_oracle::list_codes(row, key)) .collect() } +/// Whether `text`'s row publishes a condition for an observation in a blocking +/// state. +/// +/// **Extracted so the sabotage can invoke the rule rather than restate it.** +/// `a_state_the_row_does_not_publish_fails_the_accounting` used to strip the +/// row's conditions and then assert only that the stripping had worked -- so it +/// demonstrated the sabotage, never that the accounting REJECTS it. The +/// accounting could have been deleted and that test would have stayed green. +/// Found by a review. +fn publication_holds(observation: &Observation, text: &str) -> bool { + invariant::blocking_states(observation).is_empty() + || !published_codes(text, DIAGNOSTIC_LISTS).is_empty() +} + #[test] fn every_state_that_blocks_agreement_reaches_the_row() { // **This is the rule M3.1 established, given an instrument at last.** A @@ -452,9 +466,8 @@ fn every_state_that_blocks_agreement_reaches_the_row() { continue; } - let published = published_codes(&text, DIAGNOSTIC_LISTS); assert!( - !published.is_empty(), + publication_holds(&shape.observation, &text), "{}: the observation is in {} state(s) that forbid agreement -- \ {blocking:?} -- and the row publishes no condition at all. A \ survey reading it would see a verdict it cannot account \ @@ -631,6 +644,22 @@ fn a_state_the_row_does_not_publish_fails_the_accounting() { published_codes(&stripped, DIAGNOSTIC_LISTS).is_empty(), "the sabotage must actually remove the conditions: {stripped}" ); + + // **And the accounting must REJECT it.** Asserting only that the stripping + // worked demonstrated the sabotage and nothing else -- the rule could have + // been deleted and this stayed green, which is the shape of vacuity this + // whole suite exists to avoid. Calling the same predicate the corpus rule + // calls is what makes this a test of the rule. + assert!( + !publication_holds(&observation, &stripped), + "a report in a blocking state whose row publishes nothing must fail the \ + accounting: {stripped}" + ); + assert!( + publication_holds(&observation, &text), + "and the unsabotaged report must pass it, or the rule rejects \ + everything: {text}" + ); } #[test] From d6d3b41dd990c18aa89c7d0d033dde733df765a9 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 01:04:14 -0400 Subject: [PATCH 16/46] fix(probes): make BlockingState::ALL exhaustive by construction The same defect as the previous commit's fourth finding, one level up, and introduced by the fix for it. The doc on ALL claimed "an exhaustive list the compiler checks: adding a variant without adding it there fails to build". That is not what the compiler checks. The match in described() IS exhaustive-checked, which is what made the claim look right, but it forces a new variant to acquire an ARM, never an ENTRY in a separate array. Measured rather than read: a new variant plus the described() arm the match demands compiled cleanly and left all ten invariant tests green, reached by none of them. ALL is the list the completeness guard iterates, so a variant missing from it is a blocking state nothing tests -- the exact failure the guard was added to prevent, reintroduced by the shape of its own fix. The guard's reverse loop is not a substitute. It catches a state blocking_states produces and ALL omits, but only once some perturbation reaches it -- and a state with no perturbation entry is precisely what the test exists to catch, so it is circular in the case that matters. Fixed by declaring the enum, ALL and described() from one list through a macro, so a variant that is not in the list does not exist. The claim is now true rather than deleted. Corrected the same false claim where it was restated in the guard's own comment. Sabotage-verified in both directions: the original sabotage is now inexpressible, there being no second place to omit the variant from; and its reachable equivalent, a new state in the list with no perturbation entry, reddens every_blocking_state_has_a_perturbation where before the whole suite stayed green. Three rounds on one guard -- strings, a hand-written ALL, generation. Each earlier fix moved the census somewhere harder to see rather than removing it. Completed item: M3.8: Make `BlockingState::ALL` exhaustive by construction rather than by assertion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 32 +++++ .../src/topology/invariant.rs | 128 +++++++++--------- .../src/topology/invariant/tests.rs | 14 +- 3 files changed, 111 insertions(+), 63 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 25c069455..ff81696dd 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -410,6 +410,38 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. table. Introduced a `BlockingState` enum with `ALL`, so the guard holds the table against the type's variants and a new state that nobody describes fails to build past it. + (**The last clause was false, and M3.8 corrects it.** `ALL` was a hand-written array; nothing + tied it to the enum.) + +- [x] **M3.8** -- Make `BlockingState::ALL` exhaustive by construction rather than by assertion. + + The same defect as M3.7's fourth finding, one level up, and introduced by the fix for it. The + doc on `ALL` claimed "an exhaustive list the compiler checks: adding a variant without adding it + there fails to build". That is not what the compiler checks. The `match` in `described()` is + exhaustive-checked, which is what made the claim look right -- but it forces a new variant to + acquire an ARM, never an ENTRY in a separate array. + + Measured, not read: a new variant plus the `described()` arm the match demands compiled cleanly + and left all ten invariant tests green, reached by none of them. `ALL` is the list the + completeness guard iterates, so a variant missing from it is a blocking state nothing tests -- + which is the exact failure the guard was added to prevent, reintroduced by the shape of its fix. + + The reverse loop in the guard is not a substitute. It catches a state `blocking_states` produces + and `ALL` omits, but only once some perturbation reaches it -- and a state with no perturbation + entry is precisely what the test exists to catch, so it is circular in the case that matters. + + Fixed by declaring the enum, `ALL` and `described()` from one list through a macro, so a variant + that is not in the list does not exist. The claim is now true rather than deleted. + + Sabotage-verified in both directions: the original sabotage is now inexpressible (there is no + second place to omit the variant from), and its reachable equivalent -- a new state in the list + with no perturbation entry -- reddens `every_blocking_state_has_a_perturbation`, where before + the whole suite stayed green. + + **Three rounds on one guard: strings, then a hand-written `ALL`, then generation.** Each fix + moved the census somewhere harder to see rather than removing it. Worth stating because the + reviewer's finding was not a new defect -- it was the same defect wearing the previous fix. + ## M4 -- Carried over from M2: the items M3 gates These were written under M2 and are blocked on M3 above: each one targets the prose-against-row diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 542485f35..12cfb3555 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -55,82 +55,88 @@ use super::{Coherence, CrossCheck, Observation, PartitioningCache, Verdict}; #[cfg(test)] mod tests; -/// A state an observation can be in that forbids an agreeing verdict. +/// Declares [`BlockingState`]: the variants, [`BlockingState::ALL`] and +/// [`BlockingState::described`] all from ONE list. /// -/// **A type rather than a `&'static str`, so completeness is checkable.** These -/// were strings, and the test that claimed to check every state had a -/// perturbation derived BOTH of its sets from the perturbation table -- so a new -/// branch in [`blocking_states`] that no mutation reached appeared in neither -/// set and both loops stayed green. The guard could only confirm that existing -/// labels described existing mutations. +/// **This exists so that `ALL` cannot drift from the enum.** Writing the two by +/// hand does not prevent it, and the difference is not cosmetic: `ALL` is what +/// the completeness guard iterates, so a variant missing from it is a blocking +/// state nothing tests. A hand-written `ALL` was measured to allow exactly that +/// -- a new variant, its `described()` arm supplied because the `match` forces +/// one, compiled cleanly and left all ten invariant tests green while being +/// reached by none of them. /// -/// With a type, [`BlockingState::ALL`] is an exhaustive list the compiler -/// checks: adding a variant without adding it there fails to build, and the -/// test compares the table against `ALL` rather than against itself. Found by a -/// review, one round after the guard was added in response to an earlier one. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BlockingState { +/// The `match` in `described()` is genuinely exhaustive-checked, which is what +/// made the hand-written version look safe. It is not enough: it forces a new +/// variant to acquire an ARM, never an ENTRY in a separate array. Generating +/// both from one list is what ties them together, because the enum itself comes +/// from that list -- a variant that is not in it does not exist. +macro_rules! blocking_states { + ($( $(#[$doc:meta])* $variant:ident => $described:literal ),+ $(,)?) => { + /// A state an observation can be in that forbids an agreeing verdict. + /// + /// **A type rather than a `&'static str`, so completeness is + /// checkable.** These were strings, and the test that claimed to check + /// every state had a perturbation derived BOTH of its sets from the + /// perturbation table -- so a new branch in [`blocking_states`] that no + /// mutation reached appeared in neither set and both loops stayed green. + /// The guard could only confirm that existing labels described existing + /// mutations. + /// + /// The type is declared by a macro from a single list, so + /// [`BlockingState::ALL`] cannot omit a variant: the variants and `ALL` + /// are the same list. An earlier version wrote them separately and + /// claimed the compiler checked the correspondence, which it did not -- + /// found by a review, two rounds after the strings. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum BlockingState { + $( $(#[$doc])* $variant, )+ + } + + impl BlockingState { + /// Every state, so a test can check the perturbation table covers + /// them all. + /// + /// Generated from the same list as the variants, so it is exhaustive + /// by construction rather than by anyone remembering. + pub const ALL: &'static [Self] = &[ $( Self::$variant, )+ ]; + + /// How the report names this state, for a violation a reader has to + /// act on. + #[must_use] + pub const fn described(self) -> &'static str { + match self { + $( Self::$variant => $described, )+ + } + } + } + }; +} + +blocking_states! { /// A level was named as the outermost partitioning cache with no summary. /// /// The state the renderer prints as `BUG IN THIS PROBE`, and the defect this /// component exists because of. - PartitioningSummaryMissing, + PartitioningSummaryMissing => "summary missing for the outermost partitioning cache", /// The enumeration recorded anomalies. - EnumerationAnomalies, + EnumerationAnomalies => "the enumeration recorded anomalies", /// The topology was not measured from a running machine. - NotMeasured, + NotMeasured => "the topology was not measured from a running machine", /// No cache levels were reported. - NoCacheLevels, + NoCacheLevels => "no cache levels were reported", /// No packages were reported, though the machine has one. - NoPackages, + NoPackages => "no packages were reported", /// No cores were reported, though the machine has one. - NoCores, + NoCores => "no cores were reported", /// A core record contradicts itself. - ContradictoryCore, + ContradictoryCore => "a core record contradicts itself", /// A cache level is numbered 0, which Windows does not report. - UnnumberedCacheLevel, + UnnumberedCacheLevel => "a cache level is numbered 0", /// The crate's two enumerations did not agree. - EnumerationsDisagreed, + EnumerationsDisagreed => "the crate's two enumerations did not agree", /// The bracket did not establish that the machine held still. - BracketNotHeld, -} - -impl BlockingState { - /// Every state, so a test can check the perturbation table covers them all. - /// - /// The `match` below is what makes this exhaustive: adding a variant without - /// listing it here is a compile error, not a silently untested state. - pub const ALL: &'static [Self] = &[ - Self::PartitioningSummaryMissing, - Self::EnumerationAnomalies, - Self::NotMeasured, - Self::NoCacheLevels, - Self::NoPackages, - Self::NoCores, - Self::ContradictoryCore, - Self::UnnumberedCacheLevel, - Self::EnumerationsDisagreed, - Self::BracketNotHeld, - ]; - - /// How the report names this state, for a violation a reader has to act on. - #[must_use] - pub const fn described(self) -> &'static str { - match self { - Self::PartitioningSummaryMissing => { - "summary missing for the outermost partitioning cache" - } - Self::EnumerationAnomalies => "the enumeration recorded anomalies", - Self::NotMeasured => "the topology was not measured from a running machine", - Self::NoCacheLevels => "no cache levels were reported", - Self::NoPackages => "no packages were reported", - Self::NoCores => "no cores were reported", - Self::ContradictoryCore => "a core record contradicts itself", - Self::UnnumberedCacheLevel => "a cache level is numbered 0", - Self::EnumerationsDisagreed => "the crate's two enumerations did not agree", - Self::BracketNotHeld => "the bracket did not establish that the machine held still", - } - } + BracketNotHeld => "the bracket did not establish that the machine held still", } /// A state that forbids an agreeing verdict, found beside one. diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index 83379df8b..9a8b7f757 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -170,8 +170,18 @@ fn every_blocking_state_has_a_perturbation() { // what its name claims. Found by a review, one round after the guard was // added in response to an earlier one. // - // `ALL` is exhaustive by compiler: `described()` matches on every variant, - // so adding one without listing it there fails to build. + // **`ALL` is exhaustive by construction, not by the `described()` match.** + // This comment used to claim the latter, and it was false: the match forces + // a new variant to acquire an ARM, never an entry in a separate array. + // Measured -- a variant absent from a hand-written `ALL` compiled and left + // all ten tests here green. `BlockingState` is now declared by a macro from + // one list, so the variants and `ALL` are the same list. + // + // The reverse loop below is NOT a substitute for that. It catches a state + // `blocking_states` produces and `ALL` omits, but only once some + // perturbation reaches it -- and a state with no perturbation entry is + // precisely the case this test exists to catch, so relying on it would be + // circular in exactly the case that matters. let mut reached: Vec = Vec::new(); for (_, mutate) in perturbations() { let mut observation = agreeing(); From a60f99d7cdcddfebfbbd702ea0404c8b73c370a9 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 01:14:28 -0400 Subject: [PATCH 17/46] docs(platform-probes): re-plan M2.14 from writing rules to making them bite M2.14 said "write two authoring rules into the repository instructions". Measurement says that would have been worse than useless, so the item is re-planned into two sub-steps before any of it is implemented. Both proposed rules already exist. CONTRACT INTEGRITY rule 1 in .github/copilot-instructions.md is "Prefer a derived fact to a restated one", and beneath it "verify the binding by sabotage: change the definition and confirm the consumer's BEHAVIOR changes". Those are the two rules M2.14 proposed. Writing them again would add a second copy of a rule, which is the exact defect that section forbids and the exact mechanism, restatement drift, that it exists to prevent. Statement is also demonstrably not the gap. Both rules were in force yesterday and both were violated: 09da7e9 claims "Sabotage-verified, EACH against the instrument it was meant to strengthen" and then names two sabotages for four fixes. The one that got none is the completeness guard, found broken by a review an hour later (M3.8). A rule cited in the commit that breaks it will not be repaired by a third copy of itself. This repository has solved the problem once already, and not with a rule: the sabotage-harness CI job records that the harness "accumulated fixes over eleven review rounds and thirteen of the later defects were introduced by earlier fixes, because every verification was a one-off command that was then discarded and nothing re-checked an earlier guarantee". That is M3.8's story verbatim, and the answer then was a ratchet. So M2.14 becomes M2.14.1, give this crate the sabotage.json it lacks while two sibling crates have one, and M2.14.2, add to rule 1 the single thing this branch learned that it does not already say -- sabotage the CLAIM that a weakness was removed, not the symptom -- plus a pointer to the mechanism. The M2.14 ID is preserved because COMPLETED-CHECKLIST.md is append-only and two archived entries cite it. Completed item: M2.14 (re-plan only): Re-plan M2.14 from "write two authoring rules" into M2.14.1 and M2.14.2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 75 ++++++++++++++++----- 1 file changed, 60 insertions(+), 15 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index ff81696dd..3ceab15af 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -51,7 +51,8 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - **M2.17** is re-scoped by M3.5: the dimensions worth crossing become the row's, and crossing prose shapes that are about to stop being checked would aim at the retiring half. - **M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are gated by nothing** and are in M5. M2.9 (a - cross-host ratio called "the finding") and M2.14 (two authoring rules) are if anything reinforced: + cross-host ratio called "the finding") and M2.14 (making two authoring rules bite) are if anything + reinforced: under this decision prose accuracy is a review obligation rather than a machine-checked one, which puts more weight on both. @@ -613,19 +614,61 @@ IDs keep their M2 numbers, for the reason given under M4. append-only invariant, and it is the one a human reviewer is least likely to notice. -- [ ] **M2.14** -- Write two authoring rules into the repository instructions, both earned on this - branch. - - **State the invariant, not the census.** "14 keys read, 3 unread" added nothing that "every key is - classified" does not, and it was wrong -- written by eyeballing a list rather than counting it, in - the commit documenting a fix for exactly that defect class. Where a number is genuinely load - bearing, it must come from a command run in the same action that writes it. - - **A new test is not done until it has been observed to fail.** Every vacuous test on this branch was - written green and stayed green until a reviewer thought to break something: a guard that matched a - violation's VARIANT where only its FACT established the point, and a fixture whose `.replace()` of - `[1]` matched nothing because the report rendered `[0]`. Sabotage belongs at authoring time, not at - review time. +- [ ] **M2.14** -- Make the two authoring rules this branch earned actually bite. Re-planned + 2026-09-13; see the rationale below before implementing either sub-step. + + **As originally written this item said "write two authoring rules into the repository + instructions". Measurement says that would have been worse than useless.** The two rules it + proposed -- *state the invariant, not the census*, and *a new test is not done until it has been + observed to fail* -- ALREADY EXIST, in + [.github/copilot-instructions.md](../../.github/copilot-instructions.md) under CONTRACT INTEGRITY + rule 1 ("Prefer a derived fact to a restated one", and beneath it "verify the binding by + sabotage: change the definition and confirm the consumer's BEHAVIOR changes"). Writing them again + would add a second copy of a rule, which is the exact defect that section forbids and the exact + mechanism -- restatement drift -- it exists to prevent. + + **And statement is demonstrably not the gap.** Both rules were in force on 2026-09-12 and both + were violated: `09da7e9` claims "Sabotage-verified, EACH against the instrument it was meant to + strengthen" and then names two sabotages for four fixes. The one that got none is the completeness + guard, which a review found broken an hour later (M3.8). A rule cited in the commit that breaks it + will not be repaired by a third copy of itself. + + **This repository has already solved this problem once, and not with a rule.** The + [ci.yml](../../.github/workflows/ci.yml) `sabotage-harness` job records that the harness "accumulated + fixes over eleven review rounds and thirteen of the later defects were introduced by earlier fixes, + because every verification was a one-off command that was then discarded and nothing re-checked an + earlier guarantee." That is M3.8's story verbatim. The answer then was a CI ratchet. + +- [ ] **M2.14.1** -- Give `windows-platform-probes` a sabotage manifest, so "observed to fail" is a + recorded artifact rather than a habit. + + `tools/run-sabotage.ps1` exists, has its own tests, and runs in CI; + [windows-placement-probe](../windows-placement-probe/sabotage.json) (9 entries) and + [windows-waitable-queues](../windows-waitable-queues/sabotage.json) (39 entries) each carry a + `sabotage.json`. **This crate carries none**, so every sabotage run while building M3 was ad-hoc + PowerShell, discarded on the spot -- which is why a `git checkout` destroyed uncommitted work + twice and a `.Replace` pattern silently matched two sites once. The manifest format's `find` must + match EXACTLY ONCE, which is precisely the guard that hand-running lacks. + + Eight commits on this branch record their sabotages in the message, so the first pass is + transcription rather than invention: the defect, the file and the test expected to redden are + already written down. + + Include at least one `expect: "survives"` control. A manifest of nothing but `caught` cannot + distinguish a suite that is watching from a suite that fails on any edit. + +- [ ] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does + NOT already say, and a pointer to the mechanism. A pointer, not a restatement. + + The genuinely new fact: **when a fix claims to have removed a weakness, sabotage the claim rather + than the symptom.** Rule 1 tells an author to prefer a derived fact over a restated one; it does + not warn that an author may believe they derived one when they only MOVED the census. That is + exactly what happened three times on a single guard -- strings, then a hand-written `ALL`, then + generation -- each fix relocating the census somewhere harder to see while its commit message + claimed the class was closed. The wording that would have caught it is about the CLAIM, and rule 1 + currently has no sentence about claims. + + > **-> DEPENDS ON M2.14.1:** the pointer has nothing to point at until the manifest exists. - [ ] **M2.16** -- Repair the garbled `Report` doc comment, and drop the two counts that have already rotted beside it. @@ -642,4 +685,6 @@ IDs keep their M2 numbers, for the reason given under M4. so the two must be fixed together or they drift apart again. Replace them with the invariant the passage is actually arguing -- that `String` already implements `fmt::Write`, so every existing write site stands untouched and only the renderer signatures move -- which is what makes the point - and cannot rot. This is the same defect class as M2.14's first authoring rule. + and cannot rot. This is the same defect class as CONTRACT INTEGRITY rule 1 in + [.github/copilot-instructions.md](../../.github/copilot-instructions.md), which M2.14 exists to + make bite. From b5adbcd788094843a0595ef1b57f1b5fa301f8ab Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 01:23:41 -0400 Subject: [PATCH 18/46] test(platform-probes): add the sabotage manifest this crate lacked M2.14.1. tools/run-sabotage.ps1 exists, has its own tests and runs in CI; windows-placement-probe and windows-waitable-queues each carry a sabotage.json. This crate carried none, so every sabotage run while building M3 was ad-hoc PowerShell discarded on the spot -- which is why a git checkout destroyed uncommitted work twice and a .Replace pattern silently matched two sites once. Seven entries, swept green: six caught, one survives, all behaving as declared. Each is a defect that was actually run by hand during M3, so this is transcription of verifications that were already made and then thrown away. Not wired into CI, matching both sibling manifests and the sabotage-harness job's note that a sweep rebuilds a crate per entry and is deliberately an occasional instrument. The control is the entry that matters most. It rewords a prose line to carry the same fact and must SURVIVE, which turns this component's central decision -- the row is the machine contract, the prose is for a reader -- from a sentence into a measurement. If it is ever reported as caught, a test has started reading the prose again and that test is the defect, not the entry. The harness found a defect in the manifest that the authoring script missed, which is the lesson worth keeping. The escape-aware key reader entry anchored on a line that the script judged unique by whole-line equality; the harness matches by substring and found it twice, because the same match arm appears in malformation at a deeper indent and the shallower line is a substring of the deeper one. The script's check was a second, weaker implementation of the harness's rule -- the exact defect class this manifest exists to catch. The anchor was widened to the function signature and the harness left as the only authority. Completed item: M2.14.1: Give `windows-platform-probes` a sabotage manifest, so "observed to fail" is a recorded artifact rather than a habit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 21 +++- crates/windows-platform-probes/sabotage.json | 112 +++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 crates/windows-platform-probes/sabotage.json diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 3ceab15af..1a039e61a 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -639,9 +639,28 @@ IDs keep their M2 numbers, for the reason given under M4. because every verification was a one-off command that was then discarded and nothing re-checked an earlier guarantee." That is M3.8's story verbatim. The answer then was a CI ratchet. -- [ ] **M2.14.1** -- Give `windows-platform-probes` a sabotage manifest, so "observed to fail" is a +- [x] **M2.14.1** -- Give `windows-platform-probes` a sabotage manifest, so "observed to fail" is a recorded artifact rather than a habit. + **Done.** [sabotage.json](sabotage.json), 7 entries, swept green: six `caught`, one `survives`, + all behaving as declared. Not wired into CI, matching the two sibling manifests and the + `sabotage-harness` job's own note that a sweep "rebuilds a crate per entry and is deliberately an + occasional instrument". + + **The control is the entry that matters most here.** It rewords a prose line to carry the same + fact and must SURVIVE, which turns this component's central decision -- the row is the machine + contract, the prose is for a reader -- from a sentence into a measurement. If it is ever reported + as caught, a test has started reading the prose again and that test is the defect. + + **The harness found a defect in the manifest that the authoring script missed, which is the + lesson.** The entry for the escape-aware key reader anchored on `'\\' => escaped = true,`; the + script checked uniqueness by whole-LINE equality and found one match, while the harness matches by + SUBSTRING and found two -- the same arm appears in `malformation` at a deeper indent, and the + shallower line is a substring of the deeper one. The script's check was a second, weaker + implementation of the harness's rule, which is precisely the defect class this manifest exists to + catch. The anchor was widened to the function signature; the harness remains the only authority on + uniqueness. + `tools/run-sabotage.ps1` exists, has its own tests, and runs in CI; [windows-placement-probe](../windows-placement-probe/sabotage.json) (9 entries) and [windows-waitable-queues](../windows-waitable-queues/sabotage.json) (39 entries) each carry a diff --git a/crates/windows-platform-probes/sabotage.json b/crates/windows-platform-probes/sabotage.json new file mode 100644 index 000000000..912606ff0 --- /dev/null +++ b/crates/windows-platform-probes/sabotage.json @@ -0,0 +1,112 @@ +{ + "description": "Sabotage manifest for windows-platform-probes. Each entry is a defect that was actually run by hand while building M3; the manifest exists so those one-off verifications stop being discarded. See CHECKLIST.md M2.14.1.", + "package": "windows-platform-probes", + "sabotages": [ + { + "name": "row drops the packages field", + "file": "src/topology_report.rs", + "expect": "caught", + "why": "A field silently vanishes from every mined row. This is the defect that was MEASURED to leave the whole suite green before MEASURED_ROW_KEYS existed, so this entry is the regression test for the key-set contract itself.", + "find": [ + " .with(\"packages\", observation.packages)" + ], + "replace": [] + }, + { + "name": "row writer emits a leading separator", + "file": "src/row.rs", + "expect": "caught", + "why": "Produces a row that is bracket-balanced but not valid JSON, so a consumer's parser rejects it. The oracle's old `balanced` check counted depth and passed exactly this; `malformation` must not.", + "find": [ + " Self::Object(members) => {", + " out.push('{');", + " for (at, (name, value)) in members.iter().enumerate() {", + " if at > 0 {", + " out.push(',');", + " }" + ], + "replace": [ + " Self::Object(members) => {", + " out.push('{');", + " for (_at, (name, value)) in members.iter().enumerate() {", + " out.push(',');" + ] + }, + { + "name": "row writer does not escape quotes", + "file": "src/row.rs", + "expect": "caught", + "why": "A quote in a caller's `io::Error` ends the JSON string early and lets the rest of the message forge keys in the mined row. `write_escaped` is the whole of the injection fix.", + "find": [ + " '\"' => out.push_str(\"\\\\\\\"\")," + ], + "replace": [ + " '\"' => out.push('\"')," + ] + }, + { + "name": "the row's key reader is not escape-aware", + "file": "src/report_oracle.rs", + "expect": "caught", + "why": "`string_end` stops at the first quote regardless of a preceding backslash, so the reader disagrees with the writer and `assert_corresponds` panics from inside the renderer on any report carrying a quoted discovery error.", + "find": [ + "fn string_end(row: &str, from: usize) -> Option {", + " let mut escaped = false;", + " for (at, character) in row[from..].char_indices() {", + " if escaped {", + " escaped = false;", + " continue;", + " }", + " match character {", + " '\\\\' => escaped = true," + ], + "replace": [ + "fn string_end(row: &str, from: usize) -> Option {", + " let mut escaped = false;", + " for (at, character) in row[from..].char_indices() {", + " if escaped {", + " escaped = false;", + " continue;", + " }", + " match character {", + " '\\\\' => {}" + ] + }, + { + "name": "cross_check forgets the changed bracket", + "file": "src/topology.rs", + "expect": "caught", + "why": "THE entry this crate's invariant module exists for. Deleting the push empties the list, so `verdict()` -- a pure function of the lists -- legitimately returns `agree` beside an observation whose bracket did not hold. Any rule reading the LISTS is blind to this by construction; only a rule reading the OBSERVATION catches it.", + "find": [ + " check.not_compared.push(NotCompared::MachineChanged);" + ], + "replace": [] + }, + { + "name": "a blocking state with no perturbation", + "file": "src/topology/invariant.rs", + "expect": "caught", + "why": "An untested blocking state. Adds rather than deletes, deliberately: the completeness guard exists to catch an ADDITION, and a hand-written `ALL` was measured to let exactly this through while all ten invariant tests stayed green.", + "find": [ + " BracketNotHeld => \"the bracket did not establish that the machine held still\"," + ], + "replace": [ + " BracketNotHeld => \"the bracket did not establish that the machine held still\",", + " /// A state added by a sabotage, with no perturbation behind it.", + " SabotageSentinel => \"a state no perturbation produces\"," + ] + }, + { + "name": "prose reworded to carry the same fact", + "file": "src/topology_report.rs", + "expect": "survives", + "why": "THE CONTROL, and it encodes this component's central decision as a measurement rather than as prose: the row is the machine contract and the prose is for a reader, so no test may read the prose. If this is ever reported as caught, a test has started asserting the wording again and THAT test is the defect -- not this entry.", + "find": [ + " let _ = writeln!(out, \"cache boundary divides this machine.\");" + ], + "replace": [ + " let _ = writeln!(out, \"cache boundary splits this machine.\");" + ] + } + ] +} From 89ff49b892cf326e747f74214a9f8fd74e1eac02 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 01:24:56 -0400 Subject: [PATCH 19/46] docs: sabotage the claim, not the symptom, and record the sabotage M2.14.2. Two paragraphs added to CONTRACT INTEGRITY rule 1. Neither restates the rule, which is what the original M2.14 wording would have done and why that item was re-planned first. The new fact is about CLAIMS. Rule 1 already says to prefer a derived fact over a restated one, and to verify the binding by sabotage. It does not warn that an author may believe they derived a fact when they only MOVED the restatement -- and such an author will sabotage the old site, watch it go red, and record the class as closed. Measured on one guard in windows-platform-probes three times in succession (strings, a hand-written ALL, generation), each fix relocating the census somewhere harder to see while its commit message claimed the class was closed, and each caught by a review rather than by its author. The second paragraph points at the mechanism rather than describing it: the manifest, run-sabotage.ps1, README-sabotage.md for the format and for why a manifest with no control can only tell you the tests are sensitive. It also states that the harness is the authority on patch-site uniqueness, because re-implementing that check beside it is the same defect -- as the previous commit measured. Completed item: M2.14.2: Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does NOT already say, and a pointer to the mechanism Completed item: M2.14: Make the two authoring rules this branch earned actually bite Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 22 +++++++++++++++++++++ crates/windows-platform-probes/CHECKLIST.md | 12 ++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7fe6b98c9..5be0df5a3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1195,6 +1195,28 @@ When you add such a predicate, **verify the binding by sabotage**: change the de confirm the consumer's *behavior* changes. If only a test's expectation moves, or nothing moves, the binding is cosmetic and the copy is still there. +**Sabotage the CLAIM, not the symptom.** When a change asserts that it *removed* a weakness — a +census eliminated, a check made exhaustive, a duplicate collapsed — the sabotage that matters is +the one that would falsify *that* assertion, which is rarely the same as the one that reproduces +the original symptom. This is the failure mode the rule above does not cover on its own: an author +who believes they derived a fact when they only **moved** the restatement will happily sabotage the +old site, watch it go red, and record the class as closed. Measured on one guard in +`windows-platform-probes`, three times in succession — strings, then a hand-written `ALL`, then +generation — each fix relocating the census somewhere harder to see while its commit message +claimed the class was closed, and each caught by a review rather than by its author. Ask what the +commit message asserts, then break *that*. + +**A sabotage is worth nothing once it is discarded**, so record it where something re-runs it +rather than in a terminal you will close. Put it in the component's `sabotage.json` and sweep it +with [tools/run-sabotage.ps1](../tools/run-sabotage.ps1) — see +[tools/README-sabotage.md](../tools/README-sabotage.md) for the manifest format and for why a +manifest with no `expect: "survives"` control can only tell you the tests are sensitive, never that +they are sensitive to the right things. The harness is the authority on whether a patch site is +unique; do not re-implement that check beside it. This is not a preference: the `sabotage-harness` +CI job exists because eleven review rounds on that harness produced thirteen later defects +*introduced by earlier fixes*, "because every verification was a one-off command that was then +discarded and nothing re-checked an earlier guarantee." + Know the limit — but know that it is narrower than it first appears. **Sequencing rules (ordering, bracket entry states, what may follow what) are not value-level, and are still derivable**: define them once as a shared executable oracle — a state machine over the diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 1a039e61a..5c1dec1de 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -614,8 +614,8 @@ IDs keep their M2 numbers, for the reason given under M4. append-only invariant, and it is the one a human reviewer is least likely to notice. -- [ ] **M2.14** -- Make the two authoring rules this branch earned actually bite. Re-planned - 2026-09-13; see the rationale below before implementing either sub-step. +- [x] **M2.14** -- Make the two authoring rules this branch earned actually bite. Re-planned + 2026-09-13; see the rationale below before implementing either sub-step. Both sub-steps done. **As originally written this item said "write two authoring rules into the repository instructions". Measurement says that would have been worse than useless.** The two rules it @@ -676,9 +676,15 @@ IDs keep their M2 numbers, for the reason given under M4. Include at least one `expect: "survives"` control. A manifest of nothing but `caught` cannot distinguish a suite that is watching from a suite that fails on any edit. -- [ ] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does +- [x] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does NOT already say, and a pointer to the mechanism. A pointer, not a restatement. + **Done.** Two paragraphs added to CONTRACT INTEGRITY rule 1 in + [.github/copilot-instructions.md](../../.github/copilot-instructions.md). Neither restates the + existing rule: the first says to sabotage the CLAIM a change makes rather than the symptom it + cites, and the second says a sabotage is worth nothing once discarded and points at the manifest + and the harness. The original M2.14 wording is nowhere in the diff, which was the point. + The genuinely new fact: **when a fix claims to have removed a weakness, sabotage the claim rather than the symptom.** Rule 1 tells an author to prefer a derived fact over a restated one; it does not warn that an author may believe they derived one when they only MOVED the census. That is From 60e66752ef1303f1e88ca99e69867beee0194378 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 01:53:20 -0400 Subject: [PATCH 20/46] fix(platform-probes)!: decide row well-formedness by a real parse M3.9. The oracle's question is "could a consumer read this row", and a consumer uses a JSON parser. Anything hand-written here is a second opinion about what JSON is, and a second opinion is a thing that can disagree -- so malformation now calls serde_json and the scanner is gone. Measured, and the measurement is why this happened at all. The hand-written check had already been through a review, which strengthened it after finding it accepted {"a":1,}. A generated test -- 1807 single-character corruptions of a real row, judged against serde_json -- then found 159 MORE disagreements, every single one a false accept: 129 stray backslashes forming invalid escapes, 10 missing ':', 13 ',' where a ':' belonged, 3 the reverse, 3 missing values, 1 string following a number. The review had found one instance of a class with 160 members. Closing the last ~26 required tracking whether an object expects a name or a value next, which is a JSON parser; so the choice was to write one or to depend on one. The agreement test was deleted in the same commit, deliberately. With the parse delegated it would compare serde_json against serde_json -- green by construction, and exactly the tautology this milestone keeps deleting. What replaced it asks a question that is still open: not whether the verdict is right, but whether it REACHES the caller, which is a property of `check` that no parser guarantees. It found a real boundary while being written: 8 corruptions destroy the leading brace, and those are Missing rather than Malformed -- not a row at all, which for a survey asking "did this host report a row" is the right answer and a different one. Both branches are asserted. Three tests stopped asserting the defect's wording. The message is serde_json's now, so this crate does not own it; pinning it would let a dependency's patch release redden tests about unclosed delimiters, which would be a false finding about this crate. A parse does NOT subsume RepeatedKey, which is why that check stays hand -written: serde_json accepts a duplicated key and silently keeps the last, precisely the malformation that survives a consumer's parse and changes what it reads. BREAKING CHANGE: report_oracle is now gated on cfg(any(test, feature = "oracle-in-renderer")), and RowDefect::Malformed carries a String rather than a &'static str. Every caller was already behind that gate; stating it is what keeps the parser out of a shipping probe. Verified by inspecting the binaries, per the precedent in that feature's own comment: the default probe-topology.exe contains no serde_json, no oracle panic string and no parser message, while the --all-features one contains all three. Swept the class rather than the reported site: fixed the module's stale claim that the key-set check was deferred, and the checklist's claim that the delimiter stack was the answer. Sabotage manifest re-swept, 7 of 7 still as declared. cargo doc verified in BOTH feature shapes -- the no-features build caught an intra-doc link the gate had broken. Completed item: M3.9: Decide the row's well-formedness by a real parse, and delete the hand-written one Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + crates/windows-platform-probes/CHECKLIST.md | 43 ++++++ crates/windows-platform-probes/Cargo.toml | 28 +++- crates/windows-platform-probes/src/lib.rs | 8 + .../src/report_oracle.rs | 130 +++++----------- .../src/report_oracle/tests.rs | 144 +++++++++++++++++- .../src/topology/invariant.rs | 4 +- 7 files changed, 259 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a26461a7f..10fcbd3b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,6 +216,7 @@ dependencies = [ name = "windows-platform-probes" version = "0.0.1" dependencies = [ + "serde_json", "windows-namespace-request-sys", "windows-placement-probe", "windows-platform-probes", diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 5c1dec1de..48c15c4ae 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -400,6 +400,10 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. separator placement. Sabotage-verified: making the writer emit a leading separator produces a balanced but invalid row, which the old check passed and the new one reddens. + (**That replacement was itself replaced, by M3.9.** The typed delimiter stack was a second + hand-written opinion about what JSON is, and a generated test found 159 more rows it accepted + and a real parser rejected.) + **3. A sabotage asserted only that it had sabotaged.** The publication-accounting sabotage stripped a condition from the report and then asserted the condition was absent -- which is a fact about the string edit, not about the rule. It would have passed with the rule deleted. The rule is now @@ -443,6 +447,45 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. moved the census somewhere harder to see rather than removing it. Worth stating because the reviewer's finding was not a new defect -- it was the same defect wearing the previous fix. +- [x] **M3.9** -- Decide the row's well-formedness by a real parse, and delete the hand-written one. + + **The question the oracle asks is "could a consumer read this row", and a consumer uses a JSON + parser.** Anything hand-written here is a second opinion about what JSON is, and a second opinion + is a thing that can disagree -- so `malformation` now calls `serde_json` and the scanner is gone. + + **Measured, and the measurement is why this happened at all.** The hand-written check had already + been through a review, which strengthened it after finding it accepted `{"a":1,}`. A generated + test -- 1807 single-character corruptions of a real row, judged against `serde_json` -- then found + **159 more disagreements, every single one a FALSE ACCEPT**: 129 stray backslashes forming invalid + escapes, 10 missing `:`, 13 `,` where a `:` belonged, 3 the reverse, 3 missing values, 1 string + following a number. The review had found one instance of a class with 160 members. + + Closing the last ~26 required tracking whether an object expects a name or a value next, which is + a JSON parser. So the choice was to write one or to depend on one. + + **The agreement test was deleted in the same commit, deliberately.** With the parse delegated it + would compare `serde_json` against `serde_json` -- green by construction, and exactly the + tautology this milestone keeps deleting. What replaced it asks a question that is still open: not + "is the verdict right" but "does the verdict REACH the caller", which is a property of `check` and + not guaranteed by any parser. It found a real boundary while being written: 8 corruptions destroy + the leading brace, and those are `Missing` rather than `Malformed` -- not a row at all, which for + a survey asking "did this host report a row" is the right answer and a different one. Both + branches are asserted. + + **Three tests stopped asserting the defect's wording.** The message is `serde_json`'s now, so + this crate does not own it; pinning it would let a dependency's patch release redden tests about + unclosed delimiters, a false finding about this crate. They assert rejection and the carried row. + + **A parse does NOT subsume `RepeatedKey`,** which is why that check stays hand-written: + `serde_json` accepts a duplicated key and silently keeps the last, which is precisely the + malformation that survives a consumer's parse and changes what it reads. + + `report_oracle` is now gated `cfg(any(test, feature = "oracle-in-renderer"))` -- every caller + already was -- which is what keeps the parser out of a shipping probe. Verified by inspecting the + binaries, per the precedent in that feature's own comment: the default `probe-topology.exe` + contains no `serde_json`, no oracle panic string, and no parser message; the `--all-features` one + contains all three. + ## M4 -- Carried over from M2: the items M3 gates These were written under M2 and are blocked on M3 above: each one targets the prose-against-row diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index a8c77b8cd..5046459d4 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -34,7 +34,9 @@ path = "src/lib.rs" # workspace does use `--all-features`, for cargo-mutants runs. The boundary is # stated rather than claimed away: the DEFAULT build is the one that carries the # print-rather-than-panic guarantee. -oracle-in-renderer = [] +# Pulls in `serde_json` because the oracle's well-formedness check IS a real +# parse. See the `[dependencies]` entry for why that is not hand-written. +oracle-in-renderer = ["dep:serde_json"] [[bin]] name = "probe-error-mode" @@ -92,6 +94,24 @@ name = "probe-long-path-unaware" path = "src/bin/long_path_unaware.rs" [dependencies] +# **Optional, and reached only through `oracle-in-renderer`.** The report +# oracle's job is to answer "would a consumer be able to parse this row", and +# the honest way to answer that is to PARSE IT with the kind of parser a +# consumer uses. This replaced a hand-written structural check. +# +# That check was not obviously wrong; it had been through a review, which +# strengthened it after finding it accepted `{"a":1,}`. Then a generated test -- +# 1807 single-character corruptions of a real row, judged against this crate -- +# found **159 more disagreements, every one a false accept**: a stray backslash +# forming an invalid escape, a missing `:`, a `,` where a `:` belonged. Closing +# the last of those meant tracking whether an object expects a name or a value, +# which is a JSON parser. So the choice was to write one or to depend on one. +# +# It is NOT in the default build, because the oracle is not: the gate here is +# the same one at the call sites in `topology_report`, and a shipping probe must +# print a self-contradicting report rather than panic on it. The module itself +# carries the gate, which is also the clearest statement of what that module is. +serde_json = { version = "1.0", optional = true } # **Every workspace dependency below is path-only, with no `version`**, because # this crate is never distributed at all -- not to a registry, and not as a # released binary either, unlike `windows-placement-probe` next door. These @@ -135,6 +155,12 @@ windows-namespace-request-sys = { path = "../windows-namespace-request-sys" } wtf-string = { path = "../wtf-string" } [dev-dependencies] +# Also here, non-optional, because `cfg(test)` builds compile `report_oracle` +# WITHOUT `oracle-in-renderer` -- the feature exists for integration tests, and +# unit tests get the module through `cfg(test)` instead. A dev-dependency is +# linked for every test target, so this is what makes the module's `cfg(any(test, +# feature = "oracle-in-renderer"))` gate resolve in both halves. +serde_json = "1.0" # A dependency on ITSELF, which cargo permits for dev-dependencies and which is # the only way to turn a feature on for this package's own integration tests: # a test under `tests/` links the library as a plain dependency, so `cfg(test)` diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index b340f8fd2..a989f0925 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -149,6 +149,14 @@ pub mod long_path; pub mod long_path_report; pub mod pool_growth; pub mod report; +/// The report oracle. **Test-support: present only where it is used.** +/// +/// Every caller is already behind this gate -- the renderers' `assert_corresponds` +/// bindings, the unit tests, and the integration tests, which reach it through the +/// self dev-dependency. Stating that here rather than leaving it implied is what +/// lets the module depend on a real JSON parser without putting one in a shipping +/// probe binary. +#[cfg(any(test, feature = "oracle-in-renderer"))] pub mod report_oracle; pub mod request_cost; pub mod row; diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 4d3edd146..c7507957a 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -38,12 +38,13 @@ //! That is not a correspondence. It is the writer's own output being read back, //! which is the one thing no amount of typing upstream can do for itself. //! -//! **The key SET is deliberately not checked here yet.** Asserting it needs a -//! list of expected keys, and a list written here would be a census that rots -- -//! this component has re-corrected the same census three times in one day. Once -//! M3.3 makes the row a typed value, the key set is derivable from the type -//! rather than declared beside it, and the check belongs there. Queued in -//! [CHECKLIST.md](../CHECKLIST.md) M3.3 rather than approximated here. +//! **The key set is checked, but not here.** This module reads a row it is +//! handed and has no way to know which keys were owed; the contract lives beside +//! the renderer that owes them, as `topology_report::MEASURED_ROW_KEYS`. An +//! earlier version of this paragraph said the check was deferred until the row +//! became a typed value, "at which point the key set is derivable from the type" +//! -- which is false, and was measured to be: a type says "a map of names to +//! values", which every key set satisfies, including the one missing a field. /// A way the report's machine-readable row is not well-formed. #[derive(Clone, Debug, PartialEq, Eq)] @@ -68,15 +69,16 @@ pub enum RowDefect { }, /// The row is not a syntactically valid JSON object. /// - /// **Structure, not only depth.** This checked that brackets BALANCED, - /// which `{"a":1,}` and `{"a":1]` both satisfy while being invalid JSON -- - /// so a writer defect producing either was accepted by the check whose whole - /// job is to read the writer's output back. Delimiters are now matched by - /// kind, and the separators between members are checked, which covers the - /// defects a hand-written writer actually produces. + /// **Decided by a real parse, not by a check written here.** The question + /// this answers is "could a consumer read this row", and a consumer uses a + /// JSON parser -- so the only answer that cannot drift from the question is + /// one a JSON parser gives. Two hand-written versions preceded this: the + /// first counted bracket depth, which `{"a":1,}` and `{"a":1]` both satisfy; + /// the second matched delimiters by kind and checked separators, and a + /// generated test still found 159 rows it accepted and `serde_json` did not. Malformed { - /// What is wrong, as specifically as the scan can say. - what: &'static str, + /// What is wrong, in the parser's own words. + what: String, /// The row, as rendered. row: String, }, @@ -156,88 +158,28 @@ pub fn check(report: &str) -> Vec { /// What is wrong with `row` as a JSON object, if anything. /// -/// **Delimiters matched by KIND, and separators checked.** This only counted -/// depth, so `{"a":1,}` and `{"a":1]` were both accepted -- each invalid JSON, -/// each exactly the kind of defect a hand-written writer produces, and each -/// silently passing the check whose whole job is reading that writer's output -/// back. Found by a review. +/// **A real parse, because the question is whether a consumer can parse it.** +/// Anything else here is a second opinion about what JSON is, and a second +/// opinion is a thing that can disagree. Both hand-written predecessors did: +/// the first counted bracket depth and accepted `{"a":1,}`; the second matched +/// delimiters by kind and checked separators, and a test that generated 1807 +/// single-character corruptions of a real row found **159 it accepted and +/// `serde_json` rejected -- every one a false accept.** Closing the last of them +/// required tracking whether an object expects a name or a value next, which is +/// a JSON parser; so this depends on one rather than growing one. /// -/// Not a full JSON parser, and it does not need to be: the producer is -/// `crate::row`, so the reachable failures are a mismatched delimiter, a stray -/// or missing separator, and an unterminated string. Numbers and keywords come -/// from typed values and cannot be malformed. -fn malformation(row: &str) -> Option<&'static str> { - let mut stack: Vec = Vec::new(); - let mut in_string = false; - let mut escaped = false; - // The last structurally significant character outside a string, so a - // separator with nothing on one side of it is visible. - let mut previous = '\0'; - - for character in row.chars() { - if escaped { - escaped = false; - continue; - } - if in_string { - match character { - '\\' => escaped = true, - '"' => { - in_string = false; - previous = '"'; - } - _ => {} - } - continue; - } - - match character { - '"' => { - if previous == '"' || previous == '}' || previous == ']' { - return Some("a string follows a value with no separator"); - } - in_string = true; - } - '[' | '{' => { - stack.push(character); - previous = character; - } - '}' | ']' => { - let opened = stack.pop(); - let expected = if character == '}' { '{' } else { '[' }; - if opened != Some(expected) { - return Some("a closing delimiter does not match the one it closes"); - } - if previous == ',' { - return Some("a trailing separator before a closing delimiter"); - } - previous = character; - } - ',' => { - if previous == ',' || previous == '{' || previous == '[' || previous == '\0' { - return Some("a separator with no value before it"); - } - previous = ','; - } - ':' => { - if previous != '"' { - return Some("a name separator that does not follow a name"); - } - previous = ':'; - } - character if character.is_whitespace() => {} - _ => previous = 'v', - } - } - - if in_string { - return Some("an unterminated string"); +/// **A parse does not subsume [`RowDefect::RepeatedKey`].** `serde_json` accepts +/// a duplicated key and silently keeps the last, which is exactly why that +/// defect is worth a check of its own: it survives the consumer's parse and +/// changes what the consumer reads. The two checks answer different questions +/// and neither replaces the other. +fn malformation(row: &str) -> Option { + // The row is required to be an OBJECT, not merely valid JSON. A bare `[1,2]` + // parses and would satisfy a laxer check, while carrying no keys at all. + match serde_json::from_str::>(row) { + Ok(_) => None, + Err(error) => Some(error.to_string()), } - if !stack.is_empty() { - return Some("an unclosed delimiter"); - } - - None } /// Every key `row` renders at its top level, in the order it renders them. diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index 177393944..10ad1c07c 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -50,6 +50,23 @@ fn a_report_with_two_rows_is_a_defect() { assert_eq!(check(&two), vec![RowDefect::Duplicated { count: 2 }]); } +/// What the parser says about `row`, for a test asserting a defect's identity +/// rather than its wording. +/// +/// **The wording is `serde_json`'s, so this crate does not get to assert it.** +/// These three tests used to name the message, which was right while the check +/// was ours -- the message WAS the finding, and a test naming it pinned which +/// branch fired. It is now a dependency's string, and pinning it would assert a +/// thing we neither own nor promise: a wording change in a patch release would +/// redden tests about unclosed delimiters, which is a false finding about this +/// crate. What survives is what these tests are actually for -- that this input +/// is rejected, and that the whole row is carried back for a reader. +fn malformation_of(row: &str) -> String { + serde_json::from_str::>(row) + .expect_err("the fixture is meant to be malformed") + .to_string() +} + #[test] fn an_unclosed_delimiter_is_a_defect() { let truncated = r#"{"reason":"x-probe-topology","caches":[{"level":1}"#; @@ -57,7 +74,7 @@ fn an_unclosed_delimiter_is_a_defect() { assert_eq!( check(&report_with(truncated)), vec![RowDefect::Malformed { - what: "an unclosed delimiter", + what: malformation_of(truncated), row: truncated.to_owned() }] ); @@ -73,7 +90,7 @@ fn a_trailing_separator_is_a_defect() { assert_eq!( check(&report_with(trailing)), vec![RowDefect::Malformed { - what: "a trailing separator before a closing delimiter", + what: malformation_of(trailing), row: trailing.to_owned() }] ); @@ -87,7 +104,7 @@ fn a_mismatched_closing_delimiter_is_a_defect() { assert_eq!( check(&report_with(mismatched)), vec![RowDefect::Malformed { - what: "a closing delimiter does not match the one it closes", + what: malformation_of(mismatched), row: mismatched.to_owned() }] ); @@ -262,3 +279,124 @@ fn a_defect_is_reported_once_per_extra_rendering_of_a_key() { row said it again" ); } + +/// Every single-character corruption of `row`, as (what was done, the result). +/// +/// Deletion and structural substitution, which between them reach the defects a +/// writer actually produces: a lost delimiter, a doubled separator, a `:` where +/// a `,` belonged, a quote that ends a string early. +fn corruptions(row: &str) -> Vec<(String, String)> { + const STRUCTURAL: [char; 8] = ['{', '}', '[', ']', ',', ':', '"', '\\']; + let mut out = Vec::new(); + + for (at, character) in row.char_indices() { + let after = at + character.len_utf8(); + + let mut deleted = String::with_capacity(row.len()); + deleted.push_str(&row[..at]); + deleted.push_str(&row[after..]); + out.push((format!("deleted {character:?} at {at}"), deleted)); + + for replacement in STRUCTURAL { + if replacement == character { + continue; + } + let mut swapped = String::with_capacity(row.len() + 1); + swapped.push_str(&row[..at]); + swapped.push(replacement); + swapped.push_str(&row[after..]); + out.push(( + format!("replaced {character:?} at {at} with {replacement:?}"), + swapped, + )); + } + } + + out +} + +#[test] +fn every_unparseable_row_reaches_the_caller_as_a_defect() { + // **This is a WIRING test, and saying so matters.** An earlier version of it + // compared `malformation` against `serde_json` and was worth running, + // because `malformation` was then a hand-written check that could disagree + // -- it generated 1807 corruptions and found 159 disagreements, every one a + // false accept, which is why the hand-written version is gone. + // + // With the parse itself delegated, that comparison would be `serde_json` + // against `serde_json`: green by construction, and exactly the kind of + // tautology this crate keeps having to delete. So the question changed. It + // is no longer "is the verdict right" -- nothing here is entitled to an + // opinion on that -- but "does the verdict REACH the caller", which is a + // property of `check` and is not guaranteed by anything upstream. A `check` + // that dropped the result, or looked at the wrong line, would still be + // delegating to a correct parser. + let clean = clean_row(); + assert!( + serde_json::from_str::(&clean).is_ok(), + "the fixture must be valid JSON before corrupting it means anything" + ); + + // A corruption that destroys the LEADING BRACE is a different finding, and + // both halves are asserted rather than one being waved through. `check` + // selects the row by `starts_with('{')`, so such a line is not a malformed + // row -- it is not a row, and the report has none. `Missing` is the right + // answer there and `Malformed` would be the wrong one, because a survey's + // question is "did this host report a row", not "was the text well-formed". + // Measured: exactly the 8 corruptions of position 0, which is what makes the + // two branches worth separating instead of accepting any defect at all. + let mut escaped = Vec::new(); + let cases = corruptions(&clean); + for (what, candidate) in &cases { + if serde_json::from_str::(candidate).is_ok() { + continue; + } + let defects = check(&report_with(candidate)); + let expected = if candidate.starts_with('{') { + defects + .iter() + .any(|defect| matches!(defect, RowDefect::Malformed { .. })) + } else { + defects.contains(&RowDefect::Missing) + }; + if !expected { + escaped.push(format!("{what}: got {defects:?}")); + } + } + + assert!( + escaped.is_empty(), + "{} of {} corruptions are unparseable and were not reported as the \ + defect they are:\n{}", + escaped.len(), + cases.len(), + escaped.join("\n") + ); +} + +#[test] +fn the_corruption_generator_reaches_defects_of_every_kind() { + // **A generator that produced nothing, or only legal strings, would leave the + // test above vacuous and green.** A generator cannot report the shape it + // never reaches, so it has to be asked what it reached. + let cases = corruptions(&clean_row()); + assert!(cases.len() > 500, "only {} corruptions", cases.len()); + + let unparseable = cases + .iter() + .filter(|(_, candidate)| serde_json::from_str::(candidate).is_err()) + .count(); + assert!( + unparseable > 100, + "only {unparseable} of {} corruptions are unparseable, so the wiring \ + test is mostly skipping its own body", + cases.len() + ); + + let parseable = cases.len() - unparseable; + assert!( + parseable > 0, + "every corruption is unparseable, so a `check` that reported `Malformed` \ + unconditionally would satisfy the wiring test" + ); +} diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 12cfb3555..89f331259 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -4,7 +4,9 @@ //! //! # These are the correspondences that survived, moved off the text //! -//! [`crate::report_oracle`] checks relations between a report's two rendered +//! The report oracle (`crate::report_oracle`, present only in builds that run +//! it, so deliberately not a link from here) once checked relations between a +//! report's two rendered //! halves. The rules worth keeping are not really about rendering -- they relate //! a STATE to the verdict, and the report is only where that relation became //! visible. They are here instead, as predicates over [`Observation`], per From 065042f4ce3df2dbd5d884cb2ec6b3e28f5ae186 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 02:05:43 -0400 Subject: [PATCH 21/46] refactor(platform-probes): delete the last hand-written string scanners M3.10. M3.9 removed one of six scanners in the oracle; this removes the rest. The module hand-writes no string walking at all now -- every byte-level decision about quotes, escapes and delimiters is serde_json's. Two of the four were provably unsafe on an argument nothing enforced. list_codes found "code":" and took the next quote as the end; list_span_end counted brackets with no notion of being inside a string. Both were safe only because every code is a &'static str from an enum and no caller text reaches a diagnostic list -- true, load-bearing, and guarded by no test. keys had already proved the class reachable: it made assert_corresponds panic from inside report_unmeasured on a quoted discovery_error. Parsing makes the argument unnecessary rather than merely correct, which is the difference between a property and a hope. keys needed a MapAccess visitor rather than a parsed map, and the reason is a contract. It must return the row's names in ORDER and WITH DUPLICATES, and serde_json::Map sorts while silently keeping the last of a repeated key -- which would delete the evidence for RowDefect::RepeatedKey, the one malformation that survives a consumer's parse. The visitor reads each name as the parser reads it, so both properties survive while every scanning decision stays serde_json's. That reasoning is now a sabotage entry rather than a comment: replacing the visitor with the obvious Map one-liner is caught. list_span_end was deleted, not moved. Its only caller was a sabotage doing text surgery on a list; that sabotage now parses, empties the lists and re-renders. It is the third implementation, after one that split on commas -- which sliced entries in half once they became objects -- and one that used this helper. A sabotage that hand-parses is a sabotage that can quietly stop sabotaging while still passing, leaving the rule it guards unguarded. The stale manifest entry is itself the evidence. After the change the sweep reported MANIFEST STALE: pattern found 0 times for the escape-awareness entry, because the code that could hold that defect is gone. Replaced with the parsed-map entry; 7 of 7 behave as declared. serde joins serde_json as an optional dependency behind the same oracle-in-renderer gate. Default build re-verified by binary inspection: neither appears on a normal dependency edge and probe-topology.exe carries no parser string. cargo doc checked in both feature shapes. Completed item: M3.10: Delete the last hand-written string scanners in the oracle Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1 + crates/windows-platform-probes/CHECKLIST.md | 36 ++++ crates/windows-platform-probes/Cargo.toml | 10 +- crates/windows-platform-probes/sabotage.json | 27 +-- .../src/report_oracle.rs | 180 ++++++------------ .../tests/a_real_report_agrees_with_itself.rs | 34 ++-- 6 files changed, 132 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10fcbd3b8..539b402e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -216,6 +216,7 @@ dependencies = [ name = "windows-platform-probes" version = "0.0.1" dependencies = [ + "serde", "serde_json", "windows-namespace-request-sys", "windows-placement-probe", diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 48c15c4ae..4d71da172 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -486,6 +486,42 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. contains no `serde_json`, no oracle panic string, and no parser message; the `--all-features` one contains all three. +- [x] **M3.10** -- Delete the last hand-written string scanners in the oracle. + + M3.9 removed one of six; this removes the rest. **The module hand-writes no string walking at + all now** -- every byte-level decision about quotes, escapes and delimiters comes from + `serde_json`. + + **Two of the four were provably unsafe, on an argument enforced by nothing.** `list_codes` found + `"code":"` and took the next `"` as the end, and `list_span_end` counted brackets with no notion + of being inside a string. Both were safe only because every code is a `&'static str` from an enum + and no caller text reaches a diagnostic list -- true, load-bearing, and guarded by no test. + `keys` had already proved the class reachable: it made `assert_corresponds` panic from inside + `report_unmeasured` on a quoted `discovery_error`. Parsing makes the argument unnecessary rather + than merely correct, which is the difference between a property and a hope. + + **`keys` needed a visitor rather than a parsed map, and the reason is a contract.** It must return + the row's names in ORDER and WITH DUPLICATES. `serde_json::Map` sorts, and silently keeps the last + of a repeated key -- which would delete the evidence for `RowDefect::RepeatedKey`, the one + malformation that survives a consumer's parse. A `MapAccess` visitor reads each name as the parser + reads it, so both properties survive while every scanning decision stays `serde_json`'s. That + reasoning is now a sabotage entry rather than a comment: replacing the visitor with the obvious + `Map` one-liner is `caught`. + + **`list_span_end` was deleted, not moved.** Its only caller was a sabotage doing text surgery on a + list. That sabotage now parses, empties the lists and re-renders -- its third implementation, after + one that split on commas (which sliced entries in half once they became objects) and one that used + this helper. A sabotage that hand-parses is a sabotage that can quietly stop sabotaging, and it + leaves the rule it guards unguarded while still passing. + + **The stale sabotage entry is itself the evidence.** After the change the sweep reported + `MANIFEST STALE: pattern found 0 times` for the escape-awareness entry -- the defect it injected + can no longer be expressed, because the code that could hold it is gone. Replaced with the + parsed-map entry above; 7 of 7 behave as declared. + + Default build re-verified by binary inspection: neither `serde` nor `serde_json` appears on a + normal dependency edge, and `probe-topology.exe` contains no parser string. + ## M4 -- Carried over from M2: the items M3 gates These were written under M2 and are blocked on M3 above: each one targets the prose-against-row diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index 5046459d4..0371c245a 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -36,7 +36,7 @@ path = "src/lib.rs" # print-rather-than-panic guarantee. # Pulls in `serde_json` because the oracle's well-formedness check IS a real # parse. See the `[dependencies]` entry for why that is not hand-written. -oracle-in-renderer = ["dep:serde_json"] +oracle-in-renderer = ["dep:serde_json", "dep:serde"] [[bin]] name = "probe-error-mode" @@ -153,6 +153,13 @@ windows-namespace-request-sys = { path = "../windows-namespace-request-sys" } # over that is a vocabulary one -- the workspace's own WTF-16 type naming the # quantity rather than an ad-hoc count that happens to agree. wtf-string = { path = "../wtf-string" } +# Beside `serde_json`, and gated with it, for ONE reason: `keys` must report the +# row's top-level names in ORDER and WITH DUPLICATES, and a parsed map can give +# neither -- `serde_json::Map` sorts, and silently keeps the last of a repeated +# key, which is the very defect `RowDefect::RepeatedKey` exists to report. A +# `MapAccess` visitor sees the names as the parser reads them, so the scan is +# still the real parser's and this crate hand-writes no string walking at all. +serde = { version = "1.0", optional = true } [dev-dependencies] # Also here, non-optional, because `cfg(test)` builds compile `report_oracle` @@ -160,6 +167,7 @@ wtf-string = { path = "../wtf-string" } # unit tests get the module through `cfg(test)` instead. A dev-dependency is # linked for every test target, so this is what makes the module's `cfg(any(test, # feature = "oracle-in-renderer"))` gate resolve in both halves. +serde = "1.0" serde_json = "1.0" # A dependency on ITSELF, which cargo permits for dev-dependencies and which is # the only way to turn a feature on for this package's own integration tests: diff --git a/crates/windows-platform-probes/sabotage.json b/crates/windows-platform-probes/sabotage.json index 912606ff0..dd96d1ccc 100644 --- a/crates/windows-platform-probes/sabotage.json +++ b/crates/windows-platform-probes/sabotage.json @@ -45,31 +45,18 @@ ] }, { - "name": "the row's key reader is not escape-aware", + "name": "the row's key reader uses a parsed map", "file": "src/report_oracle.rs", "expect": "caught", - "why": "`string_end` stops at the first quote regardless of a preceding backslash, so the reader disagrees with the writer and `assert_corresponds` panics from inside the renderer on any report carrying a quoted discovery error.", + "why": "The obvious simplification, and it is wrong twice over: a serde_json::Map SORTS its names and silently keeps the LAST of a repeated key -- so this loses the row's key ORDER, which is part of the contract MEASURED_ROW_KEYS states, and it deletes the evidence for RowDefect::RepeatedKey, the one malformation that survives a consumer's parse. This entry exists so that the reason the visitor is not a map cannot be forgotten.", "find": [ - "fn string_end(row: &str, from: usize) -> Option {", - " let mut escaped = false;", - " for (at, character) in row[from..].char_indices() {", - " if escaped {", - " escaped = false;", - " continue;", - " }", - " match character {", - " '\\\\' => escaped = true," + " let mut reader = serde_json::Deserializer::from_str(row);", + " serde::Deserializer::deserialize_map(&mut reader, TopLevelNames).unwrap_or_default()" ], "replace": [ - "fn string_end(row: &str, from: usize) -> Option {", - " let mut escaped = false;", - " for (at, character) in row[from..].char_indices() {", - " if escaped {", - " escaped = false;", - " continue;", - " }", - " match character {", - " '\\\\' => {}" + " serde_json::from_str::>(row)", + " .map(|members| members.into_iter().map(|(name, _)| name).collect())", + " .unwrap_or_default()" ] }, { diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index c7507957a..a25593f24 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -142,12 +142,10 @@ pub fn check(report: &str) -> Vec { return found; } - let mut seen: Vec<&str> = Vec::new(); + let mut seen: Vec = Vec::new(); for key in keys(row) { if seen.contains(&key) { - found.push(RowDefect::RepeatedKey { - key: key.to_owned(), - }); + found.push(RowDefect::RepeatedKey { key }); } else { seen.push(key); } @@ -182,149 +180,91 @@ fn malformation(row: &str) -> Option { } } -/// Every key `row` renders at its top level, in the order it renders them. +/// Every key `row` renders at its top level, in the order it renders them, and +/// INCLUDING repeats. +/// +/// **Read from the parser's own tokens, not by walking the bytes.** Both +/// properties this returns are ones a parsed map destroys: `serde_json::Map` +/// sorts its names, and silently keeps the last of a repeated key -- which is +/// exactly the defect [`RowDefect::RepeatedKey`] reports, so parsing into a map +/// would delete the evidence. A `MapAccess` visitor sees each name as the parser +/// reads it, which keeps both while leaving every escape, quote and delimiter +/// decision to `serde_json`. +/// +/// The hand-written version of this was the last string scanner here, and it had +/// already produced a real defect: it used `find('"')`, which takes `\"` for a +/// terminator, so a `discovery_error` carrying an escaped quote shifted where it +/// thought strings began and text INSIDE the error was emitted as top-level +/// keys. Measured: an `io::Error` of `q":1,"q":1,"q` rendered a row that +/// `JSON.parse` accepts with four keys, and `assert_corresponds` panicked from +/// inside the renderer. Fixing that added escape-awareness to one of the +/// scanners and left the others to be argued about; this removes the question. /// /// Top level only, deliberately: a nested object's members are that object's /// keys, and repeating one there is a different question from repeating one in -/// the row. +/// the row. The visitor reads nested values as [`serde::de::IgnoredAny`], which +/// consumes them without collecting their names. #[must_use] -pub fn keys(row: &str) -> Vec<&str> { - let mut names = Vec::new(); - let mut depth = 0_i32; - let mut at = 0; - - while let Some(open) = next_string(row, at) { - // Brackets BETWEEN strings are the only ones that count. Inside a - // string they are text -- a failed discovery's message may contain any - // of them. - for character in row[at..open].chars() { - match character { - '[' | '{' => depth += 1, - ']' | '}' => depth -= 1, - _ => {} - } - } - - let Some(close) = string_end(row, open + 1) else { - break; - }; - let name = &row[open + 1..close]; - let tail = row[close + 1..].trim_start(); +pub fn keys(row: &str) -> Vec { + struct TopLevelNames; - // A name followed by `:` at depth 1 is a key of the row itself. Anything - // else is a value, or a key of a nested object. - if tail.starts_with(':') && depth == 1 { - names.push(name); - } + impl<'de> serde::de::Visitor<'de> for TopLevelNames { + type Value = Vec; - at = close + 1; - } - - names -} - -/// Where the next string starts at or after `from`. -/// -/// There is nothing to skip here -- a quote outside a string always opens one -- -/// but it is named so the pair with [`string_end`] reads as a scan rather than -/// as two bare `find` calls. -fn next_string(row: &str, from: usize) -> Option { - row[from..].find('"').map(|at| from + at) -} - -/// Where the string opening before `from` closes, honouring `\` escapes. -/// -/// **This is the half [`keys`] was missing, and it was reachable.** `keys` used -/// `find('"')`, which takes `\"` for a terminator -- so a `discovery_error` -/// carrying an escaped quote shifted the parser's idea of where strings begin -/// and end, and text INSIDE the error was emitted as top-level keys. Two equal -/// ones then read as a repeated key. -/// -/// Measured before this fix: `report_unmeasured` given an `io::Error` of -/// `q":1,"q":1,"q` rendered a row `JSON.parse` accepts with four keys, and -/// `assert_corresponds` panicked from inside the renderer -- a correct report -/// crashing the probe, which is the failure mode containment exists to prevent. -/// [`balanced`] already had this state machine; `keys` did not, so the reader -/// disagreed with the writer. -fn string_end(row: &str, from: usize) -> Option { - let mut escaped = false; - for (at, character) in row[from..].char_indices() { - if escaped { - escaped = false; - continue; - } - match character { - '\\' => escaped = true, - '"' => return Some(from + at), - _ => {} + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("the probe's machine-readable row, a JSON object") } - } - - None -} -/// Where the list opening at `start` closes, if it closes. -/// -/// Depth-aware, because each entry is an object: the first `]` after the opening -/// bracket may belong to a nested list rather than to this one. -/// -/// Public alongside [`list_codes`] because an instrument that SABOTAGES a list -/// needs the same span the reader uses. A test that cut on commas instead broke -/// silently when the entries became objects -- and a sabotage that no longer -/// sabotages leaves the rule it guards unguarded while still passing. -#[must_use] -pub fn list_span_end(row: &str, start: usize) -> Option { - let mut depth = 0_i32; - for (at, character) in row[start..].char_indices() { - match character { - '[' | '{' => depth += 1, - '}' => depth -= 1, - ']' if depth == 0 => return Some(start + at), - ']' => depth -= 1, - _ => {} + fn visit_map>( + self, + mut members: A, + ) -> Result { + let mut names = Vec::new(); + while let Some(name) = members.next_key::()? { + names.push(name); + members.next_value::()?; + } + Ok(names) } } - None + let mut reader = serde_json::Deserializer::from_str(row); + serde::Deserializer::deserialize_map(&mut reader, TopLevelNames).unwrap_or_default() } - /// The `code` of every entry in `row`'s list-valued `key`. /// /// **One definition, because two instruments need it.** The diagnostic lists -/// hold objects -- `{"code":"contradictory_cores","count":3}` -- so reading them -/// means finding each entry's `code` member rather than splitting on commas, -/// which nested objects break. Both the unit tests and the publication -/// accounting ask this question, and a second implementation of it is the kind -/// of copy that agrees until it does not. +/// hold objects -- `{"code":"contradictory_cores","count":3}` -- and both the +/// unit tests and the publication accounting ask this question. A second +/// implementation of it is the kind of copy that agrees until it does not. +/// +/// Read from a parse. The previous version searched for `"code":"` and then took +/// the next `"` as the end, which is not escape-aware: a code containing a quote +/// would have truncated. That was argued safe because every code is a +/// `&'static str` from an enum and no caller text reaches a list -- an argument +/// that was true, load-bearing, and enforced by nothing. Parsing makes the +/// argument unnecessary rather than merely correct, which is the difference +/// between a property and a hope. /// /// Returns empty for a key that is absent or not a list, which is the same /// answer as an empty list on purpose: a consumer of this is asking "what /// conditions are published", and "none" is the answer in both cases. #[must_use] pub fn list_codes(row: &str, key: &str) -> Vec { - let needle = format!("\"{key}\":["); - let Some(start) = row.find(&needle).map(|at| at + needle.len()) else { + let Ok(parsed) = serde_json::from_str::(row) else { return Vec::new(); }; - let Some(end) = list_span_end(row, start) else { + let Some(entries) = parsed.get(key).and_then(serde_json::Value::as_array) else { return Vec::new(); }; - let mut codes = Vec::new(); - let mut rest = &row[start..end]; - const CODE: &str = "\"code\":\""; - while let Some(at) = rest.find(CODE) { - let after = &rest[at + CODE.len()..]; - let Some(close) = after.find('"') else { - break; - }; - codes.push(after[..close].to_owned()); - rest = &after[close..]; - } - - codes + entries + .iter() + .filter_map(|entry| entry.get("code")) + .filter_map(serde_json::Value::as_str) + .map(str::to_owned) + .collect() } - /// The report's machine-readable row, if it carries exactly one well-formed one. /// /// Public because the instruments in `tests/` read the row to ask what it diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index b999dd31c..3f223caa0 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -613,29 +613,33 @@ fn a_state_the_row_does_not_publish_fails_the_accounting() { ); // Now empty every diagnostic list in the row, which is what a renderer that - // forgot to publish one would produce. Done by replacing each list's whole - // contents rather than by editing entries: the entries are objects now, so - // splitting on commas cuts them in half -- which is how this sabotage broke - // when M3.3 landed, and is worth the note because a sabotage that no longer - // sabotages leaves the rule it guards unguarded while still passing. + // forgot to publish one would produce. + // + // **Done by parsing, emptying and re-rendering, not by cutting the text.** + // Two earlier versions cut it: the first split on commas, which sliced the + // entries in half once they became objects; the second used a span helper in + // the oracle that was not string-aware. Both are the same mistake -- a + // sabotage that hand-parses is a sabotage that can stop sabotaging while + // still passing, and it leaves the rule it guards unguarded. Rebuilding from + // a parse also produces a row that is genuinely valid, so what this feeds the + // accounting is a report a renderer could really have emitted. let stripped = text .lines() .map(|line| { if !line.starts_with('{') { return line.to_owned(); } - let mut out = line.to_owned(); + let Ok(mut parsed) = serde_json::from_str::(line) else { + return line.to_owned(); + }; for key in DIAGNOSTIC_LISTS { - let needle = format!("\"{key}\":["); - let Some(start) = out.find(&needle).map(|at| at + needle.len()) else { - continue; - }; - let Some(end) = report_oracle::list_span_end(&out, start) else { - continue; - }; - out.replace_range(start..end, ""); + if let Some(list) = parsed.get_mut(*key) + && list.is_array() + { + *list = serde_json::Value::Array(Vec::new()); + } } - out + parsed.to_string() }) .collect::>() .join("\n"); From a0677834e32fe7f33170281dfb837d2c59b7315d Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:37:16 -0700 Subject: [PATCH 22/46] docs(platform-probes): close M3 and archive it All ten M3 items are complete, so the group migrates to COMPLETED-CHECKLIST.md and the active checklist is left holding only M4 and M5. Both PLANS.md entries said "M3.1 has landed", written when M3.1 was the only item; ten had landed since, including two that reversed decisions the same paragraph described. The re-scoping notes did NOT go to the archive. M4's preamble said "M3's preamble says which M3 item gates each of them", and the notes it meant -- how M3.2 re-scoped M2.4, how M3.1 and M3.3 gated M2.5, what evidence M3.4 took away from M2.15, how M3.5 re-scoped M2.17 -- are instructions for work that is still open. A pending instruction does not belong in an append-only file nobody may edit to correct it, so they moved into M4's preamble and M3's record carries a pointer rather than a second copy. Also corrected three statements the closure falsified: M4's heading and preamble spoke of M3 in the present tense, M5's said its items could be pulled ahead of "M3 or M4", and M2.14.1's body still said this crate carries no sabotage manifest -- true when written, false since the item it describes was completed. End-of-milestone gate, all clean: cargo check --all-targets on the DEFAULT workspace (not --workspace, so the deliberately excluded members stay excluded) in debug and release, zero compiler diagnostics in either; the in-scope crate's tests unfiltered so doctests run, 213 passed 0 failed, with Doc-tests reporting 0 -- confirmed real rather than a harness quirk, the crate has no doc code blocks at all; encoding check 642 files clean; both markdown files verified 7-bit ASCII and free of dangling anchors. git fetch reports origin/main at 3f31e92, already contained in HEAD, so there was nothing to integrate and no ON-GIT-MERGE-OR-REBASE trigger. Not pushed, at the engineer's instruction. Completed item: M3 (milestone closure): archive the completed group, update both PLANS.md entries, and relocate the still-live re-scoping notes into M4 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- PLANS.md | 2 +- crates/windows-platform-probes/CHECKLIST.md | 552 +----------------- .../COMPLETED-CHECKLIST.md | 504 ++++++++++++++++ crates/windows-platform-probes/PLANS.md | 2 +- 4 files changed, 536 insertions(+), 524 deletions(-) diff --git a/PLANS.md b/PLANS.md index dcd7e609b..3ad49451d 100644 --- a/PLANS.md +++ b/PLANS.md @@ -20,7 +20,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) | in progress | M22-M23: extract the captured-context composite into `windows-thread-ambient-sys`, a standalone platform layer that captures a thread's ambient state and applies it on another thread. M24-M26: `windows-namespace-request-sys`, marshalable Win32 namespace call parameter sets, over a round-one entry list audited from three real consumers (this repository's watcher and enumeration crates, and `MikeGrier/Globazog-rs`) rather than guessed. M27: `windows-platform-probes`, a durable home for the measurements this workspace's designs rest on, under a three-tier scheme (asserted / ignored / binary-only) where every tier is compiled by an ordinary build. Feature-scoped and deleted when complete; it is the whole of the `mikegrier/thread-ambient` branch's work, and is deliberately separate from the deferred namespace-facility items in [CHECKLIST.md](CHECKLIST.md). | [crates/windows-thread-ambient-sys/DESIGN-NOTES.md](crates/windows-thread-ambient-sys/DESIGN-NOTES.md) | | [crates/windows-overlapped-io-sys/CHECKLIST.md](crates/windows-overlapped-io-sys/CHECKLIST.md) | not started | M14: finish the contract audit -- categories 1, 2, 6, 8, 9 were not examined -- and sweep `outstanding()` for the advisory-predicate hazard. | [crates/windows-overlapped-io-sys/DESIGN-NOTES.md](crates/windows-overlapped-io-sys/DESIGN-NOTES.md) | | [crates/windows-ioring-sys/CHECKLIST.md](crates/windows-ioring-sys/CHECKLIST.md) | in progress | Memory-safe Rust over the Windows `IoRing` submission/completion ring, as a new crate. M1-M7 (ring lifecycle through the `ring-copy` topology-aligned sample) are complete and archived. The parked, pinned-thread `M6+` work and the new M10 contract audit remain. | [crates/windows-ioring-sys/DESIGN-NOTES.md](crates/windows-ioring-sys/DESIGN-NOTES.md) | -| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row published its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` could not tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row published its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` could not tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. M3 is complete and archived (ten items): those three fields publish arrays of condition codes; the surviving correspondences became invariants over the observation rather than over two renderings, so a rule that reads the diagnostic lists -- which would be a restatement of `verdict()` and blind to a deleted push site -- was rewritten to read the observation; the row is emitted from a typed value through one writer with total escaping, which is the crate's only defence against caller text reaching the mined artifact; the prose oracle and every parser serving it were deleted, and no test extracts structured data from prose anywhere in the crate. Four later items came from reviews and are the more instructive half: three instruments were found asserting less than their names claimed, `BlockingState::ALL` was found to be a census the compiler did not check despite a doc comment claiming it did, and the row's hand-written JSON well-formedness check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a FALSE accept -- and replaced by `serde_json`, after which the remaining hand-written string scanners were deleted too. What remains is M4 (four M2 leftovers M3 gated, now unblocked and re-scoped) and M5 (six ungated hygiene items). | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | Add a row here when new work is planned, against [CHECKLIST.md](CHECKLIST.md) or any crate's. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 4d71da172..4e2791d68 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -5,528 +5,35 @@ separately, in the workspace [CHECKLIST-thread-ambient.md](../../CHECKLIST-threa M27; that file is feature-scoped and is deleted when its feature completes, so durable follow-up work for the crate belongs here instead. -## M3 -- Make the encoded row the contract, and stop checking the prose against it - -Decided in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is -not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), from the session in -[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). - -The row is a machine contract mined across a fleet; the prose is for a reader. They carry different -obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and -readable**, enforced by review. Nothing is required to hold *between* them. - -Re-checked against the code rather than against M2's account of it: the original defect was fixed, -and what it left behind was larger. The row published `not_compared`, `parse_incomplete` and -`enumeration_anomalies` as **counts**, where the prose printed each entry's text. A survey reading -`"parse_incomplete":1` could not tell *the probe detected a bug in itself* from *a core record -contradicted itself* from *this topology was not measured from a running machine*. **The row was -impoverished relative to the prose** -- the artifact that gets mined carried less than the artifact -that gets read. M3.1 has since closed that particular gap; the rest of the milestone is about which -artifact carries the contract, and stands whole. - -**M2 completed with this decision**, and its ten open items were re-sequenced rather than reworked. -The milestone's own work -- the oracle, the binding, the real-host test, the derived fact set, the -partitioning discriminator and the shape corpus -- is done and archived. The leftovers had -accumulated under a heading none of them fit, and they split by whether M3 gates them: four are in -M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. - -- **M2.18 (typed banner) is dissolved into M3.3**, not carried over. It was the smallest instance - of "should a report be a value a writer renders, or a string the renderer concatenates", and - answering it alone would have typed one parameter while leaving the shape everywhere else. -- **M2.4** is re-scoped by M3.2. The exploration is still worth doing and its instrument is - unchanged, but what it hunts for changes: invariants over `Coherence`, `BracketOutcome` and +## M4 -- Carried over from M2: the items M3 gated + +These were written under M2 and were blocked on M3, which is **now complete and archived** in +[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). They are unblocked. + +Each targeted the prose-against-row machinery M3 retired or relocated, so none was merely delayed -- +each was RE-SCOPED, and the re-scoping is what makes them safe to pick up. Those notes are below. +They were written in M3's preamble and moved here when M3 was archived, because they describe work +that is still open: an instruction for a pending item is not history, and leaving it in the archive +would have left this milestone pointing at a file it may not edit. + +- **M2.4** was re-scoped by M3.2. The exploration is still worth doing and its instrument is + unchanged, but what it hunts for changed: invariants over `Coherence`, `BracketOutcome` and `Verdict` as VALUES, and facts the row fails to publish -- not correspondences between two renderings. Its closing sentence, "promote only what proves meaningful into the oracle from M2.1", now means the invariant set from M3.2. The open question attached to it -- whether this generalises past this crate -- survives unchanged and is arguably sharpened, since a data-level invariant is easier to share than a text reader. -- **M2.5** is gated by M3.1 and M3.3. Establishing that the middle of three discoveries agreed - produces a new FACT, which M3.1 says must reach the row rather than only the banner; and M3.3 - changes how the banner is built. Written first, it would be written into machinery about to move. +- **M2.5** was gated by M3.1 and M3.3, both landed. Establishing that the middle of three discoveries + agreed produces a new FACT, which M3.1 says must reach the row rather than only the banner; and + M3.3 changed how the banner is built. Written before those, it would have been written into + machinery about to move. - **M2.15** keeps its conclusion but loses its evidence. The five failures it cites were all - `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the correspondence M3.4 retires, - so afterwards they would not occur and a re-run would look clean. The underlying point stands - without them: CI builds `aarch64` and never tests it, and architecture is the one shape dimension - a corpus cannot vary because it is fixed at compile time. Restate it on that basis when picked up. -- **M2.17** is re-scoped by M3.5: the dimensions worth crossing become the row's, and crossing prose - shapes that are about to stop being checked would aim at the retiring half. -- **M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are gated by nothing** and are in M5. M2.9 (a - cross-host ratio called "the finding") and M2.14 (making two authoring rules bite) are if anything - reinforced: - under this decision prose accuracy is a review obligation rather than a machine-checked one, which - puts more weight on both. - -- [x] **M3.1** -- Publish each diagnostic as itself, not as a count. - - `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as - `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- - *which* condition occurred -- exists only in prose. Publish the entries, and give each a stable - machine-readable discriminant rather than the human sentence, so a survey can group by condition - without matching on English that is free to be reworded. The sentences stay in the prose, where - rewording them is harmless. - - **The rule this establishes, which is the durable half:** a renderer may not tell a reader - something the row cannot tell a survey. A cardinality is not a statement of the fact. - - **Done.** Three enums in `topology::diagnostic` -- 21 + 6 + 3 variants, one per condition -- each - carrying its data, rendering its sentence through `Display`, and naming itself through `code()`. - `CrossCheck`'s three `Vec` became `Vec`, and the row publishes arrays of codes - where it published `.len()`. The prose is byte-identical: the loops write `{entry}` and `Display` - emits the same sentences. - - **The wire format changed**, deliberately and not additively: `"parse_incomplete":1` is now - `"parse_incomplete":["partitioning_summary_missing"]`. The count is still available as the list's - length, so nothing is lost, and publishing both would be a restatement that can drift. Same shape - as the `efficiency_classes` correction that preceded it. - - Sabotage-verified, each mutation injected on its own line and reverted: renaming - `PartitioningSummaryMissing`'s code reddens only - `the_row_names_the_probes_own_bug_when_it_detects_one`; making the row keep only the first - condition reddens the two list tests, through the bound oracle's count rule; mislabelling - `TrailingBytes` reddens only `an_anomaly_reaches_the_row_as_its_kind`. - - **The substring-to-variant conversion cost two assertions their discrimination, found by review.** - `c.contains("no online processors and processor groups")` became - `matches!(c, MeasuredButCountsAbsent { .. })`, which holds when the entry names only ONE of the - two -- exactly what the test forbids -- and the loop's labels stopped being asserted at all. - Measured: with `absent` truncated to its first entry the whole suite stayed green at 249 passed. - Both now assert the variant's `absent` payload, and both were observed to fail -- the truncation - reddens the both-absent test, and swapping the two names reddens both. The general lesson is that - converting an assertion from a substring to a variant DROPS whatever the substring discriminated - inside the payload; the variant is the weaker claim unless the payload comes with it. - - **Which conditions are listed is deliberately not compared against the prose.** The code and the - sentence come from one variant, so there is no second implementation to disagree through -- the - correspondence holds by construction, which is stronger than a check. What remains checkable, and - is checked, is that both renderings list the same NUMBER. Found while converting the accounting - instrument: a mutation that swapped one code for another went unnoticed on the - `verdict incomplete` shape, because the oracle reads the length. `corruptions` now APPENDS a code - rather than substituting one, so the length always differs. - -- [x] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before - rendering. - - Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three - oracle rules that survive the decision. They stop being comparisons of two rendered texts and - become predicates over `Observation` and `CrossCheck` -- `SummaryMissing` implies the verdict is not - `agree`, a non-empty `parse_incomplete` implies the verdict is not `agree`, and so on. No parser is - involved, and the check runs whether or not anything was rendered. - - Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, - restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. - - **Done, and the item's own framing was wrong in a way worth recording.** It named - "diagnostics-against-verdict" and "counters-against-verdict" as rules to move. Two of those read - `CrossCheck`'s lists -- and `verdict` is a pure function of those lists, so such a rule restates - the definition, cannot fail for any input, and CANNOT CATCH A DELETED PUSH SITE: the deletion - empties the list, the rule sees nothing, and the verdict is `agree` legitimately. Written that - way first, with three tests that asserted acceptance under violation-sounding names. - - Every rule now reads the OBSERVATION. `blocking_states` names ten states that forbid an agreeing - verdict, each with a push site in `cross_check` that it does not consult, plus the two counter - rules. `check` takes the verdict rather than deriving it, so a test can supply the answer a - broken `cross_check` would give -- otherwise every branch is reachable only by editing the source - and a green run says nothing. - - Bound at `observe` (every observation MEASURED, rendered or not -- what this item asked for) and - at `report` (every observation RENDERED, which on the test side is most of them, since the suite - builds observations by hand). Not in `cross_check`, which would recurse. - - Sabotage: deleting the `PartitioningSummaryMissing` push reddens four tests, two of them new -- - the invariant's own accounting test, and a render test through `assert_holds` at the renderer - binding. The invariant is not the sole detector for that push site; its value is the nine others, - several of which have no dedicated test. - -- [x] **M3.3** -- Emit the row from a typed value through one writer. - - > **-> PREREQUISITE: M3.4 lands first.** The reason is on M3.4: this item's nested per-entry data - > makes `ndjson_list_len` silently miscount, so the parsers it would break should be gone before - > the row changes shape rather than taught a shape they are about to lose. - - The row is built today by interpolating every value positionally into a `concat!` template. - Two defect classes follow from that construction and both are closed by replacing it, not by - checking it: - - **Injection.** Measured on PR #88: an `io::Error` containing `{` was selected as the report's - machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text - reaching the mined artifact is contamination of the contract. - - **Field order and labelling.** A field's name and its value are related only by counting - positions, so a reordered argument or a miscounted placeholder yields mislabelled data that - still parses, which nothing downstream can detect. Stated as the coupling rather than as a - count of placeholders: that count was written twice and wrong twice within an hour. - - A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the - writer here rather than adding a serialization dependency -- this crate has none and the row is - one flat object. - - **Carry each diagnostic's DATA, which M3.1 left behind.** M3.1 publishes a condition's code but - not the values its variant holds -- a survey learns `contradictory_cores` without learning that - three cores contradicted themselves. The variants already carry those values, for `Display`; what - stopped M3.1 publishing them is that the row is still a positional `concat!` template, where a - nested per-entry object has to be hand-assembled. Once the row is typed this is a field like any - other. Not deferred for want of a consumer -- the shape of the row is the blocker, and it is this - item. This subsumes M2.18: the banner becomes a typed field like any other, and the - question of who may construct one is answered by the row's constructor rather than separately. - - **Done.** `crate::row` holds a `Value` and a `Row` whose members are name-and-value pairs, with - one writer that escapes strings. Both defect classes are now unrepresentable rather than - detected: a name and its value move together or not at all, and a `Value::Text` cannot end the - string it is in. - - Each diagnostic publishes its DATA through `published()`, so a survey learns - `{"code":"contradictory_cores","count":3}` rather than the code alone -- what M3.1 had to leave - behind because the row was a positional template. Anomalies carry `source` and `offset` too: - the same kind at the same offset across a fleet is a different finding from the same kind - scattered, and neither is visible from a count. - - `report_unmeasured` goes through the same writer, and that is the shape that most needed it -- - it is the only renderer that interpolates caller text, a failed discovery's `io::Error`. The - error now reaches the row as a `discovery_error` field, so a survey can group failures by cause - instead of parsing the prose sentence. - - The key-set check M3.4 deferred here now exists -- but NOT in the form M3.4 predicted, and the - first attempt at it was vacuous. See the correction recorded under M3.7. - - **Two silent behaviour changes were caught by checking the old code rather than trusting the - rewrite.** `PartitioningCache` has FIVE variants, not the four a rewrite naturally reaches for; - and `SummaryMissing` publishes its level rather than `null` -- which matters precisely because - that arm is the report telling a reader the probe has a bug, and WHICH level went unchecked is - what they need. - - Sabotage-verified: removing the quote escape reddens three row tests, including the - brace-injection one. The clean row is byte-identical to what the template produced, confirmed - against a real `probe-topology` run. - -- [x] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. - - > **-> DO THIS BEFORE M3.3, and leave both IDs where they are.** M3.3 carries each diagnostic's - > data, which turns the flat code arrays into arrays of OBJECTS -- and `ndjson_list_len` splits on - > `,`, documented as safe for flat code arrays and nothing else. Pointed at - > `[{"code":"contradictory_cores","cores":3}]` it counts members rather than entries and returns 2 - > for one entry. It does not fail; it silently answers wrong, and every prose-comparison rule then - > compares that against the prose. Running M3.3 first therefore means teaching parsers a nested - > shape and deleting them one item later, with a silent-wrong-answer window in between. The IDs - > stay put because renumbering costs more than the mismatch, the same trade as M4/M5. - > - > Intended order for the rest of M3: **M3.2 -> M3.4 -> M3.3 -> M3.5**. - - > **-> CODE REVIEW RESUMES HERE.** Reviews are paused by the engineer's decision of 2026-09-12 - > until this crate no longer depends on prose as the oracle's subject, and this is the item that - > ends that dependence. The reasoning: a large share of PR #88's fifteen fix commits were defects - > in the prose-reading machinery -- the multibyte panic in `processors_in_banner`, `trim_matches` - > collapsing `[[0]]` and `[0]`, `prose_field` selecting the wrong line -- and every one of them is - > code this item deletes. Reviewing it closely is polishing something already scheduled for - > demolition. - > - > Recorded with the honest counterweight, so the decision can be re-judged on evidence rather than - > re-argued: of the six findings across the two reviews run on 2026-09-12, none was a defect in - > the prose oracle. Two were documentation drift, one was a test that had lost its - > discrimination, and the most valuable -- `disagreements` reaching the prose and not the row at - > all -- was about the ROW being incomplete and survives this item untouched. - - Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence - rules, four are comparison helpers, and **twenty-three exist only to extract values back out of - rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. - - What stays is a thin check that the row is **well-formed** -- it parses, it carries the expected - key set, and it is the only such line in the report. That is not a correspondence; it is the - writer's own output being checked, and the writer is the one place structure cannot check itself. - - Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for - a format that no longer has two readers. - - **Done, together with M3.5, because they cannot be separated.** The fact-accounting instrument is - built entirely on `report_oracle::check` and the `Correspondence` variants, so deleting the - correspondences leaves it measuring nothing and the suite red between the two items. Committed as - one commit citing both IDs, per the checklist rule for coupled items, rather than split into a - commit that does not pass. - - Measured: `report_oracle.rs` 79,394 -> 8,874 bytes, its tests 105,299 -> 5,598, the integration - instrument 71,007 -> 25,689. All eight prose correspondences and all twenty-three extraction - helpers are gone. - - What survives is the row's well-formedness: exactly one machine-readable line, brackets balanced - (string-aware, because a failed discovery's `io::Error` is interpolated into a string value and an - OS message is free to contain a bracket), and no repeated top-level key. That last one is the - malformation that survives a consumer's parse and changes what it reads, since most JSON readers - take the last. - - (The bracket check was weaker than this sentence implies -- it counted depth, so a trailing or - misplaced separator passed. Strengthened in M3.7.) - - **The key-set check is deliberately NOT here.** Asserting it needs a list of expected keys, and a - list written here is a census -- this component re-corrected the same census three times in one - day. M3.3 makes the row a typed value, at which point the key set is derivable from the type - rather than declared beside it. Moved there rather than approximated here. - - (**The second sentence is wrong, and M3.7 corrects it.** A key set is NOT derivable from a typed - row: the type says "a row is a map of names to values", which is satisfied by every key set, - including the one missing a field. The census this note was right to fear is a count; a schema is - not one, and refusing to write it down bought nothing.) - -- [x] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. - - **The instrument enumerates in one direction only, and the other direction is where M3.1's rule - lives.** `ndjson_keys` reads the ROW's keys and requires each to be classified, so it asks "does - anything read this key?" -- never "does the prose state a fact the row omits?". A fact with no key - is outside the set of things it can have an opinion about. - - Measured, and this is how it was found rather than reasoned: `CrossCheck::disagreements` reached - the prose as a listed entry per disagreement and reached the row as nothing at all. `cross_check` - said `disagree` without saying WHICH counter did, which is the same shape as the defect the - milestone came from. It survived 41 review rounds, a zero-survivor mutation sweep and the fact - accounting, because every one of those instruments starts from what the row publishes. A review - found it by reading the enum and asking who called `code()` -- the answer was nobody. - - So the accounting needs a second enumeration, from the PROSE's facts to the row's keys, or the - rule "a renderer may not tell a reader something the row cannot tell a survey" has no instrument - behind it and holds only as long as someone remembers it. - - **Done, with M3.4, and the second enumeration exists.** The instrument no longer asks "which prose - facts does the oracle read" -- there are none. It asks, for every state `topology::invariant` knows - forbids agreement, whether the row publishes a condition for it; and it holds the row's published - conditions against what the cross-check found, across the corpus. - - (As first written this said the second rule held the row against a count of PROSE lines, which it - did at the time. The follow-up commit that removed the last prose parsing replaced that with the - comparison against the cross-check -- recorded further down this same item, so the item disagreed - with itself. Found by a review.) - - Sabotage-verified against the defect that motivated it: dropping `disagreements` from the row -- - the omission that survived 41 review rounds, a zero-survivor mutation sweep and the old accounting - -- now reddens the rule that holds the row against the cross-check. That rule was named - `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists` when this evidence was recorded - and is `the_row_lists_exactly_the_conditions_the_cross_check_found` now; the sabotage was re-run - against the current name. Recorded evidence that cannot be re-run as written is evidence nobody - will re-run. - - **One asymmetry, found by the instrument rather than reasoned.** Counting all four lists against - prose lines failed: the prose folds every anomaly into ONE - `windows-topology-sys recorded N enumeration anomal...` sentence while the row lists one code per - anomaly, so three anomalies read as two dropped entries. `enumeration_anomalies` counts on its own - axis and is checked against the OBSERVATION -- one published code per anomaly recorded -- which is - the artifact the row owes fidelity to. Checking it against the number inside that sentence would - be the prose-reading this milestone retired. - - Both publication rules carry a corpus guard, because both skip a shape in no blocking state and a - drifted all-healthy corpus would leave them green while checking nothing. - - **The last prose parsing in the matrix is gone.** M3.5 left one site: a rule that filtered - rendered lines by prefix, counted them, and compared that number against the row -- the only place - left where the test matrix obtained structured data by reading sentences. It had a unit-test twin - in `src/tests.rs` that the first sweep missed and a second, wider sweep found. - - Both are replaced by the same claim against `cross_check`: the row's codes must EQUAL the - cross-check's, in order. Strictly stronger -- a count catches only a dropped entry, this catches a - drop, a reorder and a substitution -- and it never reads a sentence. It also covers all three - lists, which the prose count could not: under INCOMPLETE the renderer gives `not_compared` and - `parse_incomplete` the same bare `- ` prefix, so only their total was recoverable from prose. - - **The ordering half was vacuous, and the guard is what found it.** Reversing the row's - `parse_incomplete` order reddened nothing: every corpus shape varied one dimension, so each landed - at most one entry per list, and a one-element list has no order to get wrong. A first version of - the guard summed the three lists and passed while the sabotage still did nothing -- one entry in - each of two lists is two conditions and no order. Corrected to measure the largest SINGLE list, - and a `several conditions at once, in one list` shape added. The reorder now reddens. - - What remains that touches rendered text at all: selecting the row line, and asserting positional - containment -- the banner is the first line, the banner is one line, there is exactly one row. - None reads prose for its content. - - - [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates - the facts a report publishes and measures, by mutation, which are read. The instrument is sound and - the target changes: enumerate the row's fields, and require each to be read by an invariant or - explicitly classified as unread. Its corpus of shapes keeps its purpose -- it exists to defeat the - imagination-driven fixture, which the decision does not change. - - The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on - its own terms rather than against the row. - -- [x] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. - - Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is - to split unless the module is indivisible. It is not -- it carries current decisions and a large - volume of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture the repository - instructions describe. - - Move the rationale to `DESIGN-RATIONALE.md`, cross-referenced by decision anchor, leaving Tier 1 - stating what was decided and what forced it. The decision added by this milestone is written to be - split that way already, so it is the worked example rather than the hard case. - - **Done, and the result is still XL -- say so rather than imply otherwise.** 25,942 bytes moved; - DESIGN-NOTES.md went 92,467 -> 67,807, which is over the 64 KiB threshold still. The split was - made at the one unambiguous Tier 2 fracture rather than trimmed to hit a number. - - The fracture: the correspondence-oracle investigation. It is Tier 2 on both tests -- a record of - how a decision was reached rather than a statement of one, AND a decision since superseded by - [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract). Moving it also - resolved latent drift: it cites `Correspondence`, the fact-accounting instrument and the prose - rules, none of which survived M3.4 and M3.5. As history those sentences are accurate; as Tier 1 - they described deleted code. - - Pure relocation, verified byte-for-byte against the pre-split file (439 lines, identical). The - two moved anchors are kept in Tier 1 beside a pointer, so existing links land somewhere that says - where the content went, and every in-repo reference was repointed at the content. - -> **-> OPEN QUESTION for the engineer:** the remaining bulk of DESIGN-NOTES.md is neither current -> decisions nor rationale -- it is FINDINGS, measurements about Windows that are this crate's actual -> product (the completion-port fork, the thread-agnosticism probe, the x64 comparison, the long-path -> pair, the topology cross-check). They do not belong in a rationale file, and filing them as -> decisions is what keeps Tier 1 XL. Whether they want a tier of their own is a structural choice -> about this component's documentation scheme, so it is raised rather than taken. - -- [x] **M3.7** -- Make four instruments as strong as their names claim. - - A review of the completed M3 found no wrong behaviour and four weak instruments -- tests and - guards whose names assert a property they could not actually fail to satisfy. That is the - recurring defect class of this whole branch, so the four are recorded with what each one was - measured to miss. - - **1. The row's key set was unenforced.** M3.3's note above claimed the check "is derived: - `Row::keys` reads the value, and a test asserts the reader and the writer agree". Both halves - read the same `Row`, so the test says only that the writer is self-consistent. Measured: deleting - `.with("packages", ...)` from the renderer left the ENTIRE suite green -- a field silently - vanishes from every downstream survey and nothing objects. Fixed by declaring - `MEASURED_ROW_KEYS` / `UNMEASURED_ROW_KEYS` as the contract the renderer is held to. This is not - the census M3.4 feared: a count is derivable from the thing it counts, so restating it is drift - waiting to happen; a schema is NOT derivable from the row, which is exactly why writing it down - buys something. - - **2. The well-formedness oracle accepted invalid JSON.** `balanced()` counted bracket depth, so - `{"a":1,}` (trailing separator) and `{"a":1]` (mismatched closer) both passed -- and a consumer - would reject both. Replaced by `malformation()`, a typed delimiter stack that also checks - separator placement. Sabotage-verified: making the writer emit a leading separator produces a - balanced but invalid row, which the old check passed and the new one reddens. - - (**That replacement was itself replaced, by M3.9.** The typed delimiter stack was a second - hand-written opinion about what JSON is, and a generated test found 159 more rows it accepted - and a real parser rejected.) - - **3. A sabotage asserted only that it had sabotaged.** The publication-accounting sabotage stripped - a condition from the report and then asserted the condition was absent -- which is a fact about - the string edit, not about the rule. It would have passed with the rule deleted. The rule is now - `publication_holds(observation, text)`, and the sabotage asserts it REJECTS the stripped report - and ACCEPTS the original. - - **4. A completeness guard compared a table against itself.** `blocking_states` returned strings, - and the guard that checked every blocking state was described derived both sides from that one - table. Introduced a `BlockingState` enum with `ALL`, so the guard holds the table against the - type's variants and a new state that nobody describes fails to build past it. - - (**The last clause was false, and M3.8 corrects it.** `ALL` was a hand-written array; nothing - tied it to the enum.) - -- [x] **M3.8** -- Make `BlockingState::ALL` exhaustive by construction rather than by assertion. - - The same defect as M3.7's fourth finding, one level up, and introduced by the fix for it. The - doc on `ALL` claimed "an exhaustive list the compiler checks: adding a variant without adding it - there fails to build". That is not what the compiler checks. The `match` in `described()` is - exhaustive-checked, which is what made the claim look right -- but it forces a new variant to - acquire an ARM, never an ENTRY in a separate array. - - Measured, not read: a new variant plus the `described()` arm the match demands compiled cleanly - and left all ten invariant tests green, reached by none of them. `ALL` is the list the - completeness guard iterates, so a variant missing from it is a blocking state nothing tests -- - which is the exact failure the guard was added to prevent, reintroduced by the shape of its fix. - - The reverse loop in the guard is not a substitute. It catches a state `blocking_states` produces - and `ALL` omits, but only once some perturbation reaches it -- and a state with no perturbation - entry is precisely what the test exists to catch, so it is circular in the case that matters. - - Fixed by declaring the enum, `ALL` and `described()` from one list through a macro, so a variant - that is not in the list does not exist. The claim is now true rather than deleted. - - Sabotage-verified in both directions: the original sabotage is now inexpressible (there is no - second place to omit the variant from), and its reachable equivalent -- a new state in the list - with no perturbation entry -- reddens `every_blocking_state_has_a_perturbation`, where before - the whole suite stayed green. - - **Three rounds on one guard: strings, then a hand-written `ALL`, then generation.** Each fix - moved the census somewhere harder to see rather than removing it. Worth stating because the - reviewer's finding was not a new defect -- it was the same defect wearing the previous fix. - -- [x] **M3.9** -- Decide the row's well-formedness by a real parse, and delete the hand-written one. - - **The question the oracle asks is "could a consumer read this row", and a consumer uses a JSON - parser.** Anything hand-written here is a second opinion about what JSON is, and a second opinion - is a thing that can disagree -- so `malformation` now calls `serde_json` and the scanner is gone. - - **Measured, and the measurement is why this happened at all.** The hand-written check had already - been through a review, which strengthened it after finding it accepted `{"a":1,}`. A generated - test -- 1807 single-character corruptions of a real row, judged against `serde_json` -- then found - **159 more disagreements, every single one a FALSE ACCEPT**: 129 stray backslashes forming invalid - escapes, 10 missing `:`, 13 `,` where a `:` belonged, 3 the reverse, 3 missing values, 1 string - following a number. The review had found one instance of a class with 160 members. - - Closing the last ~26 required tracking whether an object expects a name or a value next, which is - a JSON parser. So the choice was to write one or to depend on one. - - **The agreement test was deleted in the same commit, deliberately.** With the parse delegated it - would compare `serde_json` against `serde_json` -- green by construction, and exactly the - tautology this milestone keeps deleting. What replaced it asks a question that is still open: not - "is the verdict right" but "does the verdict REACH the caller", which is a property of `check` and - not guaranteed by any parser. It found a real boundary while being written: 8 corruptions destroy - the leading brace, and those are `Missing` rather than `Malformed` -- not a row at all, which for - a survey asking "did this host report a row" is the right answer and a different one. Both - branches are asserted. - - **Three tests stopped asserting the defect's wording.** The message is `serde_json`'s now, so - this crate does not own it; pinning it would let a dependency's patch release redden tests about - unclosed delimiters, a false finding about this crate. They assert rejection and the carried row. - - **A parse does NOT subsume `RepeatedKey`,** which is why that check stays hand-written: - `serde_json` accepts a duplicated key and silently keeps the last, which is precisely the - malformation that survives a consumer's parse and changes what it reads. - - `report_oracle` is now gated `cfg(any(test, feature = "oracle-in-renderer"))` -- every caller - already was -- which is what keeps the parser out of a shipping probe. Verified by inspecting the - binaries, per the precedent in that feature's own comment: the default `probe-topology.exe` - contains no `serde_json`, no oracle panic string, and no parser message; the `--all-features` one - contains all three. - -- [x] **M3.10** -- Delete the last hand-written string scanners in the oracle. - - M3.9 removed one of six; this removes the rest. **The module hand-writes no string walking at - all now** -- every byte-level decision about quotes, escapes and delimiters comes from - `serde_json`. - - **Two of the four were provably unsafe, on an argument enforced by nothing.** `list_codes` found - `"code":"` and took the next `"` as the end, and `list_span_end` counted brackets with no notion - of being inside a string. Both were safe only because every code is a `&'static str` from an enum - and no caller text reaches a diagnostic list -- true, load-bearing, and guarded by no test. - `keys` had already proved the class reachable: it made `assert_corresponds` panic from inside - `report_unmeasured` on a quoted `discovery_error`. Parsing makes the argument unnecessary rather - than merely correct, which is the difference between a property and a hope. - - **`keys` needed a visitor rather than a parsed map, and the reason is a contract.** It must return - the row's names in ORDER and WITH DUPLICATES. `serde_json::Map` sorts, and silently keeps the last - of a repeated key -- which would delete the evidence for `RowDefect::RepeatedKey`, the one - malformation that survives a consumer's parse. A `MapAccess` visitor reads each name as the parser - reads it, so both properties survive while every scanning decision stays `serde_json`'s. That - reasoning is now a sabotage entry rather than a comment: replacing the visitor with the obvious - `Map` one-liner is `caught`. - - **`list_span_end` was deleted, not moved.** Its only caller was a sabotage doing text surgery on a - list. That sabotage now parses, empties the lists and re-renders -- its third implementation, after - one that split on commas (which sliced entries in half once they became objects) and one that used - this helper. A sabotage that hand-parses is a sabotage that can quietly stop sabotaging, and it - leaves the rule it guards unguarded while still passing. - - **The stale sabotage entry is itself the evidence.** After the change the sweep reported - `MANIFEST STALE: pattern found 0 times` for the escape-awareness entry -- the defect it injected - can no longer be expressed, because the code that could hold it is gone. Replaced with the - parsed-map entry above; 7 of 7 behave as declared. - - Default build re-verified by binary inspection: neither `serde` nor `serde_json` appears on a - normal dependency edge, and `probe-topology.exe` contains no parser string. - -## M4 -- Carried over from M2: the items M3 gates - -These were written under M2 and are blocked on M3 above: each one targets the prose-against-row -machinery that M3 retires or relocates, so doing them first means doing them twice. M3's preamble -says which M3 item gates each of them. + `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the correspondence M3.4 retired, + so they can no longer occur and a re-run now looks clean. The underlying point stands without + them: CI builds `aarch64` and never tests it, and architecture is the one shape dimension a corpus + cannot vary because it is fixed at compile time. Restate it on that basis when picked up. +- **M2.17** was re-scoped by M3.5: the dimensions worth crossing are the row's, and crossing prose + shapes that have since stopped being checked would have aimed at the retiring half. **The IDs keep their M2 numbers deliberately.** [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md) is append-only and its entries are immutable, and two archived entries already cite M2.4 and M2.14 -- @@ -616,8 +123,9 @@ correctness in the archive. ## M5 -- Carried over from M2: unblocked hygiene **Nothing gates these.** They are grouped last by priority, not by dependency -- none of them touches -the report pipeline, so any of them may be pulled forward ahead of M3 or M4 at any time. They were -discovered during M2 and parked there under a heading none of them fit. +the report pipeline, so any of them may be pulled forward ahead of M4 at any time. They were +discovered during M2 and parked there under a heading none of them fit. M3, which gated M4 but never +gated these, is complete and archived. IDs keep their M2 numbers, for the reason given under M4. @@ -743,13 +251,13 @@ IDs keep their M2 numbers, for the reason given under M4. `tools/run-sabotage.ps1` exists, has its own tests, and runs in CI; [windows-placement-probe](../windows-placement-probe/sabotage.json) (9 entries) and [windows-waitable-queues](../windows-waitable-queues/sabotage.json) (39 entries) each carry a - `sabotage.json`. **This crate carries none**, so every sabotage run while building M3 was ad-hoc + `sabotage.json`. **This crate carried none**, so every sabotage run while building M3 was ad-hoc PowerShell, discarded on the spot -- which is why a `git checkout` destroyed uncommitted work twice and a `.Replace` pattern silently matched two sites once. The manifest format's `find` must - match EXACTLY ONCE, which is precisely the guard that hand-running lacks. + match EXACTLY ONCE, which is precisely the guard that hand-running lacked. - Eight commits on this branch record their sabotages in the message, so the first pass is - transcription rather than invention: the defect, the file and the test expected to redden are + Eight commits on this branch recorded their sabotages in the message, so the first pass was + transcription rather than invention: the defect, the file and the test expected to redden were already written down. Include at least one `expect: "survives"` control. A manifest of nothing but `caught` cannot diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index f3c4c5b8c..f59d0ff35 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -523,3 +523,507 @@ number and its milestone heading; renumbering would cost correctness in the arch *(Recorded 2026-09-12 18:45:27 -04:00. This entry closes a milestone rather than completing an item, so it carries no `###` item heading and nothing links to it by anchor.)* + +## Moved 2026-09-12 -- M3: the encoded row became the contract, and the prose stopped being checked + + +Decided in [DESIGN-NOTES.md](DESIGN-NOTES.md) -> [The encoded row is the contract; the prose is +not](DESIGN-NOTES.md#d-encoded-row-is-the-contract), from the session in +[design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). + +The row is a machine contract mined across a fleet; the prose is for a reader. They carry different +obligations -- the row must be **correct**, enforced by machine; the prose must be **accurate and +readable**, enforced by review. Nothing is required to hold *between* them. + +Re-checked against the code rather than against M2's account of it: the original defect was fixed, +and what it left behind was larger. The row published `not_compared`, `parse_incomplete` and +`enumeration_anomalies` as **counts**, where the prose printed each entry's text. A survey reading +`"parse_incomplete":1` could not tell *the probe detected a bug in itself* from *a core record +contradicted itself* from *this topology was not measured from a running machine*. **The row was +impoverished relative to the prose** -- the artifact that gets mined carried less than the artifact +that gets read. M3.1 has since closed that particular gap; the rest of the milestone is about which +artifact carries the contract, and stands whole. + +**M2 completed with this decision**, and its ten open items were re-sequenced rather than reworked. +The milestone's own work -- the oracle, the binding, the real-host test, the derived fact set, the +partitioning discriminator and the shape corpus -- is done and archived. The leftovers had +accumulated under a heading none of them fit, and they split by whether M3 gates them: four are in +M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. + +- **M2.18 (typed banner) is dissolved into M3.3**, not carried over. It was the smallest instance + of "should a report be a value a writer renders, or a string the renderer concatenates", and + answering it alone would have typed one parameter while leaving the shape everywhere else. +- **The per-item re-scoping notes for M2.4, M2.5, M2.15 and M2.17 now live in M4's preamble**, not + here. They were written during this milestone but they instruct work that is still open, and a + pending instruction does not belong in an append-only archive nobody may edit to correct it. +- **M2.7, M2.8, M2.9, M2.13, M2.14 and M2.16 are gated by nothing** and are in M5. M2.9 (a + cross-host ratio called "the finding") and M2.14 (making two authoring rules bite) are if anything + reinforced: + under this decision prose accuracy is a review obligation rather than a machine-checked one, which + puts more weight on both. + +- [x] **M3.1** -- Publish each diagnostic as itself, not as a count. + + `not_compared`, `parse_incomplete` and `enumeration_anomalies` reach the row as + `check.parse_incomplete.len()` and its two siblings, so the fact that a mining pass most needs -- + *which* condition occurred -- exists only in prose. Publish the entries, and give each a stable + machine-readable discriminant rather than the human sentence, so a survey can group by condition + without matching on English that is free to be reworded. The sentences stay in the prose, where + rewording them is harmless. + + **The rule this establishes, which is the durable half:** a renderer may not tell a reader + something the row cannot tell a survey. A cardinality is not a statement of the fact. + + **Done.** Three enums in `topology::diagnostic` -- 21 + 6 + 3 variants, one per condition -- each + carrying its data, rendering its sentence through `Display`, and naming itself through `code()`. + `CrossCheck`'s three `Vec` became `Vec`, and the row publishes arrays of codes + where it published `.len()`. The prose is byte-identical: the loops write `{entry}` and `Display` + emits the same sentences. + + **The wire format changed**, deliberately and not additively: `"parse_incomplete":1` is now + `"parse_incomplete":["partitioning_summary_missing"]`. The count is still available as the list's + length, so nothing is lost, and publishing both would be a restatement that can drift. Same shape + as the `efficiency_classes` correction that preceded it. + + Sabotage-verified, each mutation injected on its own line and reverted: renaming + `PartitioningSummaryMissing`'s code reddens only + `the_row_names_the_probes_own_bug_when_it_detects_one`; making the row keep only the first + condition reddens the two list tests, through the bound oracle's count rule; mislabelling + `TrailingBytes` reddens only `an_anomaly_reaches_the_row_as_its_kind`. + + **The substring-to-variant conversion cost two assertions their discrimination, found by review.** + `c.contains("no online processors and processor groups")` became + `matches!(c, MeasuredButCountsAbsent { .. })`, which holds when the entry names only ONE of the + two -- exactly what the test forbids -- and the loop's labels stopped being asserted at all. + Measured: with `absent` truncated to its first entry the whole suite stayed green at 249 passed. + Both now assert the variant's `absent` payload, and both were observed to fail -- the truncation + reddens the both-absent test, and swapping the two names reddens both. The general lesson is that + converting an assertion from a substring to a variant DROPS whatever the substring discriminated + inside the payload; the variant is the weaker claim unless the payload comes with it. + + **Which conditions are listed is deliberately not compared against the prose.** The code and the + sentence come from one variant, so there is no second implementation to disagree through -- the + correspondence holds by construction, which is stronger than a check. What remains checkable, and + is checked, is that both renderings list the same NUMBER. Found while converting the accounting + instrument: a mutation that swapped one code for another went unnoticed on the + `verdict incomplete` shape, because the oracle reads the length. `corruptions` now APPENDS a code + rather than substituting one, so the length always differs. + +- [x] **M3.2** -- Assert the surviving correspondences as invariants on the observation, before + rendering. + + Alarm-against-verdict, diagnostics-against-verdict and counters-against-verdict are the three + oracle rules that survive the decision. They stop being comparisons of two rendered texts and + become predicates over `Observation` and `CrossCheck` -- `SummaryMissing` implies the verdict is not + `agree`, a non-empty `parse_incomplete` implies the verdict is not `agree`, and so on. No parser is + involved, and the check runs whether or not anything was rendered. + + Each one must be sabotage-verified on arrival: delete the invariant, confirm the suite reddens, + restore it. A predicate that cannot fail is the failure mode this crate keeps meeting. + + **Done, and the item's own framing was wrong in a way worth recording.** It named + "diagnostics-against-verdict" and "counters-against-verdict" as rules to move. Two of those read + `CrossCheck`'s lists -- and `verdict` is a pure function of those lists, so such a rule restates + the definition, cannot fail for any input, and CANNOT CATCH A DELETED PUSH SITE: the deletion + empties the list, the rule sees nothing, and the verdict is `agree` legitimately. Written that + way first, with three tests that asserted acceptance under violation-sounding names. + + Every rule now reads the OBSERVATION. `blocking_states` names ten states that forbid an agreeing + verdict, each with a push site in `cross_check` that it does not consult, plus the two counter + rules. `check` takes the verdict rather than deriving it, so a test can supply the answer a + broken `cross_check` would give -- otherwise every branch is reachable only by editing the source + and a green run says nothing. + + Bound at `observe` (every observation MEASURED, rendered or not -- what this item asked for) and + at `report` (every observation RENDERED, which on the test side is most of them, since the suite + builds observations by hand). Not in `cross_check`, which would recurse. + + Sabotage: deleting the `PartitioningSummaryMissing` push reddens four tests, two of them new -- + the invariant's own accounting test, and a render test through `assert_holds` at the renderer + binding. The invariant is not the sole detector for that push site; its value is the nine others, + several of which have no dedicated test. + +- [x] **M3.3** -- Emit the row from a typed value through one writer. + + > **-> PREREQUISITE: M3.4 lands first.** The reason is on M3.4: this item's nested per-entry data + > makes `ndjson_list_len` silently miscount, so the parsers it would break should be gone before + > the row changes shape rather than taught a shape they are about to lose. + + The row is built today by interpolating every value positionally into a `concat!` template. + Two defect classes follow from that construction and both are closed by replacing it, not by + checking it: + + **Injection.** Measured on PR #88: an `io::Error` containing `{` was selected as the report's + machine-readable row, so the oracle checked the caller's text instead of the probe's. Caller text + reaching the mined artifact is contamination of the contract. + + **Field order and labelling.** A field's name and its value are related only by counting + positions, so a reordered argument or a miscounted placeholder yields mislabelled data that + still parses, which nothing downstream can detect. Stated as the coupling rather than as a + count of placeholders: that count was written twice and wrong twice within an hour. + + A typed row struct plus a single writer that escapes strings makes both unrepresentable. Write the + writer here rather than adding a serialization dependency -- this crate has none and the row is + one flat object. + + **Carry each diagnostic's DATA, which M3.1 left behind.** M3.1 publishes a condition's code but + not the values its variant holds -- a survey learns `contradictory_cores` without learning that + three cores contradicted themselves. The variants already carry those values, for `Display`; what + stopped M3.1 publishing them is that the row is still a positional `concat!` template, where a + nested per-entry object has to be hand-assembled. Once the row is typed this is a field like any + other. Not deferred for want of a consumer -- the shape of the row is the blocker, and it is this + item. This subsumes M2.18: the banner becomes a typed field like any other, and the + question of who may construct one is answered by the row's constructor rather than separately. + + **Done.** `crate::row` holds a `Value` and a `Row` whose members are name-and-value pairs, with + one writer that escapes strings. Both defect classes are now unrepresentable rather than + detected: a name and its value move together or not at all, and a `Value::Text` cannot end the + string it is in. + + Each diagnostic publishes its DATA through `published()`, so a survey learns + `{"code":"contradictory_cores","count":3}` rather than the code alone -- what M3.1 had to leave + behind because the row was a positional template. Anomalies carry `source` and `offset` too: + the same kind at the same offset across a fleet is a different finding from the same kind + scattered, and neither is visible from a count. + + `report_unmeasured` goes through the same writer, and that is the shape that most needed it -- + it is the only renderer that interpolates caller text, a failed discovery's `io::Error`. The + error now reaches the row as a `discovery_error` field, so a survey can group failures by cause + instead of parsing the prose sentence. + + The key-set check M3.4 deferred here now exists -- but NOT in the form M3.4 predicted, and the + first attempt at it was vacuous. See the correction recorded under M3.7. + + **Two silent behaviour changes were caught by checking the old code rather than trusting the + rewrite.** `PartitioningCache` has FIVE variants, not the four a rewrite naturally reaches for; + and `SummaryMissing` publishes its level rather than `null` -- which matters precisely because + that arm is the report telling a reader the probe has a bug, and WHICH level went unchecked is + what they need. + + Sabotage-verified: removing the quote escape reddens three row tests, including the + brace-injection one. The clean row is byte-identical to what the template produced, confirmed + against a real `probe-topology` run. + +- [x] **M3.4** -- Retire the prose-against-row correspondences and the parsers that serve only them. + + > **-> DO THIS BEFORE M3.3, and leave both IDs where they are.** M3.3 carries each diagnostic's + > data, which turns the flat code arrays into arrays of OBJECTS -- and `ndjson_list_len` splits on + > `,`, documented as safe for flat code arrays and nothing else. Pointed at + > `[{"code":"contradictory_cores","cores":3}]` it counts members rather than entries and returns 2 + > for one entry. It does not fail; it silently answers wrong, and every prose-comparison rule then + > compares that against the prose. Running M3.3 first therefore means teaching parsers a nested + > shape and deleting them one item later, with a silent-wrong-answer window in between. The IDs + > stay put because renumbering costs more than the mismatch, the same trade as M4/M5. + > + > Intended order for the rest of M3: **M3.2 -> M3.4 -> M3.3 -> M3.5**. + + > **-> CODE REVIEW RESUMES HERE.** Reviews are paused by the engineer's decision of 2026-09-12 + > until this crate no longer depends on prose as the oracle's subject, and this is the item that + > ends that dependence. The reasoning: a large share of PR #88's fifteen fix commits were defects + > in the prose-reading machinery -- the multibyte panic in `processors_in_banner`, `trim_matches` + > collapsing `[[0]]` and `[0]`, `prose_field` selecting the wrong line -- and every one of them is + > code this item deletes. Reviewing it closely is polishing something already scheduled for + > demolition. + > + > Recorded with the honest counterweight, so the decision can be re-judged on evidence rather than + > re-argued: of the six findings across the two reviews run on 2026-09-12, none was a defect in + > the prose oracle. Two were documentation drift, one was a test that had lost its + > discrimination, and the most valuable -- `disagreements` reaching the prose and not the row at + > all -- was about the ROW being incomplete and survives this item untouched. + + Of 38 top-level functions in [src/report_oracle.rs](src/report_oracle.rs), ten are correspondence + rules, four are comparison helpers, and **twenty-three exist only to extract values back out of + rendered text**. With M3.2 and M3.3 landed, that extraction layer has no remaining consumer. + + What stays is a thin check that the row is **well-formed** -- it parses, it carries the expected + key set, and it is the only such line in the report. That is not a correspondence; it is the + writer's own output being checked, and the writer is the one place structure cannot check itself. + + Retire, do not merely stop calling. Dead extraction helpers left in place are a second grammar for + a format that no longer has two readers. + + **Done, together with M3.5, because they cannot be separated.** The fact-accounting instrument is + built entirely on `report_oracle::check` and the `Correspondence` variants, so deleting the + correspondences leaves it measuring nothing and the suite red between the two items. Committed as + one commit citing both IDs, per the checklist rule for coupled items, rather than split into a + commit that does not pass. + + Measured: `report_oracle.rs` 79,394 -> 8,874 bytes, its tests 105,299 -> 5,598, the integration + instrument 71,007 -> 25,689. All eight prose correspondences and all twenty-three extraction + helpers are gone. + + What survives is the row's well-formedness: exactly one machine-readable line, brackets balanced + (string-aware, because a failed discovery's `io::Error` is interpolated into a string value and an + OS message is free to contain a bracket), and no repeated top-level key. That last one is the + malformation that survives a consumer's parse and changes what it reads, since most JSON readers + take the last. + + (The bracket check was weaker than this sentence implies -- it counted depth, so a trailing or + misplaced separator passed. Strengthened in M3.7.) + + **The key-set check is deliberately NOT here.** Asserting it needs a list of expected keys, and a + list written here is a census -- this component re-corrected the same census three times in one + day. M3.3 makes the row a typed value, at which point the key set is derivable from the type + rather than declared beside it. Moved there rather than approximated here. + + (**The second sentence is wrong, and M3.7 corrects it.** A key set is NOT derivable from a typed + row: the type says "a row is a map of names to values", which is satisfied by every key set, + including the one missing a field. The census this note was right to fear is a count; a schema is + not one, and refusing to write it down bought nothing.) + +- [x] **M3.5** -- Re-aim the shape corpus and the fact accounting at the row. + + **The instrument enumerates in one direction only, and the other direction is where M3.1's rule + lives.** `ndjson_keys` reads the ROW's keys and requires each to be classified, so it asks "does + anything read this key?" -- never "does the prose state a fact the row omits?". A fact with no key + is outside the set of things it can have an opinion about. + + Measured, and this is how it was found rather than reasoned: `CrossCheck::disagreements` reached + the prose as a listed entry per disagreement and reached the row as nothing at all. `cross_check` + said `disagree` without saying WHICH counter did, which is the same shape as the defect the + milestone came from. It survived 41 review rounds, a zero-survivor mutation sweep and the fact + accounting, because every one of those instruments starts from what the row publishes. A review + found it by reading the enum and asking who called `code()` -- the answer was nobody. + + So the accounting needs a second enumeration, from the PROSE's facts to the row's keys, or the + rule "a renderer may not tell a reader something the row cannot tell a survey" has no instrument + behind it and holds only as long as someone remembers it. + + **Done, with M3.4, and the second enumeration exists.** The instrument no longer asks "which prose + facts does the oracle read" -- there are none. It asks, for every state `topology::invariant` knows + forbids agreement, whether the row publishes a condition for it; and it holds the row's published + conditions against what the cross-check found, across the corpus. + + (As first written this said the second rule held the row against a count of PROSE lines, which it + did at the time. The follow-up commit that removed the last prose parsing replaced that with the + comparison against the cross-check -- recorded further down this same item, so the item disagreed + with itself. Found by a review.) + + Sabotage-verified against the defect that motivated it: dropping `disagreements` from the row -- + the omission that survived 41 review rounds, a zero-survivor mutation sweep and the old accounting + -- now reddens the rule that holds the row against the cross-check. That rule was named + `the_row_lists_a_condition_for_every_diagnostic_the_prose_lists` when this evidence was recorded + and is `the_row_lists_exactly_the_conditions_the_cross_check_found` now; the sabotage was re-run + against the current name. Recorded evidence that cannot be re-run as written is evidence nobody + will re-run. + + **One asymmetry, found by the instrument rather than reasoned.** Counting all four lists against + prose lines failed: the prose folds every anomaly into ONE + `windows-topology-sys recorded N enumeration anomal...` sentence while the row lists one code per + anomaly, so three anomalies read as two dropped entries. `enumeration_anomalies` counts on its own + axis and is checked against the OBSERVATION -- one published code per anomaly recorded -- which is + the artifact the row owes fidelity to. Checking it against the number inside that sentence would + be the prose-reading this milestone retired. + + Both publication rules carry a corpus guard, because both skip a shape in no blocking state and a + drifted all-healthy corpus would leave them green while checking nothing. + + **The last prose parsing in the matrix is gone.** M3.5 left one site: a rule that filtered + rendered lines by prefix, counted them, and compared that number against the row -- the only place + left where the test matrix obtained structured data by reading sentences. It had a unit-test twin + in `src/tests.rs` that the first sweep missed and a second, wider sweep found. + + Both are replaced by the same claim against `cross_check`: the row's codes must EQUAL the + cross-check's, in order. Strictly stronger -- a count catches only a dropped entry, this catches a + drop, a reorder and a substitution -- and it never reads a sentence. It also covers all three + lists, which the prose count could not: under INCOMPLETE the renderer gives `not_compared` and + `parse_incomplete` the same bare `- ` prefix, so only their total was recoverable from prose. + + **The ordering half was vacuous, and the guard is what found it.** Reversing the row's + `parse_incomplete` order reddened nothing: every corpus shape varied one dimension, so each landed + at most one entry per list, and a one-element list has no order to get wrong. A first version of + the guard summed the three lists and passed while the sabotage still did nothing -- one entry in + each of two lists is two conditions and no order. Corrected to measure the largest SINGLE list, + and a `several conditions at once, in one list` shape added. The reorder now reddens. + + What remains that touches rendered text at all: selecting the row line, and asserting positional + containment -- the banner is the first line, the banner is one line, there is exactly one row. + None reads prose for its content. + + + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) enumerates + the facts a report publishes and measures, by mutation, which are read. The instrument is sound and + the target changes: enumerate the row's fields, and require each to be read by an invariant or + explicitly classified as unread. Its corpus of shapes keeps its purpose -- it exists to defeat the + imagination-driven fixture, which the decision does not change. + + The prose half becomes a rendering test: the renderer emits what it is supposed to emit, judged on + its own terms rather than against the row. + +- [x] **M3.6** -- Split [DESIGN-NOTES.md](DESIGN-NOTES.md) into Tier 1 and Tier 2. + + Measured: 88 KiB, which is **XL** on the repository's byte scale, and the default posture at XL is + to split unless the module is indivisible. It is not -- it carries current decisions and a large + volume of how-we-got-here reasoning, which is exactly the Tier 1 / Tier 2 fracture the repository + instructions describe. + + Move the rationale to `DESIGN-RATIONALE.md`, cross-referenced by decision anchor, leaving Tier 1 + stating what was decided and what forced it. The decision added by this milestone is written to be + split that way already, so it is the worked example rather than the hard case. + + **Done, and the result is still XL -- say so rather than imply otherwise.** 25,942 bytes moved; + DESIGN-NOTES.md went 92,467 -> 67,807, which is over the 64 KiB threshold still. The split was + made at the one unambiguous Tier 2 fracture rather than trimmed to hit a number. + + The fracture: the correspondence-oracle investigation. It is Tier 2 on both tests -- a record of + how a decision was reached rather than a statement of one, AND a decision since superseded by + [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract). Moving it also + resolved latent drift: it cites `Correspondence`, the fact-accounting instrument and the prose + rules, none of which survived M3.4 and M3.5. As history those sentences are accurate; as Tier 1 + they described deleted code. + + Pure relocation, verified byte-for-byte against the pre-split file (439 lines, identical). The + two moved anchors are kept in Tier 1 beside a pointer, so existing links land somewhere that says + where the content went, and every in-repo reference was repointed at the content. + +> **-> OPEN QUESTION for the engineer:** the remaining bulk of DESIGN-NOTES.md is neither current +> decisions nor rationale -- it is FINDINGS, measurements about Windows that are this crate's actual +> product (the completion-port fork, the thread-agnosticism probe, the x64 comparison, the long-path +> pair, the topology cross-check). They do not belong in a rationale file, and filing them as +> decisions is what keeps Tier 1 XL. Whether they want a tier of their own is a structural choice +> about this component's documentation scheme, so it is raised rather than taken. + +- [x] **M3.7** -- Make four instruments as strong as their names claim. + + A review of the completed M3 found no wrong behaviour and four weak instruments -- tests and + guards whose names assert a property they could not actually fail to satisfy. That is the + recurring defect class of this whole branch, so the four are recorded with what each one was + measured to miss. + + **1. The row's key set was unenforced.** M3.3's note above claimed the check "is derived: + `Row::keys` reads the value, and a test asserts the reader and the writer agree". Both halves + read the same `Row`, so the test says only that the writer is self-consistent. Measured: deleting + `.with("packages", ...)` from the renderer left the ENTIRE suite green -- a field silently + vanishes from every downstream survey and nothing objects. Fixed by declaring + `MEASURED_ROW_KEYS` / `UNMEASURED_ROW_KEYS` as the contract the renderer is held to. This is not + the census M3.4 feared: a count is derivable from the thing it counts, so restating it is drift + waiting to happen; a schema is NOT derivable from the row, which is exactly why writing it down + buys something. + + **2. The well-formedness oracle accepted invalid JSON.** `balanced()` counted bracket depth, so + `{"a":1,}` (trailing separator) and `{"a":1]` (mismatched closer) both passed -- and a consumer + would reject both. Replaced by `malformation()`, a typed delimiter stack that also checks + separator placement. Sabotage-verified: making the writer emit a leading separator produces a + balanced but invalid row, which the old check passed and the new one reddens. + + (**That replacement was itself replaced, by M3.9.** The typed delimiter stack was a second + hand-written opinion about what JSON is, and a generated test found 159 more rows it accepted + and a real parser rejected.) + + **3. A sabotage asserted only that it had sabotaged.** The publication-accounting sabotage stripped + a condition from the report and then asserted the condition was absent -- which is a fact about + the string edit, not about the rule. It would have passed with the rule deleted. The rule is now + `publication_holds(observation, text)`, and the sabotage asserts it REJECTS the stripped report + and ACCEPTS the original. + + **4. A completeness guard compared a table against itself.** `blocking_states` returned strings, + and the guard that checked every blocking state was described derived both sides from that one + table. Introduced a `BlockingState` enum with `ALL`, so the guard holds the table against the + type's variants and a new state that nobody describes fails to build past it. + + (**The last clause was false, and M3.8 corrects it.** `ALL` was a hand-written array; nothing + tied it to the enum.) + +- [x] **M3.8** -- Make `BlockingState::ALL` exhaustive by construction rather than by assertion. + + The same defect as M3.7's fourth finding, one level up, and introduced by the fix for it. The + doc on `ALL` claimed "an exhaustive list the compiler checks: adding a variant without adding it + there fails to build". That is not what the compiler checks. The `match` in `described()` is + exhaustive-checked, which is what made the claim look right -- but it forces a new variant to + acquire an ARM, never an ENTRY in a separate array. + + Measured, not read: a new variant plus the `described()` arm the match demands compiled cleanly + and left all ten invariant tests green, reached by none of them. `ALL` is the list the + completeness guard iterates, so a variant missing from it is a blocking state nothing tests -- + which is the exact failure the guard was added to prevent, reintroduced by the shape of its fix. + + The reverse loop in the guard is not a substitute. It catches a state `blocking_states` produces + and `ALL` omits, but only once some perturbation reaches it -- and a state with no perturbation + entry is precisely what the test exists to catch, so it is circular in the case that matters. + + Fixed by declaring the enum, `ALL` and `described()` from one list through a macro, so a variant + that is not in the list does not exist. The claim is now true rather than deleted. + + Sabotage-verified in both directions: the original sabotage is now inexpressible (there is no + second place to omit the variant from), and its reachable equivalent -- a new state in the list + with no perturbation entry -- reddens `every_blocking_state_has_a_perturbation`, where before + the whole suite stayed green. + + **Three rounds on one guard: strings, then a hand-written `ALL`, then generation.** Each fix + moved the census somewhere harder to see rather than removing it. Worth stating because the + reviewer's finding was not a new defect -- it was the same defect wearing the previous fix. + +- [x] **M3.9** -- Decide the row's well-formedness by a real parse, and delete the hand-written one. + + **The question the oracle asks is "could a consumer read this row", and a consumer uses a JSON + parser.** Anything hand-written here is a second opinion about what JSON is, and a second opinion + is a thing that can disagree -- so `malformation` now calls `serde_json` and the scanner is gone. + + **Measured, and the measurement is why this happened at all.** The hand-written check had already + been through a review, which strengthened it after finding it accepted `{"a":1,}`. A generated + test -- 1807 single-character corruptions of a real row, judged against `serde_json` -- then found + **159 more disagreements, every single one a FALSE ACCEPT**: 129 stray backslashes forming invalid + escapes, 10 missing `:`, 13 `,` where a `:` belonged, 3 the reverse, 3 missing values, 1 string + following a number. The review had found one instance of a class with 160 members. + + Closing the last ~26 required tracking whether an object expects a name or a value next, which is + a JSON parser. So the choice was to write one or to depend on one. + + **The agreement test was deleted in the same commit, deliberately.** With the parse delegated it + would compare `serde_json` against `serde_json` -- green by construction, and exactly the + tautology this milestone keeps deleting. What replaced it asks a question that is still open: not + "is the verdict right" but "does the verdict REACH the caller", which is a property of `check` and + not guaranteed by any parser. It found a real boundary while being written: 8 corruptions destroy + the leading brace, and those are `Missing` rather than `Malformed` -- not a row at all, which for + a survey asking "did this host report a row" is the right answer and a different one. Both + branches are asserted. + + **Three tests stopped asserting the defect's wording.** The message is `serde_json`'s now, so + this crate does not own it; pinning it would let a dependency's patch release redden tests about + unclosed delimiters, a false finding about this crate. They assert rejection and the carried row. + + **A parse does NOT subsume `RepeatedKey`,** which is why that check stays hand-written: + `serde_json` accepts a duplicated key and silently keeps the last, which is precisely the + malformation that survives a consumer's parse and changes what it reads. + + `report_oracle` is now gated `cfg(any(test, feature = "oracle-in-renderer"))` -- every caller + already was -- which is what keeps the parser out of a shipping probe. Verified by inspecting the + binaries, per the precedent in that feature's own comment: the default `probe-topology.exe` + contains no `serde_json`, no oracle panic string, and no parser message; the `--all-features` one + contains all three. + +- [x] **M3.10** -- Delete the last hand-written string scanners in the oracle. + + M3.9 removed one of six; this removes the rest. **The module hand-writes no string walking at + all now** -- every byte-level decision about quotes, escapes and delimiters comes from + `serde_json`. + + **Two of the four were provably unsafe, on an argument enforced by nothing.** `list_codes` found + `"code":"` and took the next `"` as the end, and `list_span_end` counted brackets with no notion + of being inside a string. Both were safe only because every code is a `&'static str` from an enum + and no caller text reaches a diagnostic list -- true, load-bearing, and guarded by no test. + `keys` had already proved the class reachable: it made `assert_corresponds` panic from inside + `report_unmeasured` on a quoted `discovery_error`. Parsing makes the argument unnecessary rather + than merely correct, which is the difference between a property and a hope. + + **`keys` needed a visitor rather than a parsed map, and the reason is a contract.** It must return + the row's names in ORDER and WITH DUPLICATES. `serde_json::Map` sorts, and silently keeps the last + of a repeated key -- which would delete the evidence for `RowDefect::RepeatedKey`, the one + malformation that survives a consumer's parse. A `MapAccess` visitor reads each name as the parser + reads it, so both properties survive while every scanning decision stays `serde_json`'s. That + reasoning is now a sabotage entry rather than a comment: replacing the visitor with the obvious + `Map` one-liner is `caught`. + + **`list_span_end` was deleted, not moved.** Its only caller was a sabotage doing text surgery on a + list. That sabotage now parses, empties the lists and re-renders -- its third implementation, after + one that split on commas (which sliced entries in half once they became objects) and one that used + this helper. A sabotage that hand-parses is a sabotage that can quietly stop sabotaging, and it + leaves the rule it guards unguarded while still passing. + + **The stale sabotage entry is itself the evidence.** After the change the sweep reported + `MANIFEST STALE: pattern found 0 times` for the escape-awareness entry -- the defect it injected + can no longer be expressed, because the code that could hold it is gone. Replaced with the + parsed-map entry above; 7 of 7 behave as declared. + + Default build re-verified by binary inspection: neither `serde` nor `serde_json` appears on a + normal dependency edge, and `probe-topology.exe` contains no parser string. diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index 67bef6163..af947af03 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. M3.1 has landed, so those three fields now publish arrays of condition codes. M4 holds the four carried-over items M3 gates; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#d-correspondence-failures) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. **Complete and archived, ten items.** The three fields publish arrays of condition codes; the surviving correspondences became invariants over the observation rather than over two renderings; the row is emitted from a typed value through one writer with total escaping; the prose oracle and its parsers are gone, and no test extracts structured data from prose anywhere in the crate. The last four items came from reviews: three instruments asserted less than their names claimed, `BlockingState::ALL` was a census the compiler did not check while a doc comment said it did, and the hand-written JSON check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a false ACCEPT -- then replaced by `serde_json`, after which the remaining hand-written string scanners were deleted. M4 holds the four carried-over items M3 gated, now unblocked and re-scoped; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#d-correspondence-failures) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | From d20556a9c589064777e2b1403b421f7db7c7c06a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 12 Sep 2026 23:44:03 -0700 Subject: [PATCH 23/46] docs: give repository timestamps their offset Two of the dated formats already required one -- `## Resolved <... +-hh:mm>` and the completed-item stamp -- while `## Moved` did not. Worth noting that the correct form was present and then drifted: the first heading in this workspace's COMPLETED-CHECKLIST.md is a full `## Moved 2026-09-09 19:00:17 -04:00`, and the four entries after it are bare dates. Framed as a presentation concern rather than a correctness one, because that is what it is. A bare local date is not wrong -- the event happened when it happened -- it is just ambiguous in a way a few characters remove. An offset does not stop a machine with a misconfigured zone from writing a misleading date; it makes that date convertible afterwards, which is the whole of the benefit. The worked example is this session. A laptop reporting -04:00 while its owner sat in -07:00 ran three hours fast, so timestamps taken late in the evening carried the next day's date. Every affected commit stayed unambiguous because git stores the instant with its offset, so 2026-09-13T01:14:28-04:00 is exactly 2026-09-12 22:14 -07:00. One bare date in a checklist was the only thing that had to be re-derived from elsewhere, and it is corrected here. The rule explicitly does NOT ask anyone to go back and repair bare dates already written, or to rewrite history to relabel an offset. Instants are correct either way, and rewriting changes every hash -- on this branch two tracked files cite commit hashes, one of them a split's mandatory `Split from ... at ` provenance line, and both would dangle. An archive heading upgraded in the course of writing this was reverted for the same reason: COMPLETED-CHECKLIST.md is append-only, and a heading that is merely imprecise is not grounds to edit it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 33 ++++++++++++++++++++- crates/windows-platform-probes/CHECKLIST.md | 2 +- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5be0df5a3..6bc6c13fd 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1456,13 +1456,44 @@ step breakdowns described during planning. When a group of related items is fully complete: 1. Move the completed group to `COMPLETED-CHECKLIST.md` in the same directory. -2. Prefix the moved block with a heading: `## Moved YYYY-MM-DD — `. +2. Prefix the moved block with a heading: + `## Moved YYYY-MM-DD HH:MM:SS ±hh:mm — `, taken from + `Get-Date -Format "yyyy-MM-dd HH:mm:ss zzz"`. Carry the offset, per "Timestamps carry their + offset" below; existing headings without one are fine and are not worth going back to change. 3. `COMPLETED-CHECKLIST.md` is **append-only**; always add new groups at the bottom. 4. Leave only the remaining pending or in-progress items in the source `CHECKLIST.md`. Named feature files (`CHECKLIST-.md`) should be **deleted entirely** once all items are complete. Move their content to `COMPLETED-CHECKLIST.md` in the same directory before deleting. +### Timestamps carry their offset + +**When you write a date into a repository file, make it UTC or give it its offset.** This governs +`## Moved`, `## Resolved`, and the completed-item stamp. It costs a few characters and it is worth +them. + +**This is a presentation concern, not a correctness one.** A timestamp with an offset is +self-describing: two readers in different zones, or one reader on a machine whose zone is set +wrongly, still agree on the instant it denotes. A bare local date leaves that to be inferred. Nothing +is *wrong* with a bare date — the event happened when it happened — it is just ambiguous in a way +that a few extra characters remove. + +Note what the offset does and does not buy. It does **not** prevent a misconfigured machine from +writing a misleading date; it makes that date convertible afterwards. Worked example from this +repository: a laptop reporting `-04:00` while its owner sat in `-07:00` ran three hours fast, so +timestamps taken late in the evening carried the next day's date. Every affected commit was still +unambiguous, because git stores the instant with its offset — `2026-09-13T01:14:28-04:00` is exactly +`2026-09-12 22:14 -07:00`. A bare `2026-09-13` in a checklist was the only thing that had to be +re-derived from elsewhere. + +- **Do not go back and repair bare dates already written**, and do not rewrite history to relabel an + offset. The instants are correct either way, the ambiguity is small, and rewriting changes every + hash — on the branch that prompted this, two tracked files cited commit hashes, one of them a + split's mandatory `Split from ... at ` provenance line, and both would have dangled. Fix such + a date only when something visible actually depends on it. +- **`Get-Date` reports what the machine believes.** If its offset looks implausible for where the + engineer actually is, say so rather than quietly writing the date down. + ## Design note files Any directory in the repository may have a DESIGN-NOTES.md file. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 4e2791d68..12ad02f53 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -202,7 +202,7 @@ IDs keep their M2 numbers, for the reason given under M4. - [x] **M2.14** -- Make the two authoring rules this branch earned actually bite. Re-planned - 2026-09-13; see the rationale below before implementing either sub-step. Both sub-steps done. + 2026-09-12; see the rationale below before implementing either sub-step. Both sub-steps done. **As originally written this item said "write two authoring rules into the repository instructions". Measurement says that would have been worse than useless.** The two rules it From b54c3a06540a5746ff5e1b32a41129b94b883862 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 00:15:48 -0700 Subject: [PATCH 24/46] docs(platform-probes): correct claims this branch's own later commits falsified A whole-branch review found no defects in the code and two clusters of stale claims, both written early on this branch and falsified by its own later commits. This is the drift class the surrounding prose describes, landing in the file that is least allowed to carry it. Tier 1 DESIGN-NOTES, section D-encoded-row-is-the-contract: - It said "Nothing here enforces that rule yet" and pointed at CHECKLIST.md for the queued M3.5. M3.5 landed on this branch, so the rule IS enforced -- by every_state_that_blocks_agreement_reaches_the_row and the shared publication_holds predicate -- and CHECKLIST.md no longer contains the item, which is archived. This was the costly one: it instructed the next reader not to assume the instruments cover the rule, when they do. - It said the row "is built today" by interpolating positionally through a concat! template. That template was deleted three commits later; the row goes through Row::with and src/row.rs is the typed writer the sentence described as hypothetical. The only concat! left in topology_report.rs is inside a comment about the old design, verified by grep before saying so. - It counted report_oracle.rs's functions in the present tense. That file no longer extracts text at all. Restated as what the design COST, with no count of the current file -- a count of a live file is a census, and this branch has already corrected several. Cargo.toml: the dev-dependency comment asserted the opposite of what Cargo does. It claimed cfg(test) builds compile report_oracle WITHOUT oracle-in-renderer. They do not: the self dev-dependency is an edge from the package to itself, so under resolver 2 the feature unifies onto the package in any build that includes dev-dependencies. Measured with a compile_error! probe in both directions -- cargo check --tests succeeds under not(feature) and fails under feature, while a plain --lib build has it off. Consequently the matching serde dev-dependency was dead weight and is removed; all 213 tests pass without it. Also repointed src/topology/invariant.rs at COMPLETED-CHECKLIST.md for the M3.2 sabotage evidence, which the archive commit had left pointing at a file that no longer holds the item, and named the sabotage.json entry that now re-runs that evidence instead of asserting it. Two claims written while fixing this were themselves wrong and were caught by checking rather than asserting: the replacement text said report_oracle "is now five functions" (it is six, so the count was dropped rather than corrected), and the concat! claim needed the comment-versus-code distinction above. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/Cargo.toml | 23 +++++-- .../windows-platform-probes/DESIGN-NOTES.md | 63 ++++++++++++------- .../src/topology/invariant.rs | 5 +- 3 files changed, 61 insertions(+), 30 deletions(-) diff --git a/crates/windows-platform-probes/Cargo.toml b/crates/windows-platform-probes/Cargo.toml index 0371c245a..db464bcba 100644 --- a/crates/windows-platform-probes/Cargo.toml +++ b/crates/windows-platform-probes/Cargo.toml @@ -162,12 +162,23 @@ wtf-string = { path = "../wtf-string" } serde = { version = "1.0", optional = true } [dev-dependencies] -# Also here, non-optional, because `cfg(test)` builds compile `report_oracle` -# WITHOUT `oracle-in-renderer` -- the feature exists for integration tests, and -# unit tests get the module through `cfg(test)` instead. A dev-dependency is -# linked for every test target, so this is what makes the module's `cfg(any(test, -# feature = "oracle-in-renderer"))` gate resolve in both halves. -serde = "1.0" +# Here because [tests/a_real_report_agrees_with_itself.rs] uses `serde_json` +# DIRECTLY, to empty the row's diagnostic lists by parsing and re-rendering +# rather than by cutting the text. +# +# **Not to satisfy `report_oracle`'s gate, which an earlier version of this +# comment claimed.** That claim was backwards: it said `cfg(test)` builds compile +# the module without `oracle-in-renderer`. They do not. The self dev-dependency +# below is an edge from this package to itself, so under resolver 2 the feature +# unifies onto the package in ANY build that includes dev-dependencies -- the +# `cfg(test)` lib build included. Measured with a `compile_error!` probe in both +# directions: `cargo check --tests` succeeds under `not(feature = ...)` and fails +# under `feature = ...`, while a plain `--lib` build has the feature off. +# +# So the two arms of `cfg(any(test, feature = "oracle-in-renderer"))` are never +# exercised separately, the optional `serde`/`serde_json` are already active in +# every test build, and a matching `serde` dev-dependency was dead weight -- +# removed after confirming all 213 tests still pass without it. serde_json = "1.0" # A dependency on ITSELF, which cargo permits for dev-dependencies and which is # the only way to turn a feature on for this package's own integration tests: diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index ee7c6537b..e85c7213c 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1147,27 +1147,43 @@ comparisons and become **invariants on the observation, checked before rendering** -- `summary_missing` implies the verdict is not `agree`, and likewise for the other diagnostics and the counters. No parser is involved. -**Nothing here enforces that rule yet, and the first thing it would have caught -was already broken.** Every instrument in this crate starts from what the row -publishes -- the fact accounting enumerates the row's keys, the mutation sweep -perturbs code the row's construction reaches -- so all of them ask "does -anything read this key?" and none asks "does the prose state a fact the row -omits?". Measured: `CrossCheck::disagreements` was rendered per-entry in the -prose and published in the row as nothing at all, so a survey could see -`"cross_check":"disagree"` and not which counter disagreed. It survived 41 -review rounds and a zero-survivor mutation sweep. A reviewer found it by reading -the enum and asking who called `code()`. - -The second enumeration -- prose facts to row keys -- is queued as M3.5 in -[CHECKLIST.md](CHECKLIST.md). Until it exists the rule is a convention rather -than a checked property, which is worth stating plainly rather than leaving a -reader to assume the instruments cover it. +**This rule is enforced, and the first thing it would have caught was already +broken when the rule was written.** Every instrument in this crate used to start +from what the row publishes -- the fact accounting enumerated the row's keys, the +mutation sweep perturbed code the row's construction reached -- so all of them +asked "does anything read this key?" and none asked "does the prose state a fact +the row omits?". Measured: `CrossCheck::disagreements` was rendered per-entry in +the prose and published in the row as nothing at all, so a survey could see +`"cross_check":"disagree"` and not which counter disagreed. It survived 41 review +rounds and a zero-survivor mutation sweep. A reviewer found it by reading the +enum and asking who called `code()`. + +The second enumeration -- every state that forbids agreement to a published +condition -- now exists, in +[tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs): +`every_state_that_blocks_agreement_reaches_the_row` holds `topology::invariant`'s +blocking states against the row's keys, and `publication_holds` is the shared +predicate the corpus rule and its sabotage both call. Landed as M3.5, archived in +[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). + +(Until M3.5 landed this section said "nothing here enforces that rule yet" and +pointed at CHECKLIST.md for the queued item. Both statements outlived the +milestone that made them false -- the reason this Tier 1 file is swept against +the code rather than trusted, and an instance of the restatement drift the +repository instructions describe.) ### What the text-reading design cost -Counted in [src/report_oracle.rs](src/report_oracle.rs): of 38 top-level -functions, ten are correspondence rules and four are comparison helpers. -**Twenty-three exist only to extract values back out of rendered text.** +Counted in `src/report_oracle.rs` **as it stood before this decision**: of 38 +top-level functions, ten were correspondence rules and four were comparison +helpers. **Twenty-three existed only to extract values back out of rendered +text.** None of them survives: [src/report_oracle.rs](src/report_oracle.rs) reads +no rendered text at all now, and hand-writes no string scanning -- the row's +well-formedness is a `serde_json` parse and its keys come from that parser's own +tokens. So the counts above are what the design cost, not what the file holds. +(They are also the only counts kept here, because they describe a file that no +longer exists in that form and so cannot drift; a count of the CURRENT file would +be a census, and is deliberately absent.) That is a parser for a format this crate itself writes, and it behaved like one. A large share of PR #88's review rounds were defects in the READER rather than in @@ -1187,11 +1203,12 @@ is the stronger move: artifact. Measured on PR #88: an `io::Error` containing `{` was selected as the report's machine-readable row. A typed row emitted by one writer cannot have this. -- **Field order and labelling.** The row is built today by interpolating every - value positionally through a `concat!` template, so a field's name and its - value are related only by counting -- and a reordered argument or a miscounted - `{}` yields mislabelled data that still parses. A typed row with one writer - cannot have this either. +- **Field order and labelling.** The row was built by interpolating every value + positionally through a `concat!` template, so a field's name and its value were + related only by counting -- and a reordered argument or a miscounted `{}` gave + mislabelled data that still parses. A typed row with one writer cannot have + this either, and that is what M3.3 built: the template is gone, and + [src/row.rs](src/row.rs) is the one writer. Stated as the coupling rather than as a count, deliberately, and the reason is on the record: this said "eighteen values", was corrected to "seventeen" when diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 89f331259..a77c798fe 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -40,7 +40,10 @@ //! requires the verdict to have moved off `agree`. A push site deleted from //! `cross_check` leaves the state visible here and fires the rule; that is the //! whole design, and it is what the sabotage evidence in -//! [CHECKLIST.md](../../CHECKLIST.md) M3.2 demonstrates. +//! [COMPLETED-CHECKLIST.md](../../COMPLETED-CHECKLIST.md) M3.2 demonstrates. It +//! is also the manifest entry `cross_check forgets the changed bracket` in +//! [sabotage.json](../../sabotage.json), which re-runs that evidence rather than +//! leaving it as a claim about a sabotage somebody once performed. //! //! # What that means for testing them //! From 4afc212eee2ce753c2f6904f7572197c50820d9e Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 19:58:48 -0400 Subject: [PATCH 25/46] fix(platform-probes): hold the NUMA counter to the invariant that names it A whole-branch review found the module header's central claim false for two push sites. It says "a push site deleted from `cross_check` leaves the state visible here and fires the rule; that is the whole design" -- an unqualified claim, and NUMA was not covered by any rule, so deleting either NUMA push fired nothing here. The exclusion had a rationale, and the rationale was half right. `GetNumaHighestNodeNumber` reports the largest node NUMBER rather than a count, so there is no enumerated quantity to hold it to: nodes 0 and 2 are a valid sparse topology, and comparing it against `numa_domains` would manufacture a violation on hardware reporting itself correctly. That rules out ONE comparison. It does not rule out the comparison `cross_check` actually makes, which is highest-against-highest, and excluding NUMA on the strength of it left the claim overstated rather than the code wrong. Measured, and the measurement is why this is a `fix` and not a `docs`: deleting the NUMA disagreement push was CAUGHT -- by two direct `cross_check` unit tests, not by the invariant layer. So this was never a hole in the suite. It was a hole in the layer whose whole stated purpose is to catch exactly that deletion, which is worse in one specific way: a reader who trusts the header would believe the layer covers a push site it does not. Closed by adding the rule rather than by narrowing the claim. `agree` is reachable only through both NUMA branches declining to fire, so beside `agree` the counter must have been readable and equal -- `AgreedWithoutComparingCounter` reuses the existing variant for the unreadable case, and a new `AgreedDespiteNumaMismatch` carries the optional parse for the mismatch, because folding an `Option` into the count-shaped variant would have meant inventing a `usize` for an absent parse. Sabotage-verified after the change: the same deletion now reddens `every_numa_branch_in_the_real_cross_check_is_one_this_module_forbids`, which pairs each branch against the real `cross_check` the way the other states were already paired. The sparse-topology test is kept and its comment now says why the two rules differ, since it is the case that distinguishes them. Also narrowed the sabotage control's `why`, which claimed "no test may read the prose" and declared any test catching a prose reword to be the defect. False: about 35 tests assert report text with `contains`, because the human report is a deliverable worth covering. The real rule is that no test may derive a MACHINE-CONTRACT requirement from the wording. As written it would have told a future reader to delete legitimate coverage. Manifest re-swept, 7 of 7 as declared. 216 tests pass on x86_64 and i686. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/sabotage.json | 2 +- .../src/topology/invariant.rs | 59 ++++++++- .../src/topology/invariant/tests.rs | 113 ++++++++++++++++++ 3 files changed, 168 insertions(+), 6 deletions(-) diff --git a/crates/windows-platform-probes/sabotage.json b/crates/windows-platform-probes/sabotage.json index dd96d1ccc..20532d7ef 100644 --- a/crates/windows-platform-probes/sabotage.json +++ b/crates/windows-platform-probes/sabotage.json @@ -87,7 +87,7 @@ "name": "prose reworded to carry the same fact", "file": "src/topology_report.rs", "expect": "survives", - "why": "THE CONTROL, and it encodes this component's central decision as a measurement rather than as prose: the row is the machine contract and the prose is for a reader, so no test may read the prose. If this is ever reported as caught, a test has started asserting the wording again and THAT test is the defect -- not this entry.", + "why": "THE CONTROL, and it encodes this component's central decision as a measurement rather than as prose: the row is the machine contract and the prose is for a reader, so no test may derive a MACHINE-CONTRACT requirement from the wording. Note the narrowness -- tests may and do assert prose (about 35 of them read report text with contains), because the human report is a deliverable and its content is worth covering. What none of them may do is parse the prose, or pin a sentence the row is separately responsible for. So this entry rewords a line no test asserts, and if it is ever reported as caught, the question to ask is WHICH test caught it: one that reads the wording to learn something the row already publishes is the defect, and one that simply covers this sentence means the entry needs a different sentence.", "find": [ " let _ = writeln!(out, \"cache boundary divides this machine.\");" ], diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index a77c798fe..ff75ee73e 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -169,6 +169,18 @@ pub enum Violation { /// What the counter reported. read: usize, }, + /// `agree` beside a NUMA highest-node number that does not match the parse. + /// + /// Separate from [`Violation::AgreedDespiteCounterMismatch`] because the + /// quantity is different in kind: a largest node NUMBER, optional on both + /// sides, rather than a count. Folding it into the count-shaped variant + /// would have meant inventing a `usize` for an absent parse. + AgreedDespiteNumaMismatch { + /// What the parse carried, which may be nothing. + parsed: Option, + /// What `GetNumaHighestNodeNumber` reported. + counter: u32, + }, } impl fmt::Display for Violation { @@ -195,6 +207,18 @@ impl fmt::Display for Violation { "the verdict is `agree` but the parse carries {parsed} where \ {counter} read {read}" ), + Self::AgreedDespiteNumaMismatch { parsed, counter } => match parsed { + Some(parsed) => write!( + f, + "the verdict is `agree` but the parse's highest NUMA node is \ + {parsed} where GetNumaHighestNodeNumber read {counter}" + ), + None => write!( + f, + "the verdict is `agree` but the parse reports no NUMA node at \ + all where GetNumaHighestNodeNumber read {counter}" + ), + }, } } } @@ -291,11 +315,19 @@ pub fn check(observation: &Observation, verdict: Verdict) -> Vec { .collect(); // Zero is how both counters report failure, so a zero beside `agree` is the - // verdict claiming a comparison that could not have happened. The NUMA - // counter is deliberately absent, for the reason the renderer gives: it - // reports the largest node NUMBER rather than a count, so there is no - // enumerated quantity to hold it to, and nodes 0 and 2 are a valid sparse - // topology. + // verdict claiming a comparison that could not have happened. + // + // **The NUMA counter is held too, one rule further down, and the reason it + // was once absent is worth keeping because it was half right.** It reports + // the largest node NUMBER rather than a count, so there is no enumerated + // quantity to hold it to -- nodes 0 and 2 are a valid sparse topology, and + // comparing it against `numa_domains` would manufacture a violation on + // hardware reporting itself correctly. That argument rules out ONE + // comparison. It does not rule out the comparison `cross_check` actually + // makes, which is highest-against-highest, and excluding NUMA from here on + // the strength of it left the module's own claim -- that a push site deleted + // from `cross_check` fires a rule here -- false for precisely those two push + // sites. Found by a review. for (counter, parsed, read) in [ ( "GetActiveProcessorCount", @@ -319,6 +351,23 @@ pub fn check(observation: &Observation, verdict: Verdict) -> Vec { } } + // The NUMA comparison, in the shape `cross_check` makes it: highest node + // number against highest node number, never against a count. `agree` is + // reachable only through both of that function's NUMA branches declining to + // fire, so beside `agree` the counter must have been readable AND equal. + match observation.raw_highest_numa_node { + None => found.push(Violation::AgreedWithoutComparingCounter { + counter: "GetNumaHighestNodeNumber", + }), + Some(counter) if observation.highest_numa_node != Some(counter) => { + found.push(Violation::AgreedDespiteNumaMismatch { + parsed: observation.highest_numa_node, + counter, + }); + } + Some(_) => {} + } + found } diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index 9a8b7f757..24a358edd 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -388,6 +388,12 @@ fn the_numa_counter_is_deliberately_not_held_to_the_enumeration() { // sparse topology. Holding it to `numa_domains` would manufacture a // violation on hardware reporting itself correctly, which is the same // over-claim `cross_check` was corrected to stop making. + // + // A sparse topology is the whole point of this case: `numa_domains` is 2 + // while the highest node number is also 2, so the two differ by one and a + // count-shaped rule would fire. The highest-against-highest rule below does + // not, which is what makes them different rules rather than one rule that + // was left out. let mut observation = agreeing(); observation.numa_domains = 2; observation.highest_numa_node = Some(2); @@ -396,3 +402,110 @@ fn the_numa_counter_is_deliberately_not_held_to_the_enumeration() { assert_eq!(observation.cross_check().verdict(), Verdict::Agree); assert_eq!(check(&observation, Verdict::Agree), Vec::new()); } + +#[test] +fn an_agreeing_verdict_requires_the_numa_counter_to_have_been_read() { + // The branch `cross_check` takes when `GetNumaHighestNodeNumber` fails: it + // files `HighestNumaNodeFailed` and returns, so `agree` is unreachable. + // Delete that push and `agree` becomes reachable beside an unread counter, + // which is the defect this rule exists to name. + let mut observation = agreeing(); + observation.raw_highest_numa_node = None; + + assert_eq!( + check(&observation, Verdict::Agree), + vec![Violation::AgreedWithoutComparingCounter { + counter: "GetNumaHighestNodeNumber" + }] + ); +} + +#[test] +fn an_agreeing_verdict_requires_the_numa_counter_to_have_matched() { + // Both directions of the mismatch, because the parse's side is an `Option` + // and the absent case renders differently -- a rule whose message says + // "carries None" where a reader expected a number is a rule that will be + // misread in the one situation it fires. + let mut mismatched = agreeing(); + mismatched.highest_numa_node = Some(1); + mismatched.raw_highest_numa_node = Some(3); + + assert_eq!( + check(&mismatched, Verdict::Agree), + vec![Violation::AgreedDespiteNumaMismatch { + parsed: Some(1), + counter: 3 + }] + ); + + let mut unparsed = agreeing(); + unparsed.highest_numa_node = None; + unparsed.raw_highest_numa_node = Some(3); + + assert_eq!( + check(&unparsed, Verdict::Agree), + vec![Violation::AgreedDespiteNumaMismatch { + parsed: None, + counter: 3 + }] + ); + assert!( + Violation::AgreedDespiteNumaMismatch { + parsed: None, + counter: 3 + } + .to_string() + .contains("no NUMA node at all"), + "the absent case must not render as a number" + ); +} + +#[test] +fn every_numa_branch_in_the_real_cross_check_is_one_this_module_forbids() { + // **The claim in this module's header, checked rather than asserted**: a + // push site deleted from `cross_check` fires a rule here. For each NUMA + // branch, the verdict the real `cross_check` draws must already be + // something other than `agree`, AND this module must forbid `agree` for the + // same observation -- so the rule pins behaviour that exists rather than + // demanding behaviour that does not. + // + // This is the pairing the other states get from + // `every_blocking_state_is_one_the_real_cross_check_already_reports`; NUMA + // had neither half until a review found the header's claim was false for + // exactly these two branches. + for (what, mutate) in [ + ( + "counter unreadable", + Box::new(|o: &mut Observation| o.raw_highest_numa_node = None) + as Box, + ), + ( + "counter disagrees with the parse", + Box::new(|o: &mut Observation| { + o.highest_numa_node = Some(1); + o.raw_highest_numa_node = Some(3); + }), + ), + ] { + let mut observation = agreeing(); + mutate(&mut observation); + + let cross_check = observation.cross_check(); + assert_ne!( + cross_check.verdict(), + Verdict::Agree, + "{what}: `cross_check` must already forbid `agree`: {cross_check:?}" + ); + assert_ne!( + check(&observation, Verdict::Agree), + Vec::new(), + "{what}: and this module must forbid it too, or a deleted push site \ + here fires nothing" + ); + assert_eq!( + check(&observation, cross_check.verdict()), + Vec::new(), + "{what}: the verdict the crate actually draws must hold every rule" + ); + } +} From 6f57dd570a6986759e7d709d36ba19329c03e6a6 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 18:24:27 -0700 Subject: [PATCH 26/46] test(platform-probes): close the mutation survivors, and encode what they imply First mutation sweep of the two modules M3 created. row.rs came back clean on its first run -- 25 tested, 19 caught, 6 unviable, no survivors -- which review could not have established. topology/invariant.rs gave up three survivors, of two different kinds. **The two that were a real gap: `online_processors > 0` relaxed to `>= 0` in blocking_states.** The guard says a machine WITH processors and no packages is a finding; relaxing it accuses a topology that reported nothing of hiding packages it never claimed. The condition exists twice on purpose. cross_check has its own copy, and blocking_states must recompute rather than read cross_check's output, or it restates verdict() and goes blind to a deleted push site -- the whole reason the module exists. Independence is the design; AGREEMENT is the property, and the property was untested. The test that names this boundary, a_topology_with_no_processors_at_all_is_not_accused_of_hiding_packages, asserts on cross_check, so it covered one copy and left the other open. Five review rounds across four models did not find this; the sweep did, because a reviewer reasons about what code claims and a sweep asks what nothing notices. Closed by holding the two copies against each other over a corpus that spans the boundary, rather than by pinning one case: a guard whose condition drifts on either side is now caught wherever it drifts. A companion test asserts the corpus reaches BOTH answers, since a correspondence over shapes that all land on one side passes vacuously. **The third is equivalent and no test can kill it: `assert_holds` -> `()`.** It derives the verdict from observation.cross_check(), and check()'s rules hold for every such pair by construction -- they are reachable by code change, not by data, which is exactly why check() takes the verdict as a PARAMETER so a test can supply what a broken cross_check would give. assert_holds has no such seam, so nothing distinguishes it from (). The argument is now recorded at the function rather than left for the next sweep to rediscover, matching the assert_corresponds precedent from PR #88. Deliberately NOT added to sabotage.json: a `survives` entry means "this change is not a defect", and removing an assertion IS a defect -- one that happens to be undetectable. Labelling it `survives` would assert the opposite of the truth. The processor guard IS added to the manifest, so the pairing is re-run in two minutes rather than rediscovered in a three-minute sweep. 8 of 8 behave as declared. Re-swept after the fix: 23 caught, 1 missed, and the one is the equivalent mutant above. Sweep results recorded in DESIGN-RATIONALE.md, which claimed three sweeps and now states five. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-RATIONALE.md | 19 +++- crates/windows-platform-probes/sabotage.json | 12 +++ .../src/topology/invariant.rs | 19 ++++ .../src/topology/invariant/tests.rs | 102 +++++++++++++++++- 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index b1decad45..ae6b7c7ac 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -431,13 +431,30 @@ coverage: not *was this branch executed* but *does anything DETECT a change to it*. On a branch whose recurring defect is a test that runs code without establishing anything about it, that difference is the whole point. -Three sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1): +Five sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1). The +first three predate the M3 rewrite and are kept for the arithmetic note below; +the last two cover modules M3 created, which no sweep had ever reached: | file | tested | caught | unviable | survivors | |---|---|---|---|---| | `topology_report.rs` | 28 | 28 | 0 | 2, then none | | `report_oracle.rs` | 143 | 138 | 5 | 6, then none | | `topology.rs` | 186 | 180 | 6 | none, first run | +| `row.rs` | 25 | 19 | 6 | none, first run | +| `topology/invariant.rs` | 26 | 21 | 2 | 3, then one equivalent | + +**The two later sweeps are the argument for running them at all.** `row.rs` -- +the crate's only defence against caller text reaching the mined artifact -- came +back clean on its first run, which no amount of review could have established. +`topology/invariant.rs` gave up a real gap that five review rounds across four +models had not: relaxing `online_processors > 0` to `>= 0` survived, because the +test that NAMES that boundary asserts on `cross_check` and so covered only one of +the two deliberate copies of the condition. A reviewer reasons about what code +claims; a sweep asks what nothing notices, and those find different things. + +The remaining survivor is `assert_holds`, for the same reason `assert_corresponds` +survived below, and the argument is now recorded at the function rather than left +for the next sweep to rediscover. The unviable column is why a caught-count does not equal a tested-count: those mutants did not compile, so they say nothing either way. An earlier version of diff --git a/crates/windows-platform-probes/sabotage.json b/crates/windows-platform-probes/sabotage.json index 20532d7ef..3da271fc3 100644 --- a/crates/windows-platform-probes/sabotage.json +++ b/crates/windows-platform-probes/sabotage.json @@ -94,6 +94,18 @@ "replace": [ " let _ = writeln!(out, \"cache boundary splits this machine.\");" ] + }, + { + "name": "blocking_states relaxes the processor guard", + "file": "src/topology/invariant.rs", + "expect": "caught", + "why": "The guard says a machine with processors and no packages is a finding; relaxing it to >= makes a topology that reported NOTHING a finding too, accusing a synthetic observation of hiding packages it never claimed. This condition exists TWICE on purpose -- cross_check has its own copy, and blocking_states must recompute rather than read cross_check's output, or it restates verdict() and goes blind to a deleted push site. Independence is the design; agreement is the property. A mutation sweep found the property untested: the test that names this boundary asserts on cross_check, so it covered only the other copy, and this mutation survived five review rounds across four models. Kept here so the pairing is re-run rather than re-discovered.", + "find": [ + " if observation.online_processors > 0 && observation.packages == 0 {" + ], + "replace": [ + " if observation.online_processors >= 0 && observation.packages == 0 {" + ] } ] } diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index ff75ee73e..4992e2a7b 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -373,6 +373,25 @@ pub fn check(observation: &Observation, verdict: Verdict) -> Vec { /// [`check`], as an assertion, for the call sites that are bound to it. /// +/// **Replacing this body with `()` survives a mutation sweep, and no test can +/// change that.** Recorded here rather than left for the next sweep to +/// re-discover, because the argument is short and the alternative is a test +/// manufactured to reach code nothing can reach. +/// +/// It derives the verdict from `observation.cross_check()`, so the pair it +/// checks is always the pair the crate itself produces -- and [`check`]'s rules +/// hold for every such pair by construction, as the module header explains: they +/// are reachable by CODE CHANGE, not by data. That is precisely why [`check`] +/// takes the verdict as a PARAMETER, letting a test supply the answer a broken +/// `cross_check` would give; this function has no such seam, so there is no +/// observation for which it panics and nothing to distinguish it from `()`. +/// +/// The same survivor was recorded for `assert_corresponds` on PR #88 -- see +/// [DESIGN-RATIONALE.md](../../DESIGN-RATIONALE.md) -- for the same reason: every +/// instrument that would notice goes THROUGH it. A binding that cannot fail on +/// data is checked by the sweep's `caught` results on [`check`] itself, which is +/// where the behaviour lives. +/// /// # Panics /// /// Panics listing every invariant the observation violated. diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index 24a358edd..e94e3e516 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -14,6 +14,9 @@ //! them to ignore the instrument -- the same rule the report oracle is built on. use super::{BlockingState, Violation, blocking_states, check}; + +/// A named mutation of an observation, for the tests that build a corpus. +type Perturb = Box; use crate::topology::{ BracketOutcome, CacheLevel, CoreShape, Observation, PartitioningCache, Verdict, }; @@ -93,7 +96,7 @@ fn the_verdict_the_crate_actually_draws_holds_every_invariant() { /// /// This doc claimed such a relation before one existed. Found by a review: the /// table happened to match, which is the condition under which nobody notices. -type Perturbation = (BlockingState, Box); +type Perturbation = (BlockingState, Perturb); fn perturbations() -> Vec { vec![ @@ -403,6 +406,103 @@ fn the_numa_counter_is_deliberately_not_held_to_the_enumeration() { assert_eq!(check(&observation, Verdict::Agree), Vec::new()); } +#[test] +fn the_processor_guard_agrees_with_the_one_cross_check_applies() { + // **Two copies of one boundary, deliberately, and nothing held them to each + // other.** `blocking_states` recomputes `online_processors > 0 && packages + // == 0` rather than asking `cross_check`, and it MUST: a rule that reads + // `cross_check`'s output restates `verdict()` and is blind to a deleted push + // site, which is the whole reason this module exists. Independence is the + // design; agreement is the property, and the property was untested. + // + // Found by a mutation sweep, not by review. Relaxing either `>` to `>=` here + // survived five review rounds across four models, because the test that + // names this boundary -- + // `a_topology_with_no_processors_at_all_is_not_accused_of_hiding_packages` + // -- asserts on `cross_check` and so covers only the OTHER copy. + // + // Written as a correspondence over a corpus that spans the boundary rather + // than as a single case, so a future guard whose condition drifts on either + // side is caught wherever it drifts. + let shapes: [(&str, Perturb); 4] = [ + ( + "no processors, and nothing else reported either", + Box::new(|o: &mut Observation| { + o.online_processors = 0; + o.raw_active_processors = 0; + o.packages = 0; + o.cores = Vec::new(); + }), + ), + ( + "no processors, but packages and cores reported", + Box::new(|o: &mut Observation| { + o.online_processors = 0; + o.raw_active_processors = 0; + }), + ), + ( + "processors reported, packages absent", + Box::new(|o: &mut Observation| o.packages = 0), + ), + ( + "processors reported, cores absent", + Box::new(|o: &mut Observation| o.cores = Vec::new()), + ), + ]; + + for (shape, mutate) in shapes { + let mut observation = agreeing(); + mutate(&mut observation); + + let states = blocking_states(&observation); + let parse = observation.cross_check().parse_incomplete; + + assert_eq!( + states.contains(&BlockingState::NoPackages), + parse.contains(&crate::topology::ParseIncomplete::NoPackages), + "{shape}: this module and `cross_check` disagree about whether \ + absent packages are a finding -- states {states:?}, parse {parse:?}" + ); + assert_eq!( + states.contains(&BlockingState::NoCores), + parse.contains(&crate::topology::ParseIncomplete::NoCores), + "{shape}: this module and `cross_check` disagree about whether \ + absent cores are a finding -- states {states:?}, parse {parse:?}" + ); + } +} + +#[test] +fn a_corpus_spanning_the_processor_guard_reaches_both_of_its_answers() { + // The correspondence above compares two computations, so it passes + // vacuously if every shape lands on the same side of the boundary. Assert + // that the corpus reaches BOTH answers, or the test is a comparison of two + // constants. + let mut absent = agreeing(); + absent.packages = 0; + absent.cores = Vec::new(); + assert!( + blocking_states(&absent).contains(&BlockingState::NoPackages), + "a machine with processors and no packages must be in the state" + ); + + let mut unmeasured = agreeing(); + unmeasured.online_processors = 0; + unmeasured.raw_active_processors = 0; + unmeasured.packages = 0; + unmeasured.cores = Vec::new(); + assert!( + !blocking_states(&unmeasured).contains(&BlockingState::NoPackages), + "a topology that reported no processors is not accused of hiding \ + packages -- this is the `> 0` the sweep found unguarded" + ); + assert!( + !blocking_states(&unmeasured).contains(&BlockingState::NoCores), + "nor of hiding cores" + ); +} + #[test] fn an_agreeing_verdict_requires_the_numa_counter_to_have_been_read() { // The branch `cross_check` takes when `GetNumaHighestNodeNumber` fails: it From 2e63a3df00e4a224042e18f88a61d32eef02c792 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 19:03:08 -0700 Subject: [PATCH 27/46] test(platform-probes): pin what the diagnostics publish into the row A whole-branch review found the row's own payloads unchecked, and a sweep of the module put a number on it: 26 mutants, 9 caught, 12 MISSED. 46%. This is the field-labelling defect row.rs exists to make unrepresentable, reappearing one level down. row.rs pairs a name with its value so position cannot mislabel them, and MEASURED_ROW_KEYS closes the top-level key set. Then published() hand-pairs names with values INSIDE each entry, and nothing was watching. What that left open, measured rather than reasoned: - Rewriting the count helper to `*value * 7 + 1` -- every count in every parse_incomplete entry wrong -- left 218 tests passing. Reproduced before touching anything. - NotCompared::code could be replaced wholesale with "" or "xyzzy". Its own doc says "Changing one of these is a breaking change to the NDJSON row"; not one of the six was pinned anywhere. - Every arm of published_anomaly was deletable, losing source labelling and every payload field. - Two arms of anomaly_code were deletable, so a real buffer overrun would publish as `unclassified` -- whose documented meaning is "this probe's vocabulary is older than the crate". A survey would mis-attribute it rather than see it. The tests that LOOKED like they covered this could not. They built their expectation from code() and compared it against a row the writer had built from code(), so both sides moved together -- the tautology class this branch deletes elsewhere, in the one place it mattered most. Closed with goldens, written as literals on purpose. A predicate over values is defined once and asked, never restated; a code and a field name are not predicates but a SCHEMA, and a schema is not derivable from the thing that emits it. Writing it twice is how a golden works. Completeness is compiler-checked where the enum belongs to this crate: the expectation is an exhaustive match, so a variant added without a golden does not compile. AnomalyKind and Source are #[non_exhaustive] upstream, so those use explicit instances plus a direct assertion that a named kind does NOT fall through to the catch-all. Sabotage-verified: the count rewrite that left 218 green now reddens. Re-swept: 12 missed becomes 2. The two that remain are both Display impls -- the PROSE rendering, which under d-encoded-row-is-the-contract is a review obligation rather than a machine-checked one. Left open deliberately and recorded as such, not closed in passing: whether prose deserves machine coverage is a decision about where that line sits. Also corrected DESIGN-RATIONALE.md, which said five sweeps covering "the two modules M3 created". M3 created THREE, and the unswept one was the largest at 629 lines and the only one with survivors. My own claim from commit 6f57dd5, wrong within the hour -- the same class as b54c3a0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-RATIONALE.md | 56 ++- .../src/topology/diagnostic.rs | 3 + .../src/topology/diagnostic/tests.rs | 330 ++++++++++++++++++ 3 files changed, 376 insertions(+), 13 deletions(-) create mode 100644 crates/windows-platform-probes/src/topology/diagnostic/tests.rs diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index ae6b7c7ac..3bba4949f 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -431,9 +431,10 @@ coverage: not *was this branch executed* but *does anything DETECT a change to it*. On a branch whose recurring defect is a test that runs code without establishing anything about it, that difference is the whole point. -Five sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1). The +Six sweeps, run through [tools/run-mutants.ps1](../../tools/run-mutants.ps1). The first three predate the M3 rewrite and are kept for the arithmetic note below; -the last two cover modules M3 created, which no sweep had ever reached: +the last three cover the three modules M3 created, none of which any sweep had +reached: | file | tested | caught | unviable | survivors | |---|---|---|---|---| @@ -442,17 +443,46 @@ the last two cover modules M3 created, which no sweep had ever reached: | `topology.rs` | 186 | 180 | 6 | none, first run | | `row.rs` | 25 | 19 | 6 | none, first run | | `topology/invariant.rs` | 26 | 21 | 2 | 3, then one equivalent | - -**The two later sweeps are the argument for running them at all.** `row.rs` -- -the crate's only defence against caller text reaching the mined artifact -- came -back clean on its first run, which no amount of review could have established. -`topology/invariant.rs` gave up a real gap that five review rounds across four -models had not: relaxing `online_processors > 0` to `>= 0` survived, because the -test that NAMES that boundary asserts on `cross_check` and so covered only one of -the two deliberate copies of the condition. A reviewer reasons about what code -claims; a sweep asks what nothing notices, and those find different things. - -The remaining survivor is `assert_holds`, for the same reason `assert_corresponds` +| `topology/diagnostic.rs` | 26 | 9 | 5 | **12**, then two prose | + +**The three later sweeps are the argument for running them at all**, and each +made a different case. `row.rs` -- the crate's only defence against caller text +reaching the mined artifact -- came back clean on its first run, which no amount +of review could have established. `topology/invariant.rs` gave up a real gap that +five review rounds across four models had not: relaxing `online_processors > 0` +to `>= 0` survived, because the test that NAMES that boundary asserts on +`cross_check` and so covered only one of the two deliberate copies of the +condition. + +**`topology/diagnostic.rs` is the one that mattered.** It survived 12 of 26 -- +46% -- and the survivors were the row's own payloads: `NotCompared::code` could +be replaced wholesale with `""`, every arm of `published_anomaly` deleted, and +two arms of `anomaly_code` deleted so that a real buffer overrun would publish as +`unclassified`, whose documented meaning is the opposite. Measured separately: +rewriting the `count` helper so every published count was wrong left 218 tests +green. + +This is the field-labelling defect [src/row.rs](src/row.rs) exists to make +unrepresentable, reappearing one level down. `row.rs` pairs a name with its value +so position cannot mislabel them; these functions then hand-pair names with +values INSIDE each entry, and nothing was watching. The tests that looked like +they covered it built their expectation from `code()` and compared it against a +row the writer had built from `code()` -- both sides moving together, which is +the tautology class this branch deletes elsewhere. + +Closed with goldens in [src/topology/diagnostic/tests.rs](src/topology/diagnostic/tests.rs), +written as literals on purpose: a code is a SCHEMA, not a predicate, and a schema +is not derivable from the thing that emits it. Completeness is compiler-checked +where the enum belongs to this crate, via an exhaustive `match` in the test. + +Two survivors remain and both are `Display` impls -- the PROSE rendering, which +under [DESIGN-NOTES.md](DESIGN-NOTES.md) -> `d-encoded-row-is-the-contract` is a +review obligation rather than a machine-checked one. They are left open +deliberately rather than by oversight; whether the prose deserves machine +coverage of its own is a decision about where that line sits, not a gap to close +in passing. + +The `assert_holds` survivor is equivalent, for the same reason `assert_corresponds` survived below, and the argument is now recorded at the function rather than left for the next sweep to rediscover. diff --git a/crates/windows-platform-probes/src/topology/diagnostic.rs b/crates/windows-platform-probes/src/topology/diagnostic.rs index 14f201e2b..42b090f42 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic.rs @@ -174,6 +174,9 @@ pub enum NotCompared { HighestNumaNodeFailed, } +#[cfg(test)] +mod tests; + impl NotCompared { /// The stable discriminant a survey groups by. /// diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs new file mode 100644 index 000000000..d7441533d --- /dev/null +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -0,0 +1,330 @@ +// Copyright (c) Mike Grier. + +//! Tests for what the diagnostics publish into the row. +//! +//! # Why these are literals +//! +//! Every assertion here writes the expected wire form out by hand. That is +//! deliberate, and it is the opposite of what this crate does elsewhere: a rule +//! that is a PREDICATE over values is defined once and asked, never restated, +//! because a hand-written second copy checks the copy rather than the contract. +//! +//! A code and a field name are not predicates. They are a SCHEMA -- the names a +//! fleet survey groups by, which this module's own docs call "a breaking change +//! to the NDJSON row" -- and a schema is not derivable from the thing that emits +//! it. Writing it down twice is how a golden works: the test disagrees when the +//! writer moves, which is the entire point. +//! +//! The distinction matters because the tests that existed before these did the +//! derivable thing to the non-derivable one. They built their expectation from +//! `code()` and compared it against a row the writer had built from `code()`, so +//! both sides moved together and neither pinned anything. +//! +//! # What that left open, measured +//! +//! A mutation sweep of the parent module returned **12 survivors of 26** -- +//! `NotCompared::code` could be replaced wholesale with `""` or `"xyzzy"`, and +//! every arm of `published_anomaly` and two of `anomaly_code` could be deleted, +//! all with a green suite. Separately, rewriting `published`'s count helper to +//! `*value * 7 + 1` -- every count in every entry wrong -- left 218 tests +//! passing. +//! +//! That is the field-labelling defect `row.rs` exists to make unrepresentable, +//! reappearing one level down: `row.rs` pairs a name with its value so position +//! cannot mislabel them, and then these functions hand-pair names with values +//! inside each entry, where nothing was watching. +//! +//! # Completeness +//! +//! Where the enum belongs to this crate, the expectation is written as an +//! exhaustive `match`, so a variant added without a golden does not compile. +//! `AnomalyKind` and `Source` are `#[non_exhaustive]` upstream and cannot be +//! matched exhaustively; for those the goldens are explicit instances, and the +//! `unclassified` fallback is asserted directly so that the arm which is +//! SUPPOSED to catch an unknown kind is distinguished from an arm that fell +//! through by accident. + +use super::{Disagreement, NotCompared, ParseIncomplete, anomaly_code, published_anomaly}; +use crate::row::Row; +use windows_topology_sys::{AnomalyKind, EnumerationAnomaly, Source}; + +/// The published value, rendered through the row's own writer. +/// +/// Wrapped in a row rather than rendered directly, so the bytes under test are +/// the bytes a survey reads -- escaping, separators and all -- rather than a +/// second rendering written for the test. +fn rendered(value: crate::row::Value) -> String { + let row = Row::new("x-test").with("entry", value).render(); + let opened = row.find("\"entry\":").expect("the entry is present") + "\"entry\":".len(); + row[opened..row.len() - 1].to_owned() +} + +fn anomaly(source: Source, offset: usize, kind: AnomalyKind) -> EnumerationAnomaly { + EnumerationAnomaly { + source, + offset, + kind, + } +} + +#[test] +fn every_not_compared_code_is_the_one_the_row_promises() { + // Exhaustive, so a seventh variant does not compile until it has a code + // here. All six were unpinned: the sweep replaced the whole function with + // `""` and with `"xyzzy"` and nothing noticed. + let golden = |reason: &NotCompared| match reason { + NotCompared::MachineChanged => "machine_changed", + NotCompared::BracketNotEstablished => "bracket_not_established", + NotCompared::CountsIncludeUnparsedRelations => "counts_include_unparsed_relations", + NotCompared::ActiveProcessorCountFailed => "active_processor_count_failed", + NotCompared::ActiveProcessorGroupCountFailed => "active_processor_group_count_failed", + NotCompared::HighestNumaNodeFailed => "highest_numa_node_failed", + }; + + for reason in [ + NotCompared::MachineChanged, + NotCompared::BracketNotEstablished, + NotCompared::CountsIncludeUnparsedRelations, + NotCompared::ActiveProcessorCountFailed, + NotCompared::ActiveProcessorGroupCountFailed, + NotCompared::HighestNumaNodeFailed, + ] { + assert_eq!(reason.code(), golden(&reason), "{reason:?}"); + assert_eq!( + rendered(reason.published()), + format!("{{\"code\":\"{}\"}}", golden(&reason)), + "{reason:?}: a bare condition publishes its code and nothing else" + ); + } +} + +#[test] +fn every_disagreement_publishes_the_pair_it_carries() { + // `parsed` and `counter` are the two numbers a survey compares, so swapping + // the labels is the mislabelling defect in its purest form: the row still + // parses and says the opposite of the truth. + assert_eq!( + rendered( + Disagreement::OnlineProcessors { + parsed: 12, + counter: 16, + } + .published() + ), + r#"{"code":"online_processors","parsed":12,"counter":16}"# + ); + assert_eq!( + rendered( + Disagreement::ProcessorGroups { + parsed: 1, + counter: 2, + } + .published() + ), + r#"{"code":"processor_groups","parsed":1,"counter":2}"# + ); + assert_eq!( + rendered( + Disagreement::HighestNumaNode { + parsed: Some(2), + counter: 3, + } + .published() + ), + r#"{"code":"highest_numa_node","parsed":2,"counter":3}"# + ); + // The absent parse renders as `null`, not as a number and not as an omitted + // field: a survey must be able to tell "the parse saw no NUMA node" from + // "the parse saw node 0". + assert_eq!( + rendered( + Disagreement::HighestNumaNode { + parsed: None, + counter: 3, + } + .published() + ), + r#"{"code":"highest_numa_node","parsed":null,"counter":3}"# + ); +} + +#[test] +fn every_parse_incomplete_shape_publishes_the_fields_its_variant_carries() { + // One instance of each PAYLOAD SHAPE rather than of each variant: the twelve + // counted variants share a single helper, and it was rewriting every one of + // them wrongly that left 218 tests green. + let cases = [ + ( + ParseIncomplete::ContradictoryCores { count: 3 }, + r#"{"code":"contradictory_cores","count":3}"#, + ), + (ParseIncomplete::NoPackages, r#"{"code":"no_packages"}"#), + ( + ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1, 2] }, + r#"{"code":"cache_levels_without_partitions","levels":[1,2]}"#, + ), + ( + ParseIncomplete::MeasuredButCountsAbsent { + absent: vec!["packages"], + }, + r#"{"code":"measured_but_counts_absent","absent":["packages"]}"#, + ), + ( + ParseIncomplete::PartitioningSummaryMissing { level: 3 }, + r#"{"code":"partitioning_summary_missing","level":3}"#, + ), + ( + ParseIncomplete::RelationsWithoutProcessors { + cores: 4, + packages: 7, + }, + r#"{"code":"relations_without_processors","cores":4,"packages":7}"#, + ), + ( + ParseIncomplete::EnumerationsDisagreed { + attempts: 2, + walk_only: 5, + cpu_sets_only: 9, + }, + r#"{"code":"enumerations_disagreed","attempts":2,"walk_only":5,"cpu_sets_only":9}"#, + ), + ]; + + for (entry, golden) in cases { + assert_eq!(rendered(entry.published()), golden, "{entry:?}"); + } +} + +#[test] +fn distinct_values_in_one_entry_are_not_interchangeable() { + // **The labelling check, stated as a property rather than as another + // golden.** The goldens above would still pass if two fields were swapped + // AND both goldens were updated to match -- which is exactly what an author + // mid-refactor does. This asks the narrower question a golden cannot: with + // every value distinct, does each name carry ITS value? + let relations = rendered( + ParseIncomplete::RelationsWithoutProcessors { + cores: 4, + packages: 7, + } + .published(), + ); + assert!( + relations.contains(r#""cores":4"#) && relations.contains(r#""packages":7"#), + "cores and packages must not be interchanged: {relations}" + ); + + let undersized = rendered(published_anomaly(&anomaly( + Source::RelationshipWalk, + 64, + AnomalyKind::Undersized { + declared: 8, + minimum: 48, + }, + ))); + assert!( + undersized.contains("\"declared\":8") && undersized.contains("\"minimum\":48"), + "declared and minimum must not be interchanged: {undersized}" + ); +} + +#[test] +fn every_named_anomaly_kind_has_a_code_of_its_own() { + // A deleted arm here does not fail loudly -- it falls through to + // `unclassified`, whose documented meaning is "this probe's vocabulary is + // older than the crate". A real overrun would then be filed as an unknown + // kind and mis-attributed across a fleet. Two of these arms were deletable + // with a green suite. + let cases = [ + ( + AnomalyKind::Undersized { + declared: 1, + minimum: 2, + }, + "undersized", + ), + ( + AnomalyKind::OverrunsBuffer { + declared: 3, + remaining: 4, + }, + "overruns_buffer", + ), + ( + AnomalyKind::TrailingBytes { remaining: 5 }, + "trailing_bytes", + ), + ( + AnomalyKind::TruncatedArray { + declared: 6, + decoded: 7, + }, + "truncated_array", + ), + ]; + + for (kind, golden) in cases { + let described = format!("{kind:?}"); + let found = anomaly_code(&anomaly(Source::RelationshipWalk, 0, kind)); + assert_eq!(found, golden, "{described}"); + assert_ne!( + found, "unclassified", + "{described}: a kind this crate names must not fall through to the \ + catch-all, which says the vocabulary is older than the crate" + ); + } +} + +#[test] +fn every_anomaly_publishes_where_it_was_found_as_well_as_what() { + // `source` and `offset` are the fields the module's docs give the reason + // for -- the same kind at the same offset across a fleet is a different + // finding from the same kind scattered. Every arm below was deletable. + let cases = [ + ( + anomaly( + Source::RelationshipWalk, + 64, + AnomalyKind::Undersized { + declared: 8, + minimum: 48, + }, + ), + r#"{"code":"undersized","source":"relationship_walk","offset":64,"declared":8,"minimum":48}"#, + ), + ( + anomaly( + Source::CpuSets, + 128, + AnomalyKind::OverrunsBuffer { + declared: 96, + remaining: 32, + }, + ), + r#"{"code":"overruns_buffer","source":"cpu_sets","offset":128,"declared":96,"remaining":32}"#, + ), + ( + anomaly( + Source::RelationshipWalk, + 256, + AnomalyKind::TrailingBytes { remaining: 12 }, + ), + r#"{"code":"trailing_bytes","source":"relationship_walk","offset":256,"remaining":12}"#, + ), + ( + anomaly( + Source::CpuSets, + 512, + AnomalyKind::TruncatedArray { + declared: 10, + decoded: 6, + }, + ), + r#"{"code":"truncated_array","source":"cpu_sets","offset":512,"declared":10,"decoded":6}"#, + ), + ]; + + for (found, golden) in cases { + assert_eq!(rendered(published_anomaly(&found)), golden, "{found:?}"); + } +} From 7c9fb7dd63b258ab80858ee5e0fb32f17f811b9b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 19:39:08 -0700 Subject: [PATCH 28/46] fix(platform-probes): call out a diagnostic that describes itself as nothing The last two mutation survivors were both `Display` impls. Blanking either left the suite green, and a reader would have got ` - ` with nothing after the dash. Both obvious answers were wrong. Pinning the sentences would make the prose machine-checked, which d-encoded-row-is-the-contract deliberately does not do -- the row is the machine contract and the prose is a review obligation. Leaving them ships a blank line. The engineer named a third: a blank is perhaps wrong, perhaps it should be called out. That is better than either, because it separates two things the survivors had conflated. **A diagnostic's WORDING is a review obligation; its PRESENCE is not.** Presence is structural -- the renderer knows it holds an entry to print -- so it can be machine-checked without any sentence being pinned. So the renderer routes every diagnostic through `described`, which substitutes an explicit BUG IN THIS PROBE line for a rendering that is empty or blank, and a test asserts no diagnostic renders as that line. That pins THAT each entry describes itself without pinning WHAT it says. A blank was the worst available answer on its own terms, separately from the sweep: it is indistinguishable from a rendering bug, from a finding with genuinely nothing to say, and from a stray newline, so a reader cannot tell whether the probe found something it failed to describe. Both improvements fall out of the one branch. The reader gets a stated defect instead of a blank, and the mutants become catchable, because blanking a Display now produces the callout the test forbids. Verified: diagnostic.rs swept to ZERO survivors, from 12 two commits ago and 2 before this one -- including the two mutants `described` itself introduced, so the fix carries its own coverage. Whitespace is treated as blank, since a reader cannot tell it from emptiness on the page, and a test pins both. The sabotage manifest's prose control STILL survives, which is the check that matters here: `described` did not quietly move the line by making wording machine-checked. 8 of 8 behave as declared. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DESIGN-RATIONALE.md | 29 +++-- .../src/topology/diagnostic.rs | 33 ++++++ .../src/topology/diagnostic/tests.rs | 103 +++++++++++++++++- .../src/topology_report.rs | 12 +- 4 files changed, 163 insertions(+), 14 deletions(-) diff --git a/crates/windows-platform-probes/DESIGN-RATIONALE.md b/crates/windows-platform-probes/DESIGN-RATIONALE.md index 3bba4949f..2ae0566e9 100644 --- a/crates/windows-platform-probes/DESIGN-RATIONALE.md +++ b/crates/windows-platform-probes/DESIGN-RATIONALE.md @@ -443,7 +443,7 @@ reached: | `topology.rs` | 186 | 180 | 6 | none, first run | | `row.rs` | 25 | 19 | 6 | none, first run | | `topology/invariant.rs` | 26 | 21 | 2 | 3, then one equivalent | -| `topology/diagnostic.rs` | 26 | 9 | 5 | **12**, then two prose | +| `topology/diagnostic.rs` | 28 | 23 | 5 | **12**, then two prose, then none | **The three later sweeps are the argument for running them at all**, and each made a different case. `row.rs` -- the crate's only defence against caller text @@ -475,12 +475,27 @@ written as literals on purpose: a code is a SCHEMA, not a predicate, and a schem is not derivable from the thing that emits it. Completeness is compiler-checked where the enum belongs to this crate, via an exhaustive `match` in the test. -Two survivors remain and both are `Display` impls -- the PROSE rendering, which -under [DESIGN-NOTES.md](DESIGN-NOTES.md) -> `d-encoded-row-is-the-contract` is a -review obligation rather than a machine-checked one. They are left open -deliberately rather than by oversight; whether the prose deserves machine -coverage of its own is a decision about where that line sits, not a gap to close -in passing. +**The last two survivors were both `Display` impls, and how they were closed is +the part worth keeping.** Blanking either left the suite green, and a reader +would have got ` - ` with nothing after the dash. Both obvious fixes were +wrong: pinning the sentences would make the prose machine-checked, which +[DESIGN-NOTES.md](DESIGN-NOTES.md) -> `d-encoded-row-is-the-contract` deliberately +does not do, and leaving them ships a blank line. + +The engineer's question -- "a blank line is perhaps wrong, perhaps it should be +called out?" -- named a third answer better than either. **A diagnostic's WORDING +is a review obligation; its PRESENCE is not.** The renderer now routes every +entry through `described`, which substitutes an explicit `BUG IN THIS PROBE` line +for a rendering that is empty or blank, and a test asserts that no diagnostic +renders as that line. That pins THAT each entry describes itself without pinning +WHAT it says. + +Both improvements fall out of the same branch. A reader gets a stated defect +instead of a blank -- which is indistinguishable from a rendering bug, from a +finding with genuinely nothing to say, and from a stray newline -- and the +mutants become catchable, because blanking a `Display` now produces the callout +the test forbids. The sweep went to **zero survivors**, including the two mutants +`described` itself introduced. The `assert_holds` survivor is equivalent, for the same reason `assert_corresponds` survived below, and the argument is now recorded at the function rather than left diff --git a/crates/windows-platform-probes/src/topology/diagnostic.rs b/crates/windows-platform-probes/src/topology/diagnostic.rs index 42b090f42..aa31d6f78 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic.rs @@ -177,6 +177,39 @@ pub enum NotCompared { #[cfg(test)] mod tests; +/// What a report should print instead of an entry that describes itself as +/// nothing. +/// +/// **The wording of a diagnostic is a review obligation; its PRESENCE is not.** +/// Under the decision that the row is the machine contract, no test pins the +/// sentence a `Display` impl produces -- and a mutation sweep showed the cost of +/// stopping there: blanking `Display for Disagreement` or `Display for +/// NotCompared` left the suite green, and a reader would have got ` - ` with +/// nothing after the dash. +/// +/// A blank is the worst of the available answers. It is indistinguishable from a +/// rendering bug, from a finding with genuinely nothing to say, and from a stray +/// newline, so a reader cannot tell whether the probe found something it failed +/// to describe. Saying so explicitly costs one branch and turns an invisible +/// defect into a visible one. +/// +/// This is the seam that lets presence be machine-checked without wording being +/// checked: a test can assert that no diagnostic renders as this string, which +/// pins THAT each entry describes itself without pinning WHAT it says. +pub const UNDESCRIBED: &str = + "BUG IN THIS PROBE: a finding was recorded with nothing to say about it"; + +/// `entry` as the report should print it, or [`UNDESCRIBED`] if it prints blank. +#[must_use] +pub fn described(entry: &impl fmt::Display) -> String { + let text = entry.to_string(); + if text.trim().is_empty() { + UNDESCRIBED.to_owned() + } else { + text + } +} + impl NotCompared { /// The stable discriminant a survey groups by. /// diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs index d7441533d..2c4212c8f 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -44,7 +44,10 @@ //! SUPPOSED to catch an unknown kind is distinguished from an arm that fell //! through by accident. -use super::{Disagreement, NotCompared, ParseIncomplete, anomaly_code, published_anomaly}; +use super::{ + Disagreement, NotCompared, ParseIncomplete, UNDESCRIBED, anomaly_code, described, + published_anomaly, +}; use crate::row::Row; use windows_topology_sys::{AnomalyKind, EnumerationAnomaly, Source}; @@ -328,3 +331,101 @@ fn every_anomaly_publishes_where_it_was_found_as_well_as_what() { assert_eq!(rendered(published_anomaly(&found)), golden, "{found:?}"); } } + +#[test] +fn every_diagnostic_describes_itself() { + // **Presence is machine-checked here; WORDING is not, and that is the + // whole point of the seam.** This asserts only that each entry renders as + // something a reader can act on -- never what it says -- so the prose stays + // a review obligation while a blank stops being possible to ship. + // + // Two `Display` impls could be blanked with a green suite, and a reader + // would have got ` - ` with nothing after the dash: indistinguishable + // from a rendering bug, from a finding with nothing to say, and from a + // stray newline. This test names that case and nothing else. + let disagreements = [ + Disagreement::OnlineProcessors { + parsed: 12, + counter: 16, + }, + Disagreement::ProcessorGroups { + parsed: 1, + counter: 2, + }, + Disagreement::HighestNumaNode { + parsed: Some(2), + counter: 3, + }, + Disagreement::HighestNumaNode { + parsed: None, + counter: 3, + }, + ]; + for entry in &disagreements { + let text = described(entry); + assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); + assert!(!text.trim().is_empty(), "{entry:?} renders blank"); + } + + // Exhaustive, so a seventh `NotCompared` must describe itself to compile. + for entry in [ + NotCompared::MachineChanged, + NotCompared::BracketNotEstablished, + NotCompared::CountsIncludeUnparsedRelations, + NotCompared::ActiveProcessorCountFailed, + NotCompared::ActiveProcessorGroupCountFailed, + NotCompared::HighestNumaNodeFailed, + ] { + let text = described(&entry); + assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); + assert!(!text.trim().is_empty(), "{entry:?} renders blank"); + } + + for entry in [ + ParseIncomplete::ContradictoryCores { count: 3 }, + ParseIncomplete::NoPackages, + ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1, 2] }, + ParseIncomplete::MeasuredButCountsAbsent { + absent: vec!["packages"], + }, + ParseIncomplete::PartitioningSummaryMissing { level: 3 }, + ParseIncomplete::RelationsWithoutProcessors { + cores: 4, + packages: 7, + }, + ParseIncomplete::EnumerationsDisagreed { + attempts: 2, + walk_only: 5, + cpu_sets_only: 9, + }, + ] { + let text = described(&entry); + assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); + assert!(!text.trim().is_empty(), "{entry:?} renders blank"); + } +} + +#[test] +fn an_entry_that_says_nothing_is_called_out_rather_than_left_blank() { + // The other half, and without it the test above cannot distinguish a + // working `described` from one that returns its input unchanged. + struct Silent; + impl std::fmt::Display for Silent { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } + } + + struct Blank; + impl std::fmt::Display for Blank { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Whitespace, not emptiness: a reader cannot tell the two apart on + // the page, so neither may the check. + f.write_str(" ") + } + } + + assert_eq!(described(&Silent), UNDESCRIBED); + assert_eq!(described(&Blank), UNDESCRIBED); + assert_eq!(described(&"a real description"), "a real description"); +} diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index 10aa9af5f..5379d1346 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -67,7 +67,7 @@ pub const MEASURED_ROW_KEYS: &[&str] = &[ /// and a survey must be able to tell it from a measured row that happens to be /// missing fields. pub const UNMEASURED_ROW_KEYS: &[&str] = &["reason", "arch", "cross_check", "discovery_error"]; -use crate::topology::diagnostic::published_anomaly; +use crate::topology::diagnostic::{described, published_anomaly}; use crate::topology::{ Disagreement, NotCompared, Observation, ParseIncomplete, PartitioningCache, Verdict, }; @@ -669,15 +669,15 @@ pub fn report(banner: &str, observation: &Observation) -> String { Verdict::Disagree => { let _ = writeln!(out, " => DISAGREE. This is a finding, not a nuisance:"); for complaint in &check.disagreements { - let _ = writeln!(out, " - {complaint}"); + let _ = writeln!(out, " - {}", described(complaint)); } // Listed even here, so a reader knows the disagreement above is not // the whole picture. for skipped in &check.not_compared { - let _ = writeln!(out, " (not compared) {skipped}"); + let _ = writeln!(out, " (not compared) {}", described(skipped)); } for caveat in &check.parse_incomplete { - let _ = writeln!(out, " (parse incomplete) {caveat}"); + let _ = writeln!(out, " (parse incomplete) {}", described(caveat)); } } Verdict::Incomplete => { @@ -687,10 +687,10 @@ pub fn report(banner: &str, observation: &Observation) -> String { ); let _ = writeln!(out, " did not establish that the parse is consistent:"); for skipped in &check.not_compared { - let _ = writeln!(out, " - {skipped}"); + let _ = writeln!(out, " - {}", described(skipped)); } for caveat in &check.parse_incomplete { - let _ = writeln!(out, " - {caveat}"); + let _ = writeln!(out, " - {}", described(caveat)); } } } From 7e0c3d56642e4ac7ae746199087ac2d47c69054c Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 20:31:46 -0700 Subject: [PATCH 29/46] test(platform-probes): make two instruments enforce the names they carry Both findings from the Copilot review on PR #89, both verified by execution before being accepted, and both the class this branch keeps meeting: an instrument that asserts less than its name. **publication_holds asked "at least one", not "every state".** It read `blocking_states(o).is_empty() || !published_codes(text).is_empty()`, so an observation in five blocking states satisfied it by publishing ONE code. The test it serves is called every_state_that_blocks_agreement_reaches_the_row. Measured: truncating the row's parse_incomplete to its first entry is caught by three tests -- and by that one not at all, which is the whole point. Those three compare the row against cross_check. This is the only instrument running the OTHER enumeration, from invariant's states INTO the row, so nothing else could expose its weakness. Now per state, via a codes_for schema written down and exhaustive: a new BlockingState that names no code does not compile. The pairing is not derivable from either side -- blocking_states computes states from an observation, the renderer emits codes, and nothing in between already knows the correspondence -- so writing it is what makes the check possible. BracketNotHeld answers to either of two codes because cross_check files a different one depending on how the bracket failed. Sabotage-verified: the truncation now reddens that test, where before it stayed green. Added the reachability guard the reviewer also asked for. A corpus of single-state shapes cannot tell "per state" from "at least one", so the deepest shape is asserted to carry two or more states. It passes today; asserting it stops a future corpus edit from quietly making the rule vacuous again. **ParseIncomplete pinned seven payload shapes and no codes.** The shape argument was right about payloads -- twelve counted variants share one helper -- and wrong about codes, which are per-variant. Twelve variants could be given a wrong code with nothing to notice, since the report corpus builds its expectation through code() and both sides move together. NotCompared already had an exhaustive code golden; this gives ParseIncomplete the same, plus a uniqueness check, because two variants sharing a code is a survey that cannot tell two conditions apart. 227 lib + 9 integration pass on x86_64 and i686. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/topology/diagnostic/tests.rs | 82 +++++++++++++++++++ .../tests/a_real_report_agrees_with_itself.rs | 77 ++++++++++++++++- 2 files changed, 157 insertions(+), 2 deletions(-) diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs index 2c4212c8f..d7c7eaf95 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -151,6 +151,88 @@ fn every_disagreement_publishes_the_pair_it_carries() { ); } +#[test] +fn every_parse_incomplete_variant_has_the_code_the_row_promises() { + // **Every variant, not every payload SHAPE.** The test below covers shapes, + // on the argument that the counted variants share one helper -- true of the + // PAYLOAD and false of the CODE, which is per-variant. So twelve variants + // could be given a wrong code with nothing to notice: the report corpus + // builds its expectation through `code()` itself, so both sides move + // together. Found by a review of the pull request. + // + // Exhaustive, so a variant added without a code here does not compile. The + // shape arguments are `..` because this pins the discriminant only; the + // payloads are the test below. + let golden = |entry: &ParseIncomplete| match entry { + ParseIncomplete::EnumerationAnomalies { .. } => "enumeration_anomalies", + ParseIncomplete::NumaDomainsOnlyInCpuSets { .. } => "numa_domains_only_in_cpu_sets", + ParseIncomplete::NoCacheLevels => "no_cache_levels", + ParseIncomplete::CacheLevelsWithoutPartitions { .. } => "cache_levels_without_partitions", + ParseIncomplete::MeasuredButCountsAbsent { .. } => "measured_but_counts_absent", + ParseIncomplete::NoPackages => "no_packages", + ParseIncomplete::NoCores => "no_cores", + ParseIncomplete::ContradictoryCores { .. } => "contradictory_cores", + ParseIncomplete::UnnumberedCacheLevels { .. } => "unnumbered_cache_levels", + ParseIncomplete::PartitioningSummaryMissing { .. } => "partitioning_summary_missing", + ParseIncomplete::NotMeasured => "not_measured", + ParseIncomplete::RelationsWithoutProcessors { .. } => "relations_without_processors", + ParseIncomplete::UnreportedRelations { .. } => "unreported_relations", + ParseIncomplete::DescribedRelations { .. } => "described_relations", + ParseIncomplete::CoresOnlyInCpuSets { .. } => "cores_only_in_cpu_sets", + ParseIncomplete::OverlappingWalkRelations { .. } => "overlapping_walk_relations", + ParseIncomplete::ProcessorAttributeConflicts { .. } => "processor_attribute_conflicts", + ParseIncomplete::NumaDomainsWithConflictingLabels { .. } => { + "numa_domains_with_conflicting_labels" + } + ParseIncomplete::NumaDomainsUnreported { .. } => "numa_domains_unreported", + ParseIncomplete::EnumerationsDisagreed { .. } => "enumerations_disagreed", + ParseIncomplete::CoherenceNotCollected => "coherence_not_collected", + }; + + let every = [ + ParseIncomplete::EnumerationAnomalies { count: 1 }, + ParseIncomplete::NumaDomainsOnlyInCpuSets { count: 1 }, + ParseIncomplete::NoCacheLevels, + ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1] }, + ParseIncomplete::MeasuredButCountsAbsent { absent: vec!["x"] }, + ParseIncomplete::NoPackages, + ParseIncomplete::NoCores, + ParseIncomplete::ContradictoryCores { count: 1 }, + ParseIncomplete::UnnumberedCacheLevels { count: 1 }, + ParseIncomplete::PartitioningSummaryMissing { level: 1 }, + ParseIncomplete::NotMeasured, + ParseIncomplete::RelationsWithoutProcessors { + cores: 1, + packages: 1, + }, + ParseIncomplete::UnreportedRelations { count: 1 }, + ParseIncomplete::DescribedRelations { count: 1 }, + ParseIncomplete::CoresOnlyInCpuSets { count: 1 }, + ParseIncomplete::OverlappingWalkRelations { count: 1 }, + ParseIncomplete::ProcessorAttributeConflicts { count: 1 }, + ParseIncomplete::NumaDomainsWithConflictingLabels { count: 1 }, + ParseIncomplete::NumaDomainsUnreported { count: 1 }, + ParseIncomplete::EnumerationsDisagreed { + attempts: 1, + walk_only: 1, + cpu_sets_only: 1, + }, + ParseIncomplete::CoherenceNotCollected, + ]; + + let mut seen: Vec<&str> = Vec::new(); + for entry in &every { + let code = entry.code(); + assert_eq!(code, golden(entry), "{entry:?}"); + assert!( + !seen.contains(&code), + "{entry:?}: `{code}` is already another variant's code, so a survey \ + cannot tell the two conditions apart" + ); + seen.push(code); + } +} + #[test] fn every_parse_incomplete_shape_publishes_the_fields_its_variant_carries() { // One instance of each PAYLOAD SHAPE rather than of each variant: the twelve diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 3f223caa0..585ec2c72 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -432,9 +432,54 @@ fn published_codes(text: &str, keys: &[&str]) -> Vec { /// demonstrated the sabotage, never that the accounting REJECTS it. The /// accounting could have been deleted and that test would have stayed green. /// Found by a review. +/// +/// **Per state, not "at least one".** This asked only whether the row published +/// SOME code, which an observation in five blocking states satisfies by +/// publishing one -- so dropping four states' conditions left the rule that +/// names itself `every_state_...` perfectly happy. Measured: truncating the +/// row's `parse_incomplete` to its first entry is caught by three tests that +/// compare the row against `cross_check`, and by this one not at all. That +/// matters because this is the only instrument running the other enumeration -- +/// from `invariant`'s states INTO the row -- so its weakness was invisible to +/// everything else. Found by a review of the pull request. fn publication_holds(observation: &Observation, text: &str) -> bool { - invariant::blocking_states(observation).is_empty() - || !published_codes(text, DIAGNOSTIC_LISTS).is_empty() + let published = published_codes(text, DIAGNOSTIC_LISTS); + invariant::blocking_states(observation) + .into_iter() + .all(|state| { + codes_for(state) + .iter() + .any(|code| published.iter().any(|found| found == code)) + }) +} + +/// The row code(s) that answer `state`. +/// +/// **A schema, written down, and exhaustive so it cannot fall behind.** This is +/// the correspondence the milestone exists to enforce -- a state the invariants +/// know about must reach a survey -- and it is not derivable from either side: +/// `blocking_states` computes states from an observation and the renderer emits +/// codes, with nothing in between that already knows the pairing. Writing it +/// here is what makes the check possible; a new state that names no code fails +/// to compile. +/// +/// `BracketNotHeld` answers to either code because `cross_check` files a +/// different one depending on how the bracket failed, and both are honest +/// reports of the same blocking state. +fn codes_for(state: invariant::BlockingState) -> &'static [&'static str] { + use invariant::BlockingState as State; + match state { + State::PartitioningSummaryMissing => &["partitioning_summary_missing"], + State::EnumerationAnomalies => &["enumeration_anomalies"], + State::NotMeasured => &["not_measured"], + State::NoCacheLevels => &["no_cache_levels"], + State::NoPackages => &["no_packages"], + State::NoCores => &["no_cores"], + State::ContradictoryCore => &["contradictory_cores"], + State::UnnumberedCacheLevel => &["unnumbered_cache_levels"], + State::EnumerationsDisagreed => &["enumerations_disagreed"], + State::BracketNotHeld => &["machine_changed", "bracket_not_established"], + } } #[test] @@ -478,6 +523,34 @@ fn every_state_that_blocks_agreement_reaches_the_row() { } } +#[test] +fn the_corpus_reaches_an_observation_in_several_blocking_states_at_once() { + // **The rule above is per-state, and a corpus of single-state shapes cannot + // tell that apart from "at least one".** Its previous form was satisfied by + // any one published code, and no shape with two states would have exposed + // that -- which is why the reachability is asserted rather than assumed: a + // corpus cannot report the shape it never reaches. + let deepest = shapes() + .into_iter() + .map(|shape| { + ( + shape.what, + invariant::blocking_states(&shape.observation).len(), + ) + }) + .max_by_key(|(_, states)| *states) + .expect("the corpus is not empty"); + + assert!( + deepest.1 >= 2, + "the deepest shape in the corpus is `{}` with {} blocking state(s), so \ + the per-state rule is never asked to distinguish one state from \ + several", + deepest.0, + deepest.1, + ); +} + #[test] fn the_row_lists_exactly_the_conditions_the_cross_check_found() { // **The rule that replaced a prose count, and the last prose parsing in the From 3135c09ab9aa2f577471ab0f5f192473213c2968 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 20:53:46 -0700 Subject: [PATCH 30/46] test(ioring): escalate the in-flight handover width instead of only retrying The anti-vacuity guard in attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing failed CI on a shared runner while passing on an idle machine, blocking an unrelated PR. Fixed so it stops flaking, rather than only explaining itself better. The mechanism is not the obvious one. Eight 1 MiB unbuffered reads cannot finish inside the ~6us an attach takes -- measured here, with ZERO of eight landed at every attempt. What defeats the test is a descheduled thread: lose the quantum between submit and completion_event and the reads get milliseconds to finish. The previous defence was four attempts at one width, which is four flips of the same coin; four unlucky ones in a row happened. So escalate instead of repeating. Width now runs 8, 16, 32, 64 reads with four attempts each, stopping at the first width that catches one in flight. Widening multiplies the stall required to beat it: to degenerate at 64 the thread must stay off-CPU long enough for 64 MiB of unbuffered I/O, where before it needed 8 MiB. Sixteen chances, the last four needing an eight-times longer stall. Measured while forcing every width to run: 0/8 land at width 8, and even at 64 only 6/64 land. The margin is large at every step, so escalation is headroom rather than hope. The common path is unchanged in cost. Width 8 catches on the first attempt on any unloaded machine and the loop exits immediately; the handover suite still runs in 0.04s. The wider flights are only reached by a machine that needs them, and re-read the same fixture (offset wraps) rather than requiring a proportionally larger one. 64 is the ring's submission capacity. The failure message now says what was tried and tells the reader NOT to re-run, since a busy machine is what the escalation already accounts for. Reaching the widest row means either a pathological stall or a device that now outruns one completion_event call, and submit->attach separates those -- the second wants a larger DIRECT_LEN, not more attempts. Both feature shapes built and tested, including the no-threadpool shape whose job failed. Duration and Instant are imported in this file under cfg(feature = "threadpool"), so the new code uses fully-qualified paths; adding them to that import compiles locally and breaks exactly the shape being fixed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-ioring-sys/tests/handover.rs | 186 +++++++++++++++----- 1 file changed, 138 insertions(+), 48 deletions(-) diff --git a/crates/windows-ioring-sys/tests/handover.rs b/crates/windows-ioring-sys/tests/handover.rs index 40baf6935..c77ece283 100644 --- a/crates/windows-ioring-sys/tests/handover.rs +++ b/crates/windows-ioring-sys/tests/handover.rs @@ -82,12 +82,32 @@ type Pending> = HashMap>; const DIRECT_OPS: usize = 8; const DIRECT_LEN: usize = 1024 * 1024; -/// Attempts at catching the unbuffered reads mid-flight. A handful, because -/// each one issues `DIRECT_OPS * DIRECT_LEN` of real device I/O; the test -/// asserts that *at least one* attempt reached the in-flight state rather than -/// requiring every attempt to, so an unlucky one cannot make it flake. +/// Attempts at catching the unbuffered reads mid-flight, at each width in +/// [`DIRECT_WIDTHS`]. The test asserts that *at least one* attempt reached the +/// in-flight state rather than requiring every attempt to, so an unlucky one +/// cannot make it flake. const DIRECT_ATTEMPTS: usize = 4; +/// How many reads to have in flight, escalating until one attempt catches them. +/// +/// **Four attempts at one width was not enough, and the reason is worth stating +/// because it is not the obvious one.** Eight 1 MiB unbuffered reads cannot +/// finish inside the ~6us an attach takes on an idle machine -- measured, with +/// zero of eight landed. What defeats the test is not a fast device but a +/// DESCHEDULED THREAD: if this thread loses its quantum between `submit` and +/// `completion_event`, the reads have milliseconds to finish and the attempt +/// degenerates. A shared CI runner does that occasionally, and four attempts in +/// a row were unlucky once. +/// +/// More attempts at the same width only buys more coin flips against the same +/// coin. Widening the flight buys HEADROOM: at 64 reads the device has eight +/// times the work to get through, so a stall has to be eight times longer to +/// beat it. So escalate rather than merely repeat, and stop at the first width +/// that works -- the common case still costs one attempt at eight. +/// +/// Bounded by the ring, which is created with 64 submission entries. +const DIRECT_WIDTHS: [usize; 4] = [8, 16, 32, 64]; + /// `FILE_FLAG_NO_BUFFERING` requires the buffer address, the file offset, and /// the length to be sector-aligned. 4096 satisfies both 512e and 4Kn devices. const ALIGN: usize = 4096; @@ -429,60 +449,130 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { let handle = file.as_raw_handle(); let mut caught_in_flight = false; - - for attempt in 0..DIRECT_ATTEMPTS { - let mut ring = IoRing::new(64, 64).expect("create ring"); - let mut contract = RingContract::new(); - let mut pending: Pending = Pending::new(); - - { - let mut batch = Batch::new(&mut ring); - for index in 0..DIRECT_OPS { - let buffer = Aligned::new(DIRECT_LEN); - let offset = (index * DIRECT_LEN) as u64; - // SAFETY: `file` outlives every operation queued here -- this - // attempt drains to completion before the next one starts, and - // the handle lives for the whole test. - let token = unsafe { batch.read_raw(handle, buffer, offset, PushOptions::new()) } - .expect("queue unbuffered read"); - contract.observe_push(token.id()); - pending.insert(token.id(), token); + // **What each attempt observed, kept so a failure can say WHY it + // degenerated rather than only THAT it did.** + // + // This guard fires when every read landed before the completion event was + // attached, and the interesting question is then which side moved: did the + // reads get faster, or did the attach get slower? Observed once in CI on a + // shared runner while passing on an idle developer machine, where the answer + // could not be recovered from the failure message at all -- it reported the + // conclusion and none of the evidence, so the only way to investigate was to + // re-run and hope. + // + // `submit_to_attach` is the number that usually settles it. It spans exactly + // the window this test depends on: the reads are in flight for it, so a + // large value means the attach was starved rather than the device being + // quick. + let mut trace: Vec<( + usize, + usize, + usize, + std::time::Duration, + std::time::Duration, + )> = Vec::new(); + + 'widths: for width in DIRECT_WIDTHS { + for attempt in 0..DIRECT_ATTEMPTS { + let mut ring = IoRing::new(64, 64).expect("create ring"); + let mut contract = RingContract::new(); + let mut pending: Pending = Pending::new(); + + { + let mut batch = Batch::new(&mut ring); + for index in 0..width { + let buffer = Aligned::new(DIRECT_LEN); + // Wraps, so a wider flight re-reads the fixture rather than + // needing a proportionally larger one. This test cares only + // that the reads are real and outstanding, never what they + // return. + let offset = ((index % DIRECT_OPS) * DIRECT_LEN) as u64; + // SAFETY: `file` outlives every operation queued here -- this + // attempt drains to completion before the next one starts, and + // the handle lives for the whole test. + let token = + unsafe { batch.read_raw(handle, buffer, offset, PushOptions::new()) } + .expect("queue unbuffered read"); + contract.observe_push(token.id()); + pending.insert(token.id(), token); + } + batch.submit_and_wait(0, 0).expect("submit without waiting"); } - batch.submit_and_wait(0, 0).expect("submit without waiting"); - } + // Started the instant the reads are in flight, so it measures the window + // this test's precondition actually depends on. + let submitted = std::time::Instant::now(); - let event = ring.completion_event().expect( + let event = ring.completion_event().expect( "this host must report IORING_FEATURE_SET_COMPLETION_EVENT to run the handover tests", ); + let attached = std::time::Instant::now(); + + // Non-blocking, so this measures what the attach actually found rather + // than waiting for a state to develop. + let already_queued = drain_to_empty(&mut ring, &mut contract, &mut pending); + trace.push(( + width, + attempt, + already_queued, + attached.duration_since(submitted), + attached.elapsed(), + )); + if already_queued < width { + caught_in_flight = true; + } - // Non-blocking, so this measures what the attach actually found rather - // than waiting for a state to develop. - let already_queued = drain_to_empty(&mut ring, &mut contract, &mut pending); - if already_queued < DIRECT_OPS { - caught_in_flight = true; + wait_and_drain( + &mut ring, + &event, + &mut contract, + &mut pending, + width - already_queued, + &format!( + "width {width}, attempt {attempt}, {already_queued} already queued at attach" + ), + ); + + assert!( + pending.is_empty(), + "width {width}, attempt {attempt}: a token was never claimed" + ); + contract.assert_quiescent(); + + // The state was reached, and every assertion above has now run + // against it. Wider flights would only cost device I/O to + // re-establish what this one already showed. + if caught_in_flight { + break 'widths; + } } - - wait_and_drain( - &mut ring, - &event, - &mut contract, - &mut pending, - DIRECT_OPS - already_queued, - &format!("attempt {attempt}, {already_queued} already queued at attach"), - ); - - assert!( - pending.is_empty(), - "attempt {attempt}: a token was never claimed" - ); - contract.assert_quiescent(); } + let observed = trace + .iter() + .map(|(width, attempt, queued, to_attach, to_drained)| { + format!( + " {width} reads, attempt {attempt}: {queued}/{width} already queued at attach; \ + submit->attach {to_attach:?}, attach->drained {to_drained:?}" + ) + }) + .collect::>() + .join("\n"); + let widest = DIRECT_WIDTHS[DIRECT_WIDTHS.len() - 1]; + assert!( caught_in_flight, - "no attempt caught a read in flight: every unbuffered read had already completed by the \ - time the event was attached, so this test degenerated into the already-queued case and \ - is no longer covering what it claims" + "no attempt caught a read in flight at ANY width: every unbuffered read had already \ + completed by the time the event was attached, so this test degenerated into the \ + already-queued case and is no longer covering what it claims.\n\n\ + Escalated to {widest} reads of {DIRECT_LEN} bytes, {DIRECT_ATTEMPTS} attempt(s) per \ + width:\n{observed}\n\n\ + **Do not simply re-run.** A busy machine is already accounted for -- that is what the \ + escalation is for, since widening the flight multiplies the stall needed to beat it. \ + Reaching the widest row above means the stall outlasted {widest} reads, which a shared \ + runner does not usually manage, or the device now resolves them faster than one \ + `completion_event` call. Read `submit->attach`: large says the ATTACH was starved and \ + this host is pathologically loaded; small says the DEVICE won, and the fix is a larger \ + `DIRECT_LEN` rather than more attempts." ); drop(file); From 3ed2723decb2dd3ad46378e837a563c1e55a7d76 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 21:11:51 -0700 Subject: [PATCH 31/46] fix(platform-probes): stop conflating uncollected coherence with disagreement A real defect, in code written an hour ago, found by the Copilot review on PR #89 and proven before being accepted. `blocking_states` read `if !matches!(coherence, Coherence::Agreed)` and filed BOTH remaining variants as `EnumerationsDisagreed`. That is true of the verdict -- neither may agree -- and false of the row, which publishes `enumerations_disagreed` for `Disagreed` and `coherence_not_collected` for `NotCollected`. So the per-state publication rule added earlier today demanded, for an uncollected observation, a code the renderer never emits for it. Latent only because the corpus never reached it: it carried `Agreed` and `Disagreed` and no `NotCollected`. Adding that shape turns the rule red immediately -- coherence that was never collected: the observation is in 1 state(s) that forbid agreement -- [EnumerationsDisagreed] -- and the row publishes no condition at all -- which is both the proof and the regression test. It is also the vacuity the same review warned about separately: a per-state rule proves only that a mapping was written until a shape exercises it. Fixed by splitting the state. `CoherenceNotCollected` is its own `BlockingState` mapped to its own code, the perturbation table keeps a distinct entry for each `Coherence` variant so neither code goes unexercised, and the branch is now a `match` on the variant rather than a `!= Agreed` test. `Coherence` is not `#[non_exhaustive]`, so that match is compiler-exhaustive: a fourth variant cannot be silently folded into whichever arm is nearest, which is precisely what happened here. Also corrected the failure message, which the run above showed saying "the row publishes no condition at all" when the row HAD published one -- just not the one the failing state maps to. A reader debugging a wrong mapping was told the opposite of the symptom. It now names the state, the codes it wanted, and what was actually published. 227 lib + 9 integration pass on x86_64 and i686. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/topology/invariant.rs | 22 ++++++++++-- .../src/topology/invariant/tests.rs | 14 ++++++++ .../tests/a_real_report_agrees_with_itself.rs | 35 +++++++++++++++++-- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 4992e2a7b..e108eed3a 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -140,6 +140,16 @@ blocking_states! { UnnumberedCacheLevel => "a cache level is numbered 0", /// The crate's two enumerations did not agree. EnumerationsDisagreed => "the crate's two enumerations did not agree", + /// Coherence between the two enumerations was never established. + /// + /// **Distinct from `EnumerationsDisagreed`, and the separation is load + /// bearing.** One branch covered both, on the reading that anything other + /// than `Agreed` forbids agreement -- true of the VERDICT and false of the + /// ROW, which publishes `coherence_not_collected` here and + /// `enumerations_disagreed` there. A state that names the wrong code makes + /// the per-state publication rule demand something the renderer never emits. + /// Latent until a corpus shape reached it. Found by a review. + CoherenceNotCollected => "coherence between the two enumerations was not collected", /// The bracket did not establish that the machine held still. BracketNotHeld => "the bracket did not establish that the machine held still", } @@ -281,8 +291,16 @@ pub fn blocking_states(observation: &Observation) -> Vec { states.push(BlockingState::UnnumberedCacheLevel); } - if !matches!(observation.coherence, Coherence::Agreed) { - states.push(BlockingState::EnumerationsDisagreed); + // **Matched by variant rather than by `!= Agreed`**, because `cross_check` + // files these under different codes and a state must name the code its own + // condition emits. `Coherence` is not `#[non_exhaustive]`, so this match is + // compiler-exhaustive and a fourth variant cannot be silently folded into + // whichever arm happens to be nearest -- which is what the `!= Agreed` form + // did to `NotCollected`. + match observation.coherence { + Coherence::Agreed => {} + Coherence::Disagreed { .. } => states.push(BlockingState::EnumerationsDisagreed), + Coherence::NotCollected => states.push(BlockingState::CoherenceNotCollected), } if observation.bracket != super::BracketOutcome::HeldStill { diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index e94e3e516..6d537876c 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -151,6 +151,20 @@ fn perturbations() -> Vec { ), ( BlockingState::EnumerationsDisagreed, + Box::new(|o: &mut Observation| { + o.coherence = windows_topology_sys::Coherence::Disagreed { + attempts: 2, + walk_only: Vec::new(), + cpu_sets_only: Vec::new(), + }; + }), + ), + ( + // Kept distinct from the `Disagreed` row above, because the row + // publishes a different code for each and a shared perturbation + // would leave one of the two codes unexercised -- which is exactly + // how the states came to be conflated. + BlockingState::CoherenceNotCollected, Box::new(|o: &mut Observation| { o.coherence = windows_topology_sys::Coherence::NotCollected; }), diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 585ec2c72..5f2ebe0d5 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -304,6 +304,17 @@ fn shapes() -> Vec { }; push("two sources that described different processors", disagreed); + // **The shape whose absence hid a wrong state-to-code mapping.** The corpus + // carried `Agreed` and `Disagreed` and never `NotCollected`, and + // `blocking_states` lumped the latter two together as + // `EnumerationsDisagreed` -- while `cross_check` publishes + // `coherence_not_collected` for it. The per-state rule therefore demanded a + // code the row does not emit for this observation, and nothing noticed, + // because no shape reached it. Found by a review. + let mut uncollected = base(); + uncollected.coherence = windows_topology_sys::Coherence::NotCollected; + push("coherence that was never collected", uncollected); + let mut anomalies = base(); anomalies.enumeration_anomalies = vec![ windows_topology_sys::EnumerationAnomaly { @@ -478,6 +489,7 @@ fn codes_for(state: invariant::BlockingState) -> &'static [&'static str] { State::ContradictoryCore => &["contradictory_cores"], State::UnnumberedCacheLevel => &["unnumbered_cache_levels"], State::EnumerationsDisagreed => &["enumerations_disagreed"], + State::CoherenceNotCollected => &["coherence_not_collected"], State::BracketNotHeld => &["machine_changed", "bracket_not_established"], } } @@ -511,14 +523,31 @@ fn every_state_that_blocks_agreement_reaches_the_row() { continue; } + // **Names the state whose code is missing, and what was published + // instead.** The message used to say "publishes no condition at all", + // which was wrong in the case that actually fired: the row DID publish a + // condition, just not the one the failing state maps to. A reader + // debugging a wrong mapping was told the opposite of the symptom. + let published = published_codes(&text, DIAGNOSTIC_LISTS); + let unpublished: Vec = blocking + .iter() + .filter(|state| { + !codes_for(**state) + .iter() + .any(|code| published.iter().any(|found| found == code)) + }) + .map(|state| format!("{state:?} (wants one of {:?})", codes_for(*state))) + .collect(); + assert!( publication_holds(&shape.observation, &text), "{}: the observation is in {} state(s) that forbid agreement -- \ - {blocking:?} -- and the row publishes no condition at all. A \ - survey reading it would see a verdict it cannot account \ - for.\n\n--- the report ---\n{text}", + {blocking:?} -- and the row publishes no code for {}. Published: \ + {published:?}. A survey reading it would see a verdict it cannot \ + account for.\n\n--- the report ---\n{text}", shape.what, blocking.len(), + unpublished.join(", "), ); } } From e0a85acdd0c615860f3856c2a9c0dfe493563ab1 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 21:23:06 -0700 Subject: [PATCH 32/46] test: close the coverage and measurement gaps the latest review found Six findings from the Copilot review on PR #89. Its Coherence items were already fixed in 3ed2723; these are the rest, each verified before being accepted. **Two defects in the tracing added to diagnose the CI flake**, which is the worst place to have them -- an instrument that misreports is worse than none, because it is consulted exactly when nobody can check it. - `submitted` was captured AFTER `submit_and_wait` returned, so the span labelled `submit->attach` omitted the submission phase. The reads begin executing inside that call, so a read that finished during it was invisible to the number meant to explain why reads had finished. Now started before the submit and labelled `submit+attach`. - `attach->drained` was sampled before `wait_and_drain` ran, so it never contained the drain it named. It measures the non-blocking poll, and is now called `attach->polled`. A reader chasing a slow drain would have been handed a number that could not contain one. **The corpus declared three blocking states it never rendered.** `codes_for` is exhaustive, so every state names a code -- but an arm whose state no shape produces is never held against a real row. Added `the_corpus_reaches_every_blocking_state`, which named NoPackages, NoCores and UnnumberedCacheLevel, and added a shape for each. This is the same gap that let CoherenceNotCollected carry the wrong code until a shape finally reached it: an exhaustive mapping proves it was WRITTEN, never that it was EXERCISED. **every_diagnostic_describes_itself instantiated 7 of 21 variants**, so it checked Display for a third of the enum while claiming all of it. The exhaustive list from the code test is now a shared fixture both use, so a new variant must be exercised rather than merely given a golden. Two hand-written lists were two chances to omit one. **A census I wrote, wrong, twice.** Two comments said "twelve counted variants"; there are eleven. Removed rather than corrected -- the shape argument does not depend on the number, so stating it buys nothing and rots. That this landed in the tests written to stop exactly this is the point. **The oracle called the row a "flat JSON object".** It has not been flat since it began publishing diagnostics: caches, policies and the three diagnostic lists are nested. A reader who believed it would have taken the nested data for a defect. 227 lib + 10 integration pass on x86_64 and i686; ioring passes in the no-threadpool shape. Clippy caught an Option whose initial value nothing read, in the timing fix above; replaced with deferred initialization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-ioring-sys/tests/handover.rs | 26 +++- .../src/report_oracle.rs | 7 +- .../src/topology/diagnostic/tests.rs | 119 ++++++++++-------- .../tests/a_real_report_agrees_with_itself.rs | 48 +++++++ 4 files changed, 139 insertions(+), 61 deletions(-) diff --git a/crates/windows-ioring-sys/tests/handover.rs b/crates/windows-ioring-sys/tests/handover.rs index c77ece283..b671a7e68 100644 --- a/crates/windows-ioring-sys/tests/handover.rs +++ b/crates/windows-ioring-sys/tests/handover.rs @@ -477,6 +477,10 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { let mut ring = IoRing::new(64, 64).expect("create ring"); let mut contract = RingContract::new(); let mut pending: Pending = Pending::new(); + // Assigned once inside the block below and read after it, so no + // initial value is needed -- and giving it one would be a value + // nothing reads. + let submitted; { let mut batch = Batch::new(&mut ring); @@ -496,11 +500,15 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { contract.observe_push(token.id()); pending.insert(token.id(), token); } + // **Started BEFORE the submit, not after it.** The reads begin + // executing inside `submit_and_wait`, so a clock started once it + // returns omits part of the very window the precondition depends + // on -- and a read that finished during the call is invisible to + // it. Measuring from here spans every instant a read could have + // used. + submitted = std::time::Instant::now(); batch.submit_and_wait(0, 0).expect("submit without waiting"); } - // Started the instant the reads are in flight, so it measures the window - // this test's precondition actually depends on. - let submitted = std::time::Instant::now(); let event = ring.completion_event().expect( "this host must report IORING_FEATURE_SET_COMPLETION_EVENT to run the handover tests", @@ -515,6 +523,11 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { attempt, already_queued, attached.duration_since(submitted), + // The NON-BLOCKING poll only. `wait_and_drain` runs after this + // and is not included, so the label says `polled` rather than + // `drained`: the earlier name claimed the whole drain, and a + // reader chasing a slow one would have been misled by a number + // that never contained it. attached.elapsed(), )); if already_queued < width { @@ -549,10 +562,10 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { let observed = trace .iter() - .map(|(width, attempt, queued, to_attach, to_drained)| { + .map(|(width, attempt, queued, to_attach, to_polled)| { format!( " {width} reads, attempt {attempt}: {queued}/{width} already queued at attach; \ - submit->attach {to_attach:?}, attach->drained {to_drained:?}" + submit+attach {to_attach:?}, attach->polled {to_polled:?}" ) }) .collect::>() @@ -570,7 +583,8 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { escalation is for, since widening the flight multiplies the stall needed to beat it. \ Reaching the widest row above means the stall outlasted {widest} reads, which a shared \ runner does not usually manage, or the device now resolves them faster than one \ - `completion_event` call. Read `submit->attach`: large says the ATTACH was starved and \ + `completion_event` call. Read `submit+attach`, which spans from just before the \ + submit to the completed attach: large says the ATTACH was starved and \ this host is pathologically loaded; small says the DEVICE won, and the fix is a larger \ `DIRECT_LEN` rather than more attempts." ); diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index a25593f24..d9718c71d 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -32,8 +32,11 @@ //! which is the stronger move. What structure cannot check is **the writer** -- //! whatever turns values into bytes is downstream of every type, and several of //! this crate's defects lived exactly there. So one check survives: the report -//! carries exactly one machine-readable row, and that row is a well-formed flat -//! JSON object. +//! carries exactly one machine-readable row, and that row is a well-formed JSON +//! object. **Not flat** -- an earlier version of this sentence said flat, which +//! the row has not been since it began publishing diagnostics: `caches`, +//! `policies` and the three diagnostic lists are nested arrays and objects. A +//! reader who believed it would have taken the nested data for a defect. //! //! That is not a correspondence. It is the writer's own output being read back, //! which is the one thing no amount of typing upstream can do for itself. diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs index d7c7eaf95..c902bf07e 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -151,11 +151,54 @@ fn every_disagreement_publishes_the_pair_it_carries() { ); } +/// Every `ParseIncomplete` variant, one instance each. +/// +/// **Shared, because two tests need the same completeness and a second +/// hand-written list is a second chance to omit a variant.** The presence test +/// carried its own seven-element sample and so exercised `Display` for a third +/// of the enum; blanking any omitted arm would have rendered `UNDESCRIBED` in a +/// real report while that test stayed green. Found by a review. +/// +/// The exhaustive `match` in the code test forces a new variant to acquire a +/// golden; this forces it to be EXERCISED. Neither implies the other, which is +/// why both exist. +fn every_parse_incomplete() -> Vec { + vec![ + ParseIncomplete::EnumerationAnomalies { count: 1 }, + ParseIncomplete::NumaDomainsOnlyInCpuSets { count: 1 }, + ParseIncomplete::NoCacheLevels, + ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1] }, + ParseIncomplete::MeasuredButCountsAbsent { absent: vec!["x"] }, + ParseIncomplete::NoPackages, + ParseIncomplete::NoCores, + ParseIncomplete::ContradictoryCores { count: 1 }, + ParseIncomplete::UnnumberedCacheLevels { count: 1 }, + ParseIncomplete::PartitioningSummaryMissing { level: 1 }, + ParseIncomplete::NotMeasured, + ParseIncomplete::RelationsWithoutProcessors { + cores: 1, + packages: 1, + }, + ParseIncomplete::UnreportedRelations { count: 1 }, + ParseIncomplete::DescribedRelations { count: 1 }, + ParseIncomplete::CoresOnlyInCpuSets { count: 1 }, + ParseIncomplete::OverlappingWalkRelations { count: 1 }, + ParseIncomplete::ProcessorAttributeConflicts { count: 1 }, + ParseIncomplete::NumaDomainsWithConflictingLabels { count: 1 }, + ParseIncomplete::NumaDomainsUnreported { count: 1 }, + ParseIncomplete::EnumerationsDisagreed { + attempts: 1, + walk_only: 1, + cpu_sets_only: 1, + }, + ParseIncomplete::CoherenceNotCollected, + ] +} #[test] fn every_parse_incomplete_variant_has_the_code_the_row_promises() { // **Every variant, not every payload SHAPE.** The test below covers shapes, // on the argument that the counted variants share one helper -- true of the - // PAYLOAD and false of the CODE, which is per-variant. So twelve variants + // PAYLOAD and false of the CODE, which is per-variant. So the counted variants // could be given a wrong code with nothing to notice: the report corpus // builds its expectation through `code()` itself, so both sides move // together. Found by a review of the pull request. @@ -189,36 +232,7 @@ fn every_parse_incomplete_variant_has_the_code_the_row_promises() { ParseIncomplete::CoherenceNotCollected => "coherence_not_collected", }; - let every = [ - ParseIncomplete::EnumerationAnomalies { count: 1 }, - ParseIncomplete::NumaDomainsOnlyInCpuSets { count: 1 }, - ParseIncomplete::NoCacheLevels, - ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1] }, - ParseIncomplete::MeasuredButCountsAbsent { absent: vec!["x"] }, - ParseIncomplete::NoPackages, - ParseIncomplete::NoCores, - ParseIncomplete::ContradictoryCores { count: 1 }, - ParseIncomplete::UnnumberedCacheLevels { count: 1 }, - ParseIncomplete::PartitioningSummaryMissing { level: 1 }, - ParseIncomplete::NotMeasured, - ParseIncomplete::RelationsWithoutProcessors { - cores: 1, - packages: 1, - }, - ParseIncomplete::UnreportedRelations { count: 1 }, - ParseIncomplete::DescribedRelations { count: 1 }, - ParseIncomplete::CoresOnlyInCpuSets { count: 1 }, - ParseIncomplete::OverlappingWalkRelations { count: 1 }, - ParseIncomplete::ProcessorAttributeConflicts { count: 1 }, - ParseIncomplete::NumaDomainsWithConflictingLabels { count: 1 }, - ParseIncomplete::NumaDomainsUnreported { count: 1 }, - ParseIncomplete::EnumerationsDisagreed { - attempts: 1, - walk_only: 1, - cpu_sets_only: 1, - }, - ParseIncomplete::CoherenceNotCollected, - ]; + let every = every_parse_incomplete(); let mut seen: Vec<&str> = Vec::new(); for entry in &every { @@ -235,9 +249,14 @@ fn every_parse_incomplete_variant_has_the_code_the_row_promises() { #[test] fn every_parse_incomplete_shape_publishes_the_fields_its_variant_carries() { - // One instance of each PAYLOAD SHAPE rather than of each variant: the twelve - // counted variants share a single helper, and it was rewriting every one of - // them wrongly that left 218 tests green. + // One instance of each PAYLOAD SHAPE rather than of each variant: the + // count-carrying variants all share a single helper, and it was rewriting + // every one of them wrongly that left 218 tests green. + // + // No number here on purpose. Two comments in this file said "twelve counted + // variants" and there are eleven -- a census, wrong, in the tests written to + // stop exactly that. The shape argument does not depend on how many there + // are, so stating it buys nothing and rots. let cases = [ ( ParseIncomplete::ContradictoryCores { count: 3 }, @@ -463,25 +482,19 @@ fn every_diagnostic_describes_itself() { assert!(!text.trim().is_empty(), "{entry:?} renders blank"); } - for entry in [ - ParseIncomplete::ContradictoryCores { count: 3 }, - ParseIncomplete::NoPackages, - ParseIncomplete::CacheLevelsWithoutPartitions { levels: vec![1, 2] }, - ParseIncomplete::MeasuredButCountsAbsent { - absent: vec!["packages"], - }, - ParseIncomplete::PartitioningSummaryMissing { level: 3 }, - ParseIncomplete::RelationsWithoutProcessors { - cores: 4, - packages: 7, - }, - ParseIncomplete::EnumerationsDisagreed { - attempts: 2, - walk_only: 5, - cpu_sets_only: 9, - }, - ] { - let text = described(&entry); + // **Every variant, from the shared fixture.** This carried its own + // seven-element sample, so it exercised `Display` for a third of the enum -- + // `NoCacheLevels`, `EnumerationAnomalies`, `NoCores` and the rest were never + // rendered here, and blanking any of their arms would have put UNDESCRIBED + // in a real report while this test stayed green. Found by a review. + let every = every_parse_incomplete(); + assert!( + every.len() > 15, + "the fixture should carry the whole enum, not a sample: {}", + every.len() + ); + for entry in &every { + let text = described(entry); assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); assert!(!text.trim().is_empty(), "{entry:?} renders blank"); } diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 5f2ebe0d5..2a5bc1587 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -315,6 +315,25 @@ fn shapes() -> Vec { uncollected.coherence = windows_topology_sys::Coherence::NotCollected; push("coherence that was never collected", uncollected); + // **The three states the corpus declared and never rendered.** `codes_for` + // named a code for each, and no shape produced any of them, so those arms + // were never held against a real row -- the same gap that let + // `CoherenceNotCollected` carry the wrong code. Found by a review. + let mut no_packages = base(); + no_packages.packages = 0; + push("processors reported, no packages", no_packages); + + let mut no_cores = base(); + no_cores.cores = Vec::new(); + push("processors reported, no cores", no_cores); + + let mut unnumbered = base(); + unnumbered.caches = vec![CacheLevel { + level: 0, + processors_per_domain: vec![4], + }]; + push("a cache level Windows does not number", unnumbered); + let mut anomalies = base(); anomalies.enumeration_anomalies = vec![ windows_topology_sys::EnumerationAnomaly { @@ -552,6 +571,35 @@ fn every_state_that_blocks_agreement_reaches_the_row() { } } +#[test] +fn the_corpus_reaches_every_blocking_state() { + // **The per-state rule proves a mapping was WRITTEN; this proves it was + // EXERCISED.** `codes_for` is exhaustive, so every state names a code -- but + // an arm whose state no shape produces is never compared against a rendered + // row, and a wrong code there sits undetected. That is not hypothetical: it + // is exactly how `CoherenceNotCollected` came to be mapped to + // `enumerations_disagreed`, caught only when a shape finally reached it. + // + // Found by a review, which observed that this corpus omitted several + // declared states outright. + let mut unreached: Vec = Vec::new(); + for state in invariant::BlockingState::ALL { + let reached = shapes() + .iter() + .any(|shape| invariant::blocking_states(&shape.observation).contains(state)); + if !reached { + unreached.push(format!("{state:?}")); + } + } + + assert!( + unreached.is_empty(), + "no corpus shape reaches {}, so `codes_for`'s arm(s) for them are never \ + held against a rendered row", + unreached.join(", ") + ); +} + #[test] fn the_corpus_reaches_an_observation_in_several_blocking_states_at_once() { // **The rule above is per-state, and a corpus of single-state shapes cannot From c26612974d40cbf0fd3732cdc36e233842617c87 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 21:33:15 -0700 Subject: [PATCH 33/46] fix(platform-probes): define row() by check(), so the two cannot disagree `row` ran its own subset of the oracle -- exactly one row, and `malformation` -- while `check` additionally reports RepeatedKey. Since serde_json accepts a duplicated key, a report that `check` faulted as ambiguous was handed back by `row` as readable. A caller asking "may I read this row" got yes for a row the crate had already judged ambiguous, and a repeated key is precisely the malformation that survives a consumer's parse and changes what it reads. Two implementations of one question, agreeing until they did not -- in the function whose own doc comment warns about that. Found by the Copilot review on PR #89. `row` now defers to `check` and returns the line only when nothing is faulted, so any defect the oracle learns later is covered without touching this. The test is written as the correspondence rather than as the one case: for a table of faulted reports it asserts `check` faults them AND `row` declines them, plus the other direction so the rule is not satisfied by refusing everything. Stating the case would have missed this, because the gap was not in a case anyone wrote down -- it was in the second implementation existing. Sabotage-verified: restoring the old body reddens it with "`check` reports [RepeatedKey { key: "processors" }] and `row` handed the row back anyway". 228 lib + 10 integration pass on x86_64 and i686. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/report_oracle.rs | 19 ++++++-- .../src/report_oracle/tests.rs | 46 +++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index d9718c71d..8d0b52dfd 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -273,11 +273,24 @@ pub fn list_codes(row: &str, key: &str) -> Vec { /// Public because the instruments in `tests/` read the row to ask what it /// publishes, and a second implementation of "which line is the row" is the kind /// of copy that agrees until it does not. +/// +/// **Defined by [`check`], so the accessor and the oracle cannot disagree.** It +/// ran its own subset -- one row, and `malformation` -- and `serde_json` accepts +/// a duplicated key, so a row that `check` reported as +/// [`RowDefect::RepeatedKey`] was handed back here as well-formed. A caller +/// asking "may I read this row" got yes for a row the crate had already judged +/// ambiguous, which is the one malformation that survives a consumer's parse and +/// changes what it reads. Found by a review. +/// +/// This is the same defect the module keeps warning about, in the function whose +/// doc comment warns about it: two implementations of one question, agreeing +/// until they did not. #[must_use] pub fn row(report: &str) -> Option<&str> { - let mut rows = report.lines().filter(|line| line.starts_with('{')); - let row = rows.next()?; - (rows.next().is_none() && malformation(row).is_none()).then_some(row) + if !check(report).is_empty() { + return None; + } + report.lines().find(|line| line.starts_with('{')) } /// [`check`], as an assertion, for tests that render a report. diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index 10ad1c07c..c76e88327 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -237,6 +237,52 @@ fn the_rows_keys_are_read_at_the_top_level_only() { ); } +#[test] +fn the_row_accessor_declines_every_report_the_oracle_faults() { + // **The accessor and the oracle answer one question, so they may not + // disagree.** `row` ran its own subset -- one row, and `malformation` -- and + // `serde_json` accepts a duplicated key, so a report `check` faulted as + // `RepeatedKey` was handed back here as readable. A caller asking "may I + // read this row" got yes for a row already judged ambiguous. + // + // Stated as the correspondence rather than as the one case, because the + // gap was not in the case anyone wrote down: it was in the SECOND + // implementation existing at all. Any future defect `check` learns is + // covered here without a new test. + let faulted = [ + report_with(r#"{"reason":"x","processors":4,"processors":8}"#), + report_with(r#"{"reason":"x","arch":"x86_64",}"#), + report_with(r#"{"reason":"x","arch":"x86_64"]"#), + report_with(r#"{"unclosed":["#), + "a report with no row at all".to_owned(), + format!( + "{}\n{}", + r#"{"reason":"x","arch":"x86_64"}"#, r#"{"reason":"y","arch":"x86"}"# + ), + ]; + + for report in &faulted { + let defects = check(report); + assert!( + !defects.is_empty(), + "the fixture must be faulted for this to mean anything: {report}" + ); + assert_eq!( + row(report), + None, + "`check` reports {defects:?} and `row` handed the row back anyway" + ); + } + + // And the other direction, or the rule is satisfied by refusing everything. + let clean = report_with(&clean_row()); + assert!(check(&clean).is_empty()); + assert!( + row(&clean).is_some(), + "a clean report must still be readable" + ); +} + #[test] fn the_row_accessor_declines_an_ambiguous_or_malformed_report() { let one = report_with(&clean_row()); From 6ef80f7e8c6b252204378df4c2a330d8b47bcdfa Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 22:05:26 -0700 Subject: [PATCH 34/46] fix(platform-probes): oblige every diagnostic variant to be exercised, not merely counted `every_diagnostic_describes_itself` guarded its `ParseIncomplete` fixture with `assert!(every.len() > 15)`. A count proves nothing about membership: a fixture of any size passes while omitting a variant, so a variant added to the enum could go unrendered here with the suite green -- which is the exact hole the shared fixture existed to close. Reported by a review. Swept the class rather than the reported instance, and the two neighbours were worse. The `NotCompared` loop carried the comment "Exhaustive, so a seventh `NotCompared` must describe itself to compile" over an ARRAY LITERAL, which forces nothing at all; `Disagreement` had no completeness check whatever. `code` and a new `ALL_CODES` are now generated from one list per enum. The `match` is exhaustive, so a variant added to the enum does not compile until it has a line there, and that line reaches `ALL_CODES` without anyone remembering -- which is what makes the list trustworthy. A hand-written `ALL_CODES` would have relocated the omission rather than closed it. The goldens in the tests are untouched and remain the independent second statement of the VALUES; this is the single statement of the SET, and the two answer different questions. `covers_every_variant` compares a fixture's codes against `ALL_CODES` in both directions, as a set, since a fixture may legitimately carry two instances of one variant to exercise a payload that differs, as the `Disagreement` one does. It also guards `every_parse_incomplete_variant_has_the_code_the_row_promises`, whose name makes the same claim and whose exhaustive `golden` match did not deliver it: a `match` obliges a variant to HAVE an arm, never obliges a fixture to reach it. Sabotage-verified, both halves of the claim separately: - Fixture omission bites. Dropping `NoCores` fails two tests naming `["no_cores"]`; dropping `HighestNumaNodeFailed` fails naming `["highest_numa_node_failed"]`, so the `NotCompared` call site is wired. - A NEW variant reaches the check on its own, which is the claim that matters and the one a fixture-drop does not test. Adding a variant first failed to compile on the pre-existing `golden` match -- the old guard doing its job, and proof that the compile error masks the new check. Adding the golden arm to satisfy the compiler and leaving the fixture alone then fails at runtime naming `["sabotage_probe"]`. Also from the same review, both verified rather than taken on faith: - The design session claimed the row emits `"efficiency_classes":[{}]`. It emits `[0]` -- the class labels as a list, per the typed writer and its tests. Corrected. - M2.14/.1/.2 were completed multi-line items still in the active checklist. Moved to COMPLETED-CHECKLIST.md under anchored headings with one-line stubs. Archive invariants checked: 87 added / 0 deleted (append-only), no `- [ ]` left, every `###` preceded by a blank line, all three stub anchors resolve, both files 7-bit ASCII. The review's third point, that `row()` accepted a repeated key `check()` rejects, was already fixed by c266129 eight minutes after the review was submitted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 85 +----------- .../COMPLETED-CHECKLIST.md | 87 ++++++++++++ ...-2026-09-12-what-the-oracle-should-read.md | 2 +- .../src/topology/diagnostic.rs | 130 ++++++++++-------- .../src/topology/diagnostic/tests.rs | 85 ++++++++++-- 5 files changed, 240 insertions(+), 149 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 12ad02f53..82d09e88d 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -201,86 +201,11 @@ IDs keep their M2 numbers, for the reason given under M4. append-only invariant, and it is the one a human reviewer is least likely to notice. -- [x] **M2.14** -- Make the two authoring rules this branch earned actually bite. Re-planned - 2026-09-12; see the rationale below before implementing either sub-step. Both sub-steps done. - - **As originally written this item said "write two authoring rules into the repository - instructions". Measurement says that would have been worse than useless.** The two rules it - proposed -- *state the invariant, not the census*, and *a new test is not done until it has been - observed to fail* -- ALREADY EXIST, in - [.github/copilot-instructions.md](../../.github/copilot-instructions.md) under CONTRACT INTEGRITY - rule 1 ("Prefer a derived fact to a restated one", and beneath it "verify the binding by - sabotage: change the definition and confirm the consumer's BEHAVIOR changes"). Writing them again - would add a second copy of a rule, which is the exact defect that section forbids and the exact - mechanism -- restatement drift -- it exists to prevent. - - **And statement is demonstrably not the gap.** Both rules were in force on 2026-09-12 and both - were violated: `09da7e9` claims "Sabotage-verified, EACH against the instrument it was meant to - strengthen" and then names two sabotages for four fixes. The one that got none is the completeness - guard, which a review found broken an hour later (M3.8). A rule cited in the commit that breaks it - will not be repaired by a third copy of itself. - - **This repository has already solved this problem once, and not with a rule.** The - [ci.yml](../../.github/workflows/ci.yml) `sabotage-harness` job records that the harness "accumulated - fixes over eleven review rounds and thirteen of the later defects were introduced by earlier fixes, - because every verification was a one-off command that was then discarded and nothing re-checked an - earlier guarantee." That is M3.8's story verbatim. The answer then was a CI ratchet. - -- [x] **M2.14.1** -- Give `windows-platform-probes` a sabotage manifest, so "observed to fail" is a - recorded artifact rather than a habit. - - **Done.** [sabotage.json](sabotage.json), 7 entries, swept green: six `caught`, one `survives`, - all behaving as declared. Not wired into CI, matching the two sibling manifests and the - `sabotage-harness` job's own note that a sweep "rebuilds a crate per entry and is deliberately an - occasional instrument". - - **The control is the entry that matters most here.** It rewords a prose line to carry the same - fact and must SURVIVE, which turns this component's central decision -- the row is the machine - contract, the prose is for a reader -- from a sentence into a measurement. If it is ever reported - as caught, a test has started reading the prose again and that test is the defect. - - **The harness found a defect in the manifest that the authoring script missed, which is the - lesson.** The entry for the escape-aware key reader anchored on `'\\' => escaped = true,`; the - script checked uniqueness by whole-LINE equality and found one match, while the harness matches by - SUBSTRING and found two -- the same arm appears in `malformation` at a deeper indent, and the - shallower line is a substring of the deeper one. The script's check was a second, weaker - implementation of the harness's rule, which is precisely the defect class this manifest exists to - catch. The anchor was widened to the function signature; the harness remains the only authority on - uniqueness. - - `tools/run-sabotage.ps1` exists, has its own tests, and runs in CI; - [windows-placement-probe](../windows-placement-probe/sabotage.json) (9 entries) and - [windows-waitable-queues](../windows-waitable-queues/sabotage.json) (39 entries) each carry a - `sabotage.json`. **This crate carried none**, so every sabotage run while building M3 was ad-hoc - PowerShell, discarded on the spot -- which is why a `git checkout` destroyed uncommitted work - twice and a `.Replace` pattern silently matched two sites once. The manifest format's `find` must - match EXACTLY ONCE, which is precisely the guard that hand-running lacked. - - Eight commits on this branch recorded their sabotages in the message, so the first pass was - transcription rather than invention: the defect, the file and the test expected to redden were - already written down. - - Include at least one `expect: "survives"` control. A manifest of nothing but `caught` cannot - distinguish a suite that is watching from a suite that fails on any edit. - -- [x] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does - NOT already say, and a pointer to the mechanism. A pointer, not a restatement. - - **Done.** Two paragraphs added to CONTRACT INTEGRITY rule 1 in - [.github/copilot-instructions.md](../../.github/copilot-instructions.md). Neither restates the - existing rule: the first says to sabotage the CLAIM a change makes rather than the symptom it - cites, and the second says a sabotage is worth nothing once discarded and points at the manifest - and the harness. The original M2.14 wording is nowhere in the diff, which was the point. - - The genuinely new fact: **when a fix claims to have removed a weakness, sabotage the claim rather - than the symptom.** Rule 1 tells an author to prefer a derived fact over a restated one; it does - not warn that an author may believe they derived one when they only MOVED the census. That is - exactly what happened three times on a single guard -- strings, then a hand-written `ALL`, then - generation -- each fix relocating the census somewhere harder to see while its commit message - claimed the class was closed. The wording that would have caught it is about the CLAIM, and rule 1 - currently has no sentence about claims. - - > **-> DEPENDS ON M2.14.1:** the pointer has nothing to point at until the manifest exists. +- [x] **M2.14** -- Make the two authoring rules this branch earned actually bite. -> [completed 2026-09-13](COMPLETED-CHECKLIST.md#m214) + +- [x] **M2.14.1** -- Give this crate a sabotage manifest, so "observed to fail" is a recorded artifact rather than a habit. -> [completed 2026-09-13](COMPLETED-CHECKLIST.md#m2141) + +- [x] **M2.14.2** -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does NOT already say. -> [completed 2026-09-13](COMPLETED-CHECKLIST.md#m2142) - [ ] **M2.16** -- Repair the garbled `Report` doc comment, and drop the two counts that have already rotted beside it. diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index f59d0ff35..28a4b0ab6 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1027,3 +1027,90 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. Default build re-verified by binary inspection: neither `serde` nor `serde_json` appears on a normal dependency edge, and `probe-topology.exe` contains no parser string. + +## Moved 2026-09-13 -- M2.14: making the two authoring rules this branch earned actually bite + +### M2.14 -- Make the two authoring rules this branch earned actually bite. *(completed 2026-09-13 22:03:09 UTC-07:00)* + + Re-planned + 2026-09-12; see the rationale below before implementing either sub-step. Both sub-steps done. + + **As originally written this item said "write two authoring rules into the repository + instructions". Measurement says that would have been worse than useless.** The two rules it + proposed -- *state the invariant, not the census*, and *a new test is not done until it has been + observed to fail* -- ALREADY EXIST, in + [.github/copilot-instructions.md](../../.github/copilot-instructions.md) under CONTRACT INTEGRITY + rule 1 ("Prefer a derived fact to a restated one", and beneath it "verify the binding by + sabotage: change the definition and confirm the consumer's BEHAVIOR changes"). Writing them again + would add a second copy of a rule, which is the exact defect that section forbids and the exact + mechanism -- restatement drift -- it exists to prevent. + + **And statement is demonstrably not the gap.** Both rules were in force on 2026-09-12 and both + were violated: `09da7e9` claims "Sabotage-verified, EACH against the instrument it was meant to + strengthen" and then names two sabotages for four fixes. The one that got none is the completeness + guard, which a review found broken an hour later (M3.8). A rule cited in the commit that breaks it + will not be repaired by a third copy of itself. + + **This repository has already solved this problem once, and not with a rule.** The + [ci.yml](../../.github/workflows/ci.yml) `sabotage-harness` job records that the harness "accumulated + fixes over eleven review rounds and thirteen of the later defects were introduced by earlier fixes, + because every verification was a one-off command that was then discarded and nothing re-checked an + earlier guarantee." That is M3.8's story verbatim. The answer then was a CI ratchet. + + +### M2.14.1 -- Give this crate a sabotage manifest, so "observed to fail" is a recorded artifact rather than a habit. *(completed 2026-09-13 22:03:09 UTC-07:00)* + + **Done.** [sabotage.json](sabotage.json), 7 entries, swept green: six `caught`, one `survives`, + all behaving as declared. Not wired into CI, matching the two sibling manifests and the + `sabotage-harness` job's own note that a sweep "rebuilds a crate per entry and is deliberately an + occasional instrument". + + **The control is the entry that matters most here.** It rewords a prose line to carry the same + fact and must SURVIVE, which turns this component's central decision -- the row is the machine + contract, the prose is for a reader -- from a sentence into a measurement. If it is ever reported + as caught, a test has started reading the prose again and that test is the defect. + + **The harness found a defect in the manifest that the authoring script missed, which is the + lesson.** The entry for the escape-aware key reader anchored on `'\\' => escaped = true,`; the + script checked uniqueness by whole-LINE equality and found one match, while the harness matches by + SUBSTRING and found two -- the same arm appears in `malformation` at a deeper indent, and the + shallower line is a substring of the deeper one. The script's check was a second, weaker + implementation of the harness's rule, which is precisely the defect class this manifest exists to + catch. The anchor was widened to the function signature; the harness remains the only authority on + uniqueness. + + `tools/run-sabotage.ps1` exists, has its own tests, and runs in CI; + [windows-placement-probe](../windows-placement-probe/sabotage.json) (9 entries) and + [windows-waitable-queues](../windows-waitable-queues/sabotage.json) (39 entries) each carry a + `sabotage.json`. **This crate carried none**, so every sabotage run while building M3 was ad-hoc + PowerShell, discarded on the spot -- which is why a `git checkout` destroyed uncommitted work + twice and a `.Replace` pattern silently matched two sites once. The manifest format's `find` must + match EXACTLY ONCE, which is precisely the guard that hand-running lacked. + + Eight commits on this branch recorded their sabotages in the message, so the first pass was + transcription rather than invention: the defect, the file and the test expected to redden were + already written down. + + Include at least one `expect: "survives"` control. A manifest of nothing but `caught` cannot + distinguish a suite that is watching from a suite that fails on any edit. + + +### M2.14.2 -- Add to CONTRACT INTEGRITY rule 1 the one thing this branch learned that it does NOT already say. *(completed 2026-09-13 22:03:09 UTC-07:00)* + + A pointer, not a restatement. + + **Done.** Two paragraphs added to CONTRACT INTEGRITY rule 1 in + [.github/copilot-instructions.md](../../.github/copilot-instructions.md). Neither restates the + existing rule: the first says to sabotage the CLAIM a change makes rather than the symptom it + cites, and the second says a sabotage is worth nothing once discarded and points at the manifest + and the harness. The original M2.14 wording is nowhere in the diff, which was the point. + + The genuinely new fact: **when a fix claims to have removed a weakness, sabotage the claim rather + than the symptom.** Rule 1 tells an author to prefer a derived fact over a restated one; it does + not warn that an author may believe they derived one when they only MOVED the census. That is + exactly what happened three times on a single guard -- strings, then a hand-written `ALL`, then + generation -- each fix relocating the census somewhere harder to see while its commit message + claimed the class was closed. The wording that would have caught it is about the CLAIM, and rule 1 + currently has no sentence about claims. + + > **-> DEPENDS ON M2.14.1:** the pointer has nothing to point at until the manifest exists. diff --git a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md index 27ba0d45c..ca3b63d2f 100644 --- a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md +++ b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md @@ -74,7 +74,7 @@ predicate was violated. At first reading this looks like the strongest case for that reads text -- a contradiction that exists only in the representation. But look at how it was actually repaired. The row now emits -`"efficiency_classes":[{}]` -- a list. **The fix was to change what the row publishes.** +`"efficiency_classes":[0]` -- the class LABELS as a list. **The fix was to change what the row publishes.** The prose comparison was the route by which a reviewer noticed, not the repair. ### A correction made while writing this up, and it strengthened the case diff --git a/crates/windows-platform-probes/src/topology/diagnostic.rs b/crates/windows-platform-probes/src/topology/diagnostic.rs index aa31d6f78..90207ef53 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic.rs @@ -56,6 +56,40 @@ fn entry(code: &'static str, fields: Vec<(&'static str, Value)>) -> Value { Value::Object(members) } +/// Defines `code` and `ALL_CODES` for a diagnostic enum from one list. +/// +/// The `match` is exhaustive, so a variant added to the enum does not compile +/// until it has a line here -- and that line reaches `ALL_CODES` without anyone +/// having to remember. That is the whole point. The tests assert that a fixture +/// covers `ALL_CODES`, so a new variant is not merely obliged to HAVE a code, it +/// is obliged to be EXERCISED; a hand-written `ALL_CODES` would have relocated +/// the omission rather than closed it. +/// +/// What this deliberately does not do is state the codes twice. The goldens in +/// the tests remain the independent second statement of the VALUES. This is the +/// single statement of the SET, and the two answer different questions. +macro_rules! diagnostic_codes { + ($enum:ident { $($pattern:pat => $code:literal,)+ }) => { + impl $enum { + /// The stable discriminant a survey groups by. + /// + /// Changing one of these is a breaking change to the NDJSON row. + #[must_use] + pub const fn code(&self) -> &'static str { + match self { + $($pattern => $code,)+ + } + } + + /// Every code this enum can produce, one per variant. + /// + /// Generated beside `code` from the same list, so it cannot omit a + /// variant the enum has. + pub const ALL_CODES: &'static [&'static str] = &[$($code,)+]; + } + }; +} + /// A counter comparison that was made and did not match. /// /// Each is a finding about the shipping crate's parse, and is the only list @@ -89,19 +123,11 @@ pub enum Disagreement { }, } -impl Disagreement { - /// The stable discriminant a survey groups by. - /// - /// Changing one of these is a breaking change to the NDJSON row. - #[must_use] - pub const fn code(&self) -> &'static str { - match self { - Self::OnlineProcessors { .. } => "online_processors", - Self::ProcessorGroups { .. } => "processor_groups", - Self::HighestNumaNode { .. } => "highest_numa_node", - } - } -} +diagnostic_codes!(Disagreement { + Self::OnlineProcessors { .. } => "online_processors", + Self::ProcessorGroups { .. } => "processor_groups", + Self::HighestNumaNode { .. } => "highest_numa_node", +}); impl Disagreement { /// This disagreement as the row publishes it. @@ -210,22 +236,14 @@ pub fn described(entry: &impl fmt::Display) -> String { } } -impl NotCompared { - /// The stable discriminant a survey groups by. - /// - /// Changing one of these is a breaking change to the NDJSON row. - #[must_use] - pub const fn code(&self) -> &'static str { - match self { - Self::MachineChanged => "machine_changed", - Self::BracketNotEstablished => "bracket_not_established", - Self::CountsIncludeUnparsedRelations => "counts_include_unparsed_relations", - Self::ActiveProcessorCountFailed => "active_processor_count_failed", - Self::ActiveProcessorGroupCountFailed => "active_processor_group_count_failed", - Self::HighestNumaNodeFailed => "highest_numa_node_failed", - } - } -} +diagnostic_codes!(NotCompared { + Self::MachineChanged => "machine_changed", + Self::BracketNotEstablished => "bracket_not_established", + Self::CountsIncludeUnparsedRelations => "counts_include_unparsed_relations", + Self::ActiveProcessorCountFailed => "active_processor_count_failed", + Self::ActiveProcessorGroupCountFailed => "active_processor_group_count_failed", + Self::HighestNumaNodeFailed => "highest_numa_node_failed", +}); impl NotCompared { /// This entry as the row publishes it. @@ -391,37 +409,29 @@ pub enum ParseIncomplete { CoherenceNotCollected, } -impl ParseIncomplete { - /// The stable discriminant a survey groups by. - /// - /// Changing one of these is a breaking change to the NDJSON row. - #[must_use] - pub const fn code(&self) -> &'static str { - match self { - Self::EnumerationAnomalies { .. } => "enumeration_anomalies", - Self::NumaDomainsOnlyInCpuSets { .. } => "numa_domains_only_in_cpu_sets", - Self::NoCacheLevels => "no_cache_levels", - Self::CacheLevelsWithoutPartitions { .. } => "cache_levels_without_partitions", - Self::MeasuredButCountsAbsent { .. } => "measured_but_counts_absent", - Self::NoPackages => "no_packages", - Self::NoCores => "no_cores", - Self::ContradictoryCores { .. } => "contradictory_cores", - Self::UnnumberedCacheLevels { .. } => "unnumbered_cache_levels", - Self::PartitioningSummaryMissing { .. } => "partitioning_summary_missing", - Self::NotMeasured => "not_measured", - Self::RelationsWithoutProcessors { .. } => "relations_without_processors", - Self::UnreportedRelations { .. } => "unreported_relations", - Self::DescribedRelations { .. } => "described_relations", - Self::CoresOnlyInCpuSets { .. } => "cores_only_in_cpu_sets", - Self::OverlappingWalkRelations { .. } => "overlapping_walk_relations", - Self::ProcessorAttributeConflicts { .. } => "processor_attribute_conflicts", - Self::NumaDomainsWithConflictingLabels { .. } => "numa_domains_with_conflicting_labels", - Self::NumaDomainsUnreported { .. } => "numa_domains_unreported", - Self::EnumerationsDisagreed { .. } => "enumerations_disagreed", - Self::CoherenceNotCollected => "coherence_not_collected", - } - } -} +diagnostic_codes!(ParseIncomplete { + Self::EnumerationAnomalies { .. } => "enumeration_anomalies", + Self::NumaDomainsOnlyInCpuSets { .. } => "numa_domains_only_in_cpu_sets", + Self::NoCacheLevels => "no_cache_levels", + Self::CacheLevelsWithoutPartitions { .. } => "cache_levels_without_partitions", + Self::MeasuredButCountsAbsent { .. } => "measured_but_counts_absent", + Self::NoPackages => "no_packages", + Self::NoCores => "no_cores", + Self::ContradictoryCores { .. } => "contradictory_cores", + Self::UnnumberedCacheLevels { .. } => "unnumbered_cache_levels", + Self::PartitioningSummaryMissing { .. } => "partitioning_summary_missing", + Self::NotMeasured => "not_measured", + Self::RelationsWithoutProcessors { .. } => "relations_without_processors", + Self::UnreportedRelations { .. } => "unreported_relations", + Self::DescribedRelations { .. } => "described_relations", + Self::CoresOnlyInCpuSets { .. } => "cores_only_in_cpu_sets", + Self::OverlappingWalkRelations { .. } => "overlapping_walk_relations", + Self::ProcessorAttributeConflicts { .. } => "processor_attribute_conflicts", + Self::NumaDomainsWithConflictingLabels { .. } => "numa_domains_with_conflicting_labels", + Self::NumaDomainsUnreported { .. } => "numa_domains_unreported", + Self::EnumerationsDisagreed { .. } => "enumerations_disagreed", + Self::CoherenceNotCollected => "coherence_not_collected", +}); impl ParseIncomplete { /// This entry as the row publishes it, with the values its variant carries. diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs index c902bf07e..86db50c60 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -151,6 +151,42 @@ fn every_disagreement_publishes_the_pair_it_carries() { ); } +/// Asserts that `fixture` contains an instance of EVERY variant, by the codes it +/// covers rather than by how many entries it has. +/// +/// **A count proves nothing.** This replaced `assert!(every.len() > 15)`, which +/// a fixture of any size passes while omitting a variant -- so a variant added +/// to the enum could go unrendered with the suite green, which is exactly the +/// hole the fixture existed to close. Found by a review. +/// +/// `ALL_CODES` is generated beside `code` from one list, so a new variant +/// reaches this check without anyone remembering to widen it. Compared as a SET +/// because a fixture may legitimately carry two instances of one variant to +/// exercise a payload that differs, as the `Disagreement` one does. +fn covers_every_variant(what: &str, covered: &[&str], all: &[&str]) { + let missing: Vec<&str> = all + .iter() + .filter(|code| !covered.contains(*code)) + .copied() + .collect(); + assert!( + missing.is_empty(), + "the {what} fixture omits {missing:?}, so those variants are never rendered here" + ); + + // The other direction, so a code retired from the enum does not linger in a + // fixture that then silently tests nothing. + let stale: Vec<&str> = covered + .iter() + .filter(|code| !all.contains(*code)) + .copied() + .collect(); + assert!( + stale.is_empty(), + "the {what} fixture carries {stale:?}, which no variant produces" + ); +} + /// Every `ParseIncomplete` variant, one instance each. /// /// **Shared, because two tests need the same completeness and a second @@ -234,6 +270,17 @@ fn every_parse_incomplete_variant_has_the_code_the_row_promises() { let every = every_parse_incomplete(); + // This test's NAME claims every variant, and the exhaustive `golden` above + // does not deliver that: a `match` obliges a variant to HAVE an arm, never + // obliges the fixture to reach it, so an omitted variant's code would go + // unchecked here. Stated independently rather than leaned on from the + // presence test, which could be deleted without this one noticing. + covers_every_variant( + "ParseIncomplete", + &every.iter().map(ParseIncomplete::code).collect::>(), + ParseIncomplete::ALL_CODES, + ); + let mut seen: Vec<&str> = Vec::new(); for entry in &every { let code = entry.code(); @@ -462,22 +509,44 @@ fn every_diagnostic_describes_itself() { counter: 3, }, ]; + covers_every_variant( + "Disagreement", + &disagreements + .iter() + .map(Disagreement::code) + .collect::>(), + Disagreement::ALL_CODES, + ); for entry in &disagreements { let text = described(entry); assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); assert!(!text.trim().is_empty(), "{entry:?} renders blank"); } - // Exhaustive, so a seventh `NotCompared` must describe itself to compile. - for entry in [ + // The comment here used to read "Exhaustive, so a seventh `NotCompared` must + // describe itself to compile" -- of an ARRAY LITERAL, which forces nothing. + // A seventh variant compiles fine and is simply never rendered. Same defect + // as the one a review reported against the count below, in a comment that + // claimed the guarantee outright; found by sweeping the class rather than + // the reported instance. + let not_compared = [ NotCompared::MachineChanged, NotCompared::BracketNotEstablished, NotCompared::CountsIncludeUnparsedRelations, NotCompared::ActiveProcessorCountFailed, NotCompared::ActiveProcessorGroupCountFailed, NotCompared::HighestNumaNodeFailed, - ] { - let text = described(&entry); + ]; + covers_every_variant( + "NotCompared", + ¬_compared + .iter() + .map(NotCompared::code) + .collect::>(), + NotCompared::ALL_CODES, + ); + for entry in ¬_compared { + let text = described(entry); assert_ne!(text, UNDESCRIBED, "{entry:?} renders blank"); assert!(!text.trim().is_empty(), "{entry:?} renders blank"); } @@ -488,10 +557,10 @@ fn every_diagnostic_describes_itself() { // rendered here, and blanking any of their arms would have put UNDESCRIBED // in a real report while this test stayed green. Found by a review. let every = every_parse_incomplete(); - assert!( - every.len() > 15, - "the fixture should carry the whole enum, not a sample: {}", - every.len() + covers_every_variant( + "ParseIncomplete", + &every.iter().map(ParseIncomplete::code).collect::>(), + ParseIncomplete::ALL_CODES, ); for entry in &every { let text = described(entry); From afa4a31983f89a42a48da7550d73e96e0212cdeb Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 22:37:16 -0700 Subject: [PATCH 35/46] chore(ioring): name the completion-event requirement, so it fits where it is used The deeper of the two `completion_event().expect(...)` sites was mis-indented: the continuation and the closing paren sat at the enclosing block's level rather than the call's. A review read that as a formatting failure that would break `cargo fmt --all --check`. It would not -- `cargo fmt --check` reports the workspace clean, before and after -- and why is the part worth recording. Nested inside the width loop, the message pushes the line past `max_width`; rustfmt responds by leaving the whole expression alone rather than by reformatting it. So rustfmt GIVING UP is silent, and mis-indented code passes the check that exists to catch it. Naming the message as a `const` makes both call sites one short line, which rustfmt does format, so the shape is now maintained by the tool rather than by hand. Verified: `cargo fmt` leaves both sites untouched afterwards. Test-only, and typed `chore` deliberately -- `tools/check-commit-scope.ps1` flagged it riding along with unrelated `windows-platform-probes` work, where a `fix` would have earned this crate a changelog entry and a version bump for a whitespace change in a test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-ioring-sys/tests/handover.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/crates/windows-ioring-sys/tests/handover.rs b/crates/windows-ioring-sys/tests/handover.rs index b671a7e68..097ed155f 100644 --- a/crates/windows-ioring-sys/tests/handover.rs +++ b/crates/windows-ioring-sys/tests/handover.rs @@ -63,6 +63,18 @@ const CHUNKS: usize = 8; const CHUNK_LEN: usize = 512; const WAVES: usize = 3; +/// Why a host cannot run these tests, named once because two sites say it. +/// +/// A `const` rather than the literal at each site because the literal does not +/// FIT at the deeper one: nested in a loop it pushes the line past `max_width`, +/// rustfmt responds by leaving the whole expression alone, and the result is +/// mis-indented code that `cargo fmt --check` reports as clean. Found by a +/// review, which read the misalignment as a formatting failure that would break +/// CI -- it would not, and that is the more interesting half: rustfmt giving up +/// is silent, so the only guard here is the shorter line. +const NEEDS_COMPLETION_EVENT: &str = + "this host must report IORING_FEATURE_SET_COMPLETION_EVENT to run the handover tests"; + /// Generous, because a positive wait must not flake on a loaded machine. /// Every test that pays it in full is one that would otherwise hang. const SIGNAL_TIMEOUT_MS: u32 = 5_000; @@ -340,9 +352,7 @@ fn an_attach_serves_both_the_backlog_and_the_wave_that_follows_it() { // signal can account for it. submit_wave(&mut ring, &file, 0, &mut contract, &mut pending); - let event = ring.completion_event().expect( - "this host must report IORING_FEATURE_SET_COMPLETION_EVENT to run the handover tests", - ); + let event = ring.completion_event().expect(NEEDS_COMPLETION_EVENT); // Wave 1 lands *after* the attach, into a queue wave 0 already made // non-empty -- so it raises no edge of its own and is only ever seen by a @@ -510,9 +520,7 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { batch.submit_and_wait(0, 0).expect("submit without waiting"); } - let event = ring.completion_event().expect( - "this host must report IORING_FEATURE_SET_COMPLETION_EVENT to run the handover tests", - ); + let event = ring.completion_event().expect(NEEDS_COMPLETION_EVENT); let attached = std::time::Instant::now(); // Non-blocking, so this measures what the attach actually found rather From 28b234062fedf3e23ace3e80639ebfb6751b9992 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 22:37:41 -0700 Subject: [PATCH 36/46] fix(platform-probes): rename assert_corresponds to what M3 left it doing M3 made the row the machine contract and retired the prose/row comparison. The oracle kept validating ONE thing -- that a report carries exactly one well-formed JSON row -- under a name that went on promising a cross-part guarantee, and five statements around it went on describing the retired relation. A review read all five as current, which is exactly what the name invited. Renamed to `assert_row_is_well_formed`. **The evidence paragraph in `topology_report::report` was FALSE, and that is the finding worth recording.** It claimed a prose/NDJSON processor-count contradiction turned 13 tests red through the binding. Run rather than read: that sabotage now leaves all 228 library tests AND all 10 real-host integration tests green. It was true before M3 and stopped being true when the oracle stopped reading prose, with nothing to notice. Replaced with a measurement of what the binding actually catches, taken the same way: sabotaging `Row::render` to drop the closing brace fails 42 library tests with the binding and 29 without it, and the 13 only it catches are about cache notes, efficiency classes, caveats, NUMA lines and partitioning levels -- not one mentions the row's syntax. They inherit the check purely by rendering, which is the argument for binding the renderer. Counts are dated; the invariant is that no per-part test parses a row, so nothing else can catch a malformed one. Also in this cluster: - `the_row_names_the_probes_own_bug_when_it_detects_one` asserted the prose string `BUG IN THIS PROBE` before asserting the row code, so a reword could redden a row-contract test -- the one thing M3 exists to stop. Its precondition now reads the typed `ParseIncomplete::PartitioningSummaryMissing` off the observation, which is what the renderer itself reads. The crate's `survives` sabotage control had not noticed, because it is anchored on a different prose line: the control samples a line rather than establishing the property. - The integration target's header still described prose-against-row as its purpose. Rewritten to observation-against-row, with the change of meaning stated rather than quietly substituted. The target keeps its name because a value against its encoding is still self-agreement. - `report_unmeasured`'s comment said the renderer "makes fewer claims, so fewer correspondences apply". None apply; well-formedness is not a thing a renderer makes less of. Two further instrument claims, same class as the census fixed in 6ef80f7: - The `NotCompared` and `Disagreement` CODE tests iterated hand-written arrays behind `every_...` names, which 6ef80f7 fixed only for the presence test. Both now derive their coverage and hold it against `ALL_CODES`; the `Disagreement` one became a cases array so its set can be derived rather than restated. - `covers_every_variant` was blind to DUPLICATE codes: two variants sharing a literal leaves one `ALL_CODES` entry satisfied by one fixture string, and only `ParseIncomplete` had a separate uniqueness assertion. Now checked on `ALL_CODES` at one site, so all three enums get it. Sabotage-verified with the goldens UPDATED to agree, so the duplicate is the only defect present: one test fails, naming `["online_processors"]`; before, that configuration was entirely green. - The tests module doc claimed the `unclassified` fallback "is asserted directly". It is not, and cannot be from this crate: `#[non_exhaustive]` is what stops a downstream crate constructing an unknown kind. Narrowed to what is checkable -- that no kind this crate names reaches that arm. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/src/lib.rs | 2 +- .../src/report_oracle.rs | 13 ++- .../src/report_oracle/tests.rs | 2 +- crates/windows-platform-probes/src/tests.rs | 22 ++++- .../src/topology/diagnostic/tests.rs | 99 ++++++++++++++----- .../src/topology/invariant.rs | 2 +- .../src/topology_report.rs | 60 ++++++----- .../tests/a_real_report_agrees_with_itself.rs | 48 +++++---- 8 files changed, 170 insertions(+), 78 deletions(-) diff --git a/crates/windows-platform-probes/src/lib.rs b/crates/windows-platform-probes/src/lib.rs index a989f0925..a507d54f7 100644 --- a/crates/windows-platform-probes/src/lib.rs +++ b/crates/windows-platform-probes/src/lib.rs @@ -151,7 +151,7 @@ pub mod pool_growth; pub mod report; /// The report oracle. **Test-support: present only where it is used.** /// -/// Every caller is already behind this gate -- the renderers' `assert_corresponds` +/// Every caller is already behind this gate -- the renderers' `assert_row_is_well_formed` /// bindings, the unit tests, and the integration tests, which reach it through the /// self dev-dependency. Stating that here rather than leaving it implied is what /// lets the module depend on a real JSON parser without putting one in a shipping diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 8d0b52dfd..ff954dac7 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -199,7 +199,7 @@ fn malformation(row: &str) -> Option { /// terminator, so a `discovery_error` carrying an escaped quote shifted where it /// thought strings began and text INSIDE the error was emitted as top-level /// keys. Measured: an `io::Error` of `q":1,"q":1,"q` rendered a row that -/// `JSON.parse` accepts with four keys, and `assert_corresponds` panicked from +/// `JSON.parse` accepts with four keys, and `assert_row_is_well_formed` panicked from /// inside the renderer. Fixing that added escape-awareness to one of the /// scanners and left the others to be argued about; this removes the question. /// @@ -295,10 +295,19 @@ pub fn row(report: &str) -> Option<&str> { /// [`check`], as an assertion, for tests that render a report. /// +/// **Named `assert_corresponds` until 2026-09-13, and the name outlived what it +/// did.** Before M3 this compared a report's prose against its encoded row; M3 +/// made the row the machine contract and retired that comparison, leaving a +/// function that validates ONE thing -- that the report carries exactly one +/// well-formed JSON row. The old name went on promising a cross-part guarantee +/// to every reader of its four call sites. Renamed after a review read those +/// call sites as still enforcing correspondence, which is exactly the mistake +/// the name invited. +/// /// # Panics /// /// Panics listing every way the row is malformed. -pub fn assert_corresponds(report: &str) { +pub fn assert_row_is_well_formed(report: &str) { let defects = check(report); assert!( defects.is_empty(), diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index c76e88327..a89664278 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -163,7 +163,7 @@ fn an_escaped_quote_inside_a_value_does_not_forge_a_key() { // `keys` used `find('"')`, which takes `\"` for a terminator, so an escaped // quote shifted where it thought strings began and ended and text INSIDE a // value was emitted as a top-level key. Two equal ones read as a repeated - // key, and `assert_corresponds` panicked from inside `report_unmeasured`. + // key, and `assert_row_is_well_formed` panicked from inside `report_unmeasured`. // // Reachable, not hypothetical: `discovery_error` carries a failed // discovery's `io::Error`, whose message is whatever the OS said. diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 1196d3927..9d1310bbc 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4587,14 +4587,26 @@ fn the_row_names_the_probes_own_bug_when_it_detects_one() { let text = crate::topology_report::report(BANNER, &observation); + // **The precondition is read off the OBSERVATION, not off the prose.** This + // asserted `text.contains("BUG IN THIS PROBE")` first, which made a + // row-contract test depend on the wording of a sentence -- the one thing M3 + // says the row exists to stop. A reword of that line would have reddened + // this test, and the crate's `survives` sabotage control did not notice + // because it is anchored on a different prose line. Found by a review. + // + // The typed condition is the honest precondition anyway: it is what the + // renderer itself reads to decide whether to alarm. + let check = observation.cross_check(); assert!( - text.contains("BUG IN THIS PROBE"), - "the prose still alarms: {text}" + check.parse_incomplete.iter().any(|entry| matches!( + entry, + crate::topology::diagnostic::ParseIncomplete::PartitioningSummaryMissing { .. } + )), + "the probe must have detected its own bug for this test to mean anything: {check:?}" ); assert!( row_codes(&text, "parse_incomplete").contains(&"partitioning_summary_missing".to_owned()), - "the row must name the condition the prose alarms about, not merely \ - count it: {text}" + "the row must name the condition, not merely count it: {text}" ); } @@ -4765,7 +4777,7 @@ fn a_discovery_error_full_of_json_cannot_manufacture_a_second_row() { "and the ONE row is the probe's, not the caller's: {}", rows[0] ); - crate::report_oracle::assert_corresponds(&text); + crate::report_oracle::assert_row_is_well_formed(&text); } #[test] diff --git a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs index 86db50c60..87b289ca0 100644 --- a/crates/windows-platform-probes/src/topology/diagnostic/tests.rs +++ b/crates/windows-platform-probes/src/topology/diagnostic/tests.rs @@ -39,10 +39,18 @@ //! Where the enum belongs to this crate, the expectation is written as an //! exhaustive `match`, so a variant added without a golden does not compile. //! `AnomalyKind` and `Source` are `#[non_exhaustive]` upstream and cannot be -//! matched exhaustively; for those the goldens are explicit instances, and the -//! `unclassified` fallback is asserted directly so that the arm which is -//! SUPPOSED to catch an unknown kind is distinguished from an arm that fell -//! through by accident. +//! matched exhaustively; for those the goldens are explicit instances, and each +//! named kind is asserted NOT to return `unclassified`, so an arm deleted from +//! the classifier is caught rather than quietly becoming a fall-through. +//! +//! **What that does not cover, stated because the wording here used to claim it +//! did.** This said the `unclassified` fallback "is asserted directly". It is +//! not, and from this crate it cannot be: `#[non_exhaustive]` is precisely the +//! attribute that stops a downstream crate constructing a variant it does not +//! know, so no unknown kind can be built here to drive that arm. The arm is +//! reachable only from a future upstream release, and what is checked is the +//! half that is checkable -- that nothing this crate DOES name reaches it. +//! Found by a review. use super::{ Disagreement, NotCompared, ParseIncomplete, UNDESCRIBED, anomaly_code, described, @@ -84,14 +92,25 @@ fn every_not_compared_code_is_the_one_the_row_promises() { NotCompared::HighestNumaNodeFailed => "highest_numa_node_failed", }; - for reason in [ + // The exhaustive `golden` above obliges a new variant to have a code; it does + // NOT oblige this array to carry one, so a seventh variant could take a wrong + // code with this `every...` test green. Reported by a review against exactly + // this loop. + let every = [ NotCompared::MachineChanged, NotCompared::BracketNotEstablished, NotCompared::CountsIncludeUnparsedRelations, NotCompared::ActiveProcessorCountFailed, NotCompared::ActiveProcessorGroupCountFailed, NotCompared::HighestNumaNodeFailed, - ] { + ]; + covers_every_variant( + "NotCompared", + &every.iter().map(NotCompared::code).collect::>(), + NotCompared::ALL_CODES, + ); + + for reason in every { assert_eq!(reason.code(), golden(&reason), "{reason:?}"); assert_eq!( rendered(reason.published()), @@ -106,36 +125,44 @@ fn every_disagreement_publishes_the_pair_it_carries() { // `parsed` and `counter` are the two numbers a survey compares, so swapping // the labels is the mislabelling defect in its purest form: the row still // parses and says the opposite of the truth. - assert_eq!( - rendered( + // + // A cases array rather than free-standing assertions, so the set this test + // exercises can be DERIVED and held against `ALL_CODES`. Written out, the + // name's "every disagreement" rested on nobody adding a fourth variant. + let cases = [ + ( Disagreement::OnlineProcessors { parsed: 12, counter: 16, - } - .published() + }, + r#"{"code":"online_processors","parsed":12,"counter":16}"#, ), - r#"{"code":"online_processors","parsed":12,"counter":16}"# - ); - assert_eq!( - rendered( + ( Disagreement::ProcessorGroups { parsed: 1, counter: 2, - } - .published() + }, + r#"{"code":"processor_groups","parsed":1,"counter":2}"#, ), - r#"{"code":"processor_groups","parsed":1,"counter":2}"# - ); - assert_eq!( - rendered( + ( Disagreement::HighestNumaNode { parsed: Some(2), counter: 3, - } - .published() + }, + r#"{"code":"highest_numa_node","parsed":2,"counter":3}"#, ), - r#"{"code":"highest_numa_node","parsed":2,"counter":3}"# + ]; + covers_every_variant( + "Disagreement", + &cases + .iter() + .map(|(found, _)| found.code()) + .collect::>(), + Disagreement::ALL_CODES, ); + for (found, golden) in &cases { + assert_eq!(rendered(found.published()), *golden, "{found:?}"); + } // The absent parse renders as `null`, not as a number and not as an omitted // field: a survey must be able to tell "the parse saw no NUMA node" from // "the parse saw node 0". @@ -164,6 +191,32 @@ fn every_disagreement_publishes_the_pair_it_carries() { /// because a fixture may legitimately carry two instances of one variant to /// exercise a payload that differs, as the `Disagreement` one does. fn covers_every_variant(what: &str, covered: &[&str], all: &[&str]) { + // **Distinctness, checked on `ALL_CODES` rather than on the fixture**, and + // checked here so all three enums get it from one site. Set membership + // alone cannot see a duplicate: if two variants were given the same + // literal, `ALL_CODES` would carry it twice and a single fixture entry + // would satisfy both copies in both directions below. `ParseIncomplete` + // had a separate uniqueness assertion; `Disagreement` and `NotCompared` + // had none, so for those two a shared code was invisible. Found by a + // review. + // + // The harm is the same one the row exists to prevent: two conditions that + // publish one code cannot be told apart by a survey. + let mut seen: Vec<&str> = Vec::new(); + let mut repeated: Vec<&str> = Vec::new(); + for code in all { + if seen.contains(code) { + repeated.push(code); + } else { + seen.push(code); + } + } + assert!( + repeated.is_empty(), + "two {what} variants publish {repeated:?}, so a survey cannot tell those \ + conditions apart" + ); + let missing: Vec<&str> = all .iter() .filter(|code| !covered.contains(*code)) diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index e108eed3a..0599977c5 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -404,7 +404,7 @@ pub fn check(observation: &Observation, verdict: Verdict) -> Vec { /// `cross_check` would give; this function has no such seam, so there is no /// observation for which it panics and nothing to distinguish it from `()`. /// -/// The same survivor was recorded for `assert_corresponds` on PR #88 -- see +/// The same survivor was recorded for `assert_row_is_well_formed` on PR #88 -- see /// [DESIGN-RATIONALE.md](../../DESIGN-RATIONALE.md) -- for the same reason: every /// instrument that would notice goes THROUGH it. A binding that cannot fail on /// data is checked by the sweep's `caught` results on [`check`] itself, which is diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index 5379d1346..e0d1fe1df 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -322,12 +322,15 @@ pub fn report_unmeasured(banner: &str, error: &io::Error) -> String { .render() ); - // Bound here too, for the reason given on `report` below. This renderer - // makes fewer claims, so fewer correspondences apply -- but "fewer apply" - // is a conclusion the oracle should reach by looking, not one assumed by - // leaving the call out. + // Bound here too, for the reason given on `report` below: an unmeasured + // report is still a report, and a survey still has to parse its row. + // + // This used to say the renderer "makes fewer claims, so fewer + // correspondences apply". There are no correspondences left to apply -- M3 + // retired the prose/row relation, and what is checked is that the row is + // well formed, which is not a thing a renderer can make less of. #[cfg(any(test, feature = "oracle-in-renderer"))] - crate::report_oracle::assert_corresponds(&out); + crate::report_oracle::assert_row_is_well_formed(&out); out } @@ -867,28 +870,33 @@ pub fn report(banner: &str, observation: &Observation) -> String { // checks one case; binding the renderer checks every case anyone writes // later, including the ones nobody thought to add. // - // **Measured, not assumed.** Re-introducing a cross-part contradiction -- - // the NDJSON processor count one higher than the prose -- turns 13 existing - // tests red through this line, none of which was written about processor - // counts: they are about cache notes, efficiency classes and caveats, and - // they inherit the check purely by rendering a report. With the same - // contradiction in place and this line removed, EVERY LIBRARY TEST PASSES: - // the per-part tests cannot see the defect at all. + // **What it catches: a renderer that emits a malformed row.** Nothing here + // compares the prose against the row. M3 retired that relation -- the row is + // the machine contract and the prose is reviewed, not parsed -- so the only + // claim this line supports is that every report this renderer produces + // carries exactly one well-formed JSON row. + // + // **Measured, not assumed** (2026-09-13, by sabotaging `Row::render` to drop + // the closing brace): 42 library tests fail with this line, 29 without it. + // The 13 that only this line catches are about cache notes, efficiency + // classes, caveats, NUMA lines and partitioning levels -- not one of them + // mentions the row's syntax. They inherit the check purely by rendering a + // report, which is the whole argument for binding the renderer instead of + // adding a test beside the others. // - // That sentence used to say THE WHOLE SUITE passes, which was true when it - // was written and stopped being true in the same commit -- this branch adds - // `tests/a_real_report_agrees_with_itself.rs`, whose tests call the oracle - // explicitly and so go red without the binding. Measured just now: the - // library suite is entirely green under that sabotage while the real-host - // integration tests fail. Named without a count on purpose, because the - // count moved between a reviewer measuring it and this correction being - // written, for exactly the reason the next paragraph gives. + // **The previous version of this paragraph was false, and that is worth + // recording.** It claimed a prose/NDJSON processor-count contradiction turned + // 13 tests red through this line. That was true before M3 and silently + // stopped being true when the oracle stopped reading prose: re-run in full, + // the sabotage it names now leaves all 228 library tests AND all 10 real-host + // integration tests green. A reviewer inferred it from the code; the check + // that settled it was running it. An evidence paragraph nothing executes is + // the same rot as a test nothing runs -- so when this mechanism changes + // again, re-measure rather than re-word. // - // Stated as the invariant rather than as a count, because the count rots. - // This read "all 190 pass" when the suite held 190 tests, and it has grown - // several times since -- so a reviewer had to run the suite three times to - // establish that the sentence was merely stale rather than wrong. The - // number was never the point; that nothing else catches the defect is. + // The counts above are dated for that reason. The invariant is the durable + // half: nothing else in the library catches a malformed row, because no + // per-part test parses one. // // **Why the gate is not `cfg(test)` alone.** It was, and the claim above was // then false for half of what "every test" means: cargo compiles this @@ -924,6 +932,6 @@ pub fn report(banner: &str, observation: &Observation) -> String { // lost is the NDJSON row a survey would have mined, which is why the default // build is the one that matters and is the one pinned above. #[cfg(any(test, feature = "oracle-in-renderer"))] - crate::report_oracle::assert_corresponds(&out); + crate::report_oracle::assert_row_is_well_formed(&out); out } diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index 2a5bc1587..c4dbd0600 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -1,34 +1,44 @@ // Copyright (c) Mike Grier. -//! The report this crate renders from a *real* measurement, checked against the -//! oracle. +//! The report this crate renders from a *real* measurement, checked against +//! the OBSERVATION it was rendered from. //! -//! # Why this is not a unit test +//! # What "agrees with itself" means here, since it changed +//! +//! It used to mean the report's two renderings agreed: prose against encoded +//! row. M3 retired that relation -- the row is the machine contract and the +//! prose is reviewed rather than parsed -- and this file's tests moved with it +//! without its header following. Reported by a review, which read the paragraphs +//! below as still describing what the file does. //! -//! The oracle's own tests pin it against fixtures. A fixture is a report -//! somebody wrote down, so a fixture-bound oracle checks correspondences over -//! states its author already imagined -- and the defect the oracle exists for -//! was a state nobody had imagined: `topology_report` printing `BUG IN THIS -//! PROBE ... Nothing below about cache partitioning can be trusted` while the -//! verdict two paragraphs below printed `=> agree`. +//! It now means the row agrees with the observation that produced it: every +//! condition `cross_check` found reaches the row, every state that blocks +//! agreement is published, one code per anomaly, and the row is well formed. +//! The two halves being compared are a value and its encoding, not two +//! sentences -- and that is still self-agreement, which is why the target keeps +//! its name. +//! +//! # Why this is not a unit test //! -//! More narrowly, a fixture cannot notice the **renderer** drifting away from -//! the prose labels the oracle looks for. Both sides would still agree with -//! each other; only the real artifact disagrees. +//! The other tests pin the renderer against fixtures. A fixture is an +//! observation somebody wrote down, so it exercises states its author already +//! imagined -- and the defect this file exists for was a state nobody had +//! imagined: `topology_report` printing `BUG IN THIS PROBE ... Nothing below +//! about cache partitioning can be trusted` while the verdict two paragraphs +//! below printed `=> agree`. //! //! Some unit tests in this crate do call `measure()` and so do read this host. -//! What none of them does is run the ORACLE over a report rendered from that -//! reading, which is the gap this file closes. On CI it runs across the hosted -//! runner fleet -- a slow survey of shapes no fixture anticipates. +//! What none of them does is check a report rendered from that reading, which +//! is the gap this file closes. On CI it runs across the hosted runner fleet -- +//! a slow survey of shapes no fixture anticipates. //! //! # It asserts nothing about this machine //! //! Deliberately. A test that expected a processor count, a cache level or a //! verdict would fail on the next runner shape rather than on a defect, and //! would have to be loosened until it asserted nothing. What it checks is that -//! whatever this host produced, the report's parts agree **with each other** -- -//! a property every host must satisfy, including one whose topology cannot be -//! read at all. +//! whatever this host produced, the row accounts for it -- a property every +//! host must satisfy, including one whose topology cannot be read at all. use windows_placement_probe::fingerprint::Fingerprint; use windows_platform_probes::report_oracle; @@ -69,7 +79,7 @@ fn a_report_rendered_from_this_host_is_well_formed() { // including one whose topology cannot be read at all. let (text, measured) = real_report(); - report_oracle::assert_corresponds(&text); + report_oracle::assert_row_is_well_formed(&text); assert!( report_oracle::row(&text).is_some(), "every report carries exactly one row, including an unmeasured one -- \ From d523f2b6a2b72401b66aa2503c72c32001898488 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 22:45:35 -0700 Subject: [PATCH 37/46] fix(platform-probes): close the remaining review findings, and narrow two claims that outran their coverage Sweeping every Copilot review on PR #89 -- seven rounds, 48 suppressed-comment entries, 27 distinct findings once deduplicated -- rather than only the inline comments, which is where most of the substance turned out to be. **`Row::with` could emit a row the crate's own oracle faults.** A repeated top-level key is the one malformation that SURVIVES a consumer's parse: `serde_json` and `JSON.parse` both accept it and keep the last value, so the artifact is not discarded as broken but mined as ambiguous. The writer now refuses it. A panic rather than a `Result` because every caller is a renderer composing a fixed schema, so a repeat is a call-site error, not a condition to handle. **The object requirement in `malformation` was untested, and untestable the way it was reached for.** The corruption corpus mutates one character at a time and none of those can turn an object into a valid NON-object, so reverting `serde_json::Map` to `serde_json::Value` left every test green -- measured, 20 of 21 pass with only the new test red. It is now pinned by calling `malformation` directly, and the comment says plainly that `check` selects rows by a leading `{` so a bare list arrives as `Missing` rather than as a malformed row. Defence in depth, described as such. **Two claims narrowed to what they cover, with the gap queued as M4.1.** `blocking_states` justified its absentees as "derived counts whose only source IS the cross-check's own arithmetic". False for most of them -- `CacheLevelsWithoutPartitions`, `NumaDomainsOnlyInCpuSets`, `CoresOnlyInCpuSets` and eight more read fields sitting on `Observation` in plain sight -- so deleting one of those push sites lets the verdict reach `agree` with `blocking_states` silent. Likewise `every_numa_branch_...` covered the two branches reading `raw_highest_numa_node` while its name claimed the family; it is now `every_numa_counter_branch_...`. Three review rounds reported this against two different wordings. Narrowing a claim is not fixing it, so M4.1 carries the work, including the requirement that no new state land without a corpus shape. Stale statements, each verified before being touched: - PLANS.md described the three diagnostic fields as arrays of condition codes. They are arrays of objects carrying a code plus the variant's values; the bare-code form was superseded M3.1. - DESIGN-NOTES said report_oracle "reads no rendered text at all". The prose reader is gone; the ROW parser is not, and a row is rendered text. - DESIGN-NOTES still queued M3 as future work, which is archived. - My own `## Moved` heading from 6ef80f7 lacked the offset the rule introduced in d20556a. Corrected. - M2.4's item text still asked for correspondence failures and promotion into the M2.1 oracle, both retired. Its re-scoping note was 25 lines above the item, which does not reach someone executing the item. Two findings REJECTED, with reasons rather than silence: - COMPLETED-CHECKLIST's `## Moved 2026-09-12` bare date: the rule says in terms that "existing headings without one are fine and are not worth going back to change", and editing it would breach the file's append-only invariant. Only my own post-rule heading was corrected. - handover.rs `attach->polled`: `drain_to_empty` IS the non-blocking poll, so the span covering it and excluding `wait_and_drain` is what the label and the comment already say. The timestamp half of that finding landed in 3135c09. The design session's "eighteen keys" was likewise left at eighteen and dated instead: it was accurate when written (4d7544e predates 77a83fc, which added `disagreements`), and the finding below it -- that no key existed for the alarm -- turns on the row as it stood. Correcting the number would have turned the record of a diagnosis into an apparent error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- PLANS.md | 2 +- crates/windows-platform-probes/CHECKLIST.md | 48 ++++++++++++++++--- .../COMPLETED-CHECKLIST.md | 2 +- .../windows-platform-probes/DESIGN-NOTES.md | 16 +++++-- ...-2026-09-12-what-the-oracle-should-read.md | 10 +++- .../src/report_oracle/tests.rs | 28 ++++++++++- crates/windows-platform-probes/src/row.rs | 22 +++++++++ .../windows-platform-probes/src/row/tests.rs | 13 +++++ .../src/topology/invariant.rs | 20 ++++++-- .../src/topology/invariant/tests.rs | 13 ++++- 10 files changed, 152 insertions(+), 22 deletions(-) diff --git a/PLANS.md b/PLANS.md index 3ad49451d..8441a8849 100644 --- a/PLANS.md +++ b/PLANS.md @@ -20,7 +20,7 @@ plans tracker: [crates/windows-file-enumeration-sys/PLANS.md](crates/windows-fil | [CHECKLIST-thread-ambient.md](CHECKLIST-thread-ambient.md) | in progress | M22-M23: extract the captured-context composite into `windows-thread-ambient-sys`, a standalone platform layer that captures a thread's ambient state and applies it on another thread. M24-M26: `windows-namespace-request-sys`, marshalable Win32 namespace call parameter sets, over a round-one entry list audited from three real consumers (this repository's watcher and enumeration crates, and `MikeGrier/Globazog-rs`) rather than guessed. M27: `windows-platform-probes`, a durable home for the measurements this workspace's designs rest on, under a three-tier scheme (asserted / ignored / binary-only) where every tier is compiled by an ordinary build. Feature-scoped and deleted when complete; it is the whole of the `mikegrier/thread-ambient` branch's work, and is deliberately separate from the deferred namespace-facility items in [CHECKLIST.md](CHECKLIST.md). | [crates/windows-thread-ambient-sys/DESIGN-NOTES.md](crates/windows-thread-ambient-sys/DESIGN-NOTES.md) | | [crates/windows-overlapped-io-sys/CHECKLIST.md](crates/windows-overlapped-io-sys/CHECKLIST.md) | not started | M14: finish the contract audit -- categories 1, 2, 6, 8, 9 were not examined -- and sweep `outstanding()` for the advisory-predicate hazard. | [crates/windows-overlapped-io-sys/DESIGN-NOTES.md](crates/windows-overlapped-io-sys/DESIGN-NOTES.md) | | [crates/windows-ioring-sys/CHECKLIST.md](crates/windows-ioring-sys/CHECKLIST.md) | in progress | Memory-safe Rust over the Windows `IoRing` submission/completion ring, as a new crate. M1-M7 (ring lifecycle through the `ring-copy` topology-aligned sample) are complete and archived. The parked, pinned-thread `M6+` work and the new M10 contract audit remain. | [crates/windows-ioring-sys/DESIGN-NOTES.md](crates/windows-ioring-sys/DESIGN-NOTES.md) | -| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row published its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` could not tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. M3 is complete and archived (ten items): those three fields publish arrays of condition codes; the surviving correspondences became invariants over the observation rather than over two renderings, so a rule that reads the diagnostic lists -- which would be a restatement of `verdict()` and blind to a deleted push site -- was rewritten to read the observation; the row is emitted from a typed value through one writer with total escaping, which is the crate's only defence against caller text reaching the mined artifact; the prose oracle and every parser serving it were deleted, and no test extracts structured data from prose anywhere in the crate. Four later items came from reviews and are the more instructive half: three instruments were found asserting less than their names claimed, `BlockingState::ALL` was found to be a census the compiler did not check despite a doc comment claiming it did, and the row's hand-written JSON well-formedness check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a FALSE accept -- and replaced by `serde_json`, after which the remaining hand-written string scanners were deleted too. What remains is M4 (four M2 leftovers M3 gated, now unblocked and re-scoped) and M5 (six ungated hygiene items). | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | +| [crates/windows-platform-probes/CHECKLIST.md](crates/windows-platform-probes/CHECKLIST.md) | in progress | M1 (streaming reports) is done and archived: every probe now writes into the sink as it measures, through a `fmt::Write` adapter that left all 332 `writeln!` call sites untouched, and the `catch_unwind`/`resume_unwind` pair is gone because there is no longer a buffer to rescue. Measured with a control -- a probe killed 300 ms into a 0.8 s run keeps its banner and heading on six runs of six, where the previous build kept nothing on six of six. M2 built the report oracle -- one executable definition of the correspondences between a report's prose and NDJSON halves, bound inside the renderers so every test that renders inherits it -- along with a derived fact set and a corpus of report shapes. It is complete; its ten unrelated leftovers -- CI hygiene, a doc repair, probe-prose corrections -- were re-sequenced into M4 (gated on M3) and M5 (gated on nothing). M3 then supersedes its central rule. Re-reading M2's own evidence showed that both defects which motivated the oracle were defects in the ENCODED ROW, not in the relation between two renderings, and that the row published its three diagnostic lists as bare counts -- so a survey reading `"parse_incomplete":1` could not tell a probe self-bug from host flakiness. The row is the machine contract and gets the facts and the invariants; the prose is for a reader and gets review. M3 is complete and archived (ten items): those three fields publish arrays of OBJECTS, each carrying a stable `code` plus the values its variant holds -- `{"code":"partitioning_summary_missing","level":9}` rather than the bare `"partitioning_summary_missing"` of the superseded M3.1 form; the surviving correspondences became invariants over the observation rather than over two renderings, so a rule that reads the diagnostic lists -- which would be a restatement of `verdict()` and blind to a deleted push site -- was rewritten to read the observation; the row is emitted from a typed value through one writer with total escaping, which is the crate's only defence against caller text reaching the mined artifact; the prose oracle and every parser serving it were deleted, and no test extracts structured data from prose anywhere in the crate. Four later items came from reviews and are the more instructive half: three instruments were found asserting less than their names claimed, `BlockingState::ALL` was found to be a census the compiler did not check despite a doc comment claiming it did, and the row's hand-written JSON well-formedness check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a FALSE accept -- and replaced by `serde_json`, after which the remaining hand-written string scanners were deleted too. What remains is M4 (four M2 leftovers M3 gated, now unblocked and re-scoped) and M5 (six ungated hygiene items). | [crates/windows-platform-probes/DESIGN-NOTES.md](crates/windows-platform-probes/DESIGN-NOTES.md#d-streaming-report), [#d-encoded-row-is-the-contract](crates/windows-platform-probes/DESIGN-NOTES.md#d-encoded-row-is-the-contract) | | [CHECKLIST-mutation-survivors.md](CHECKLIST-mutation-survivors.md) | not started | Work queued from the workspace-wide cargo-mutants sweep of 2026-09-02, whose findings are kept in [mutation-sweeps/2026-09-02/](mutation-sweeps/2026-09-02/README.md) rather than re-derived -- the run took roughly fourteen hours. 2,792 caught, 1,112 survived, 198 timed out. **The headline numbers mislead in three ways and the README says how**: a timeout in a blocking-API crate is usually a detection that lost its name rather than a gap (measured: one of `windows-waitable-queues`' 120 timeouts fails four tests in 0.00s when re-injected alone), a low score on an executable probe crate is measuring the wrong thing, and three kinds of survivor -- equivalent mutants, unreachable code, and constants that want a `const` assertion -- are not missing tests at all. M1 covers the shipping crates; M2 holds the two crates that are not libraries and whose scope is an engineer's decision; M3 re-runs and prunes rather than hand-editing the tool's output into a second source of truth. | [mutation-sweeps/2026-09-02/README.md](mutation-sweeps/2026-09-02/README.md) | Add a row here when new work is planned, against [CHECKLIST.md](CHECKLIST.md) or any crate's. diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 82d09e88d..f79149251 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -41,18 +41,54 @@ so renumbering would leave dangling references in a file that may not be edited Stable IDs cost a mismatch between an item number and its milestone; renumbering would cost correctness in the archive. -- [ ] **M2.4** -- Explore, with the sparse matrix as the instrument, whether the same correspondence - failures exist for `Coherence`, `BracketOutcome` and `Verdict`, and in the sibling probes' renderers. - Expect the matrix to be mostly empty; that is the expected shape and not a sign the exercise failed. - **Record the vacuous results as well as the findings** -- "X and Y were examined and need not - correspond" is what stops the next person re-exploring the same cells, and is the half that normally - evaporates. Promote only what proves meaningful into the oracle from M2.1. +- [ ] **M2.4** -- Explore, with the sparse matrix as the instrument, whether `Coherence`, + `BracketOutcome` and `Verdict` carry invariants the row does not yet publish -- as VALUES on the + observation, not as correspondences between two renderings -- and whether the sibling probes' + renderers have the same gaps. Expect the matrix to be mostly empty; that is the expected shape and + not a sign the exercise failed. **Record the vacuous results as well as the findings** -- "X and Y + were examined and need not be related" is what stops the next person re-exploring the same cells, + and is the half that normally evaporates. Promote only what proves meaningful into the invariant + set from M3.2. + + Re-scoped by M3.2; the note at the top of this milestone gives the reasoning. **The item text + above was rewritten on 2026-09-13 to match**: it still asked for "the same correspondence + failures" and for promotion "into the oracle from M2.1", both retired by M3, so a reader working + the list linearly would have been sent after the half that no longer exists. Found by a review -- + and the lesson generalises, since a re-scoping note 25 lines above an item does not reach someone + executing the item. > **-> OPEN QUESTION for the engineer:** M2.4 may show this generalises past this crate, in which case > the oracle belongs somewhere shared and the question becomes a repository-wide convention rather than > a probe-crate one. That is a design decision, not a mechanical follow-on, and is deliberately left > unanswered here. +- [ ] **M4.1** -- Model the observation-readable `ParseIncomplete` conditions that + `blocking_states` currently omits, so a deleted push site in `cross_check` is caught for all of + them rather than for the subset. + + **Gap:** `blocking_states` justified its absentees as "derived counts whose only source IS the + cross-check's own arithmetic". That is false for most of them -- + `CacheLevelsWithoutPartitions`, `NumaDomainsOnlyInCpuSets`, `CoresOnlyInCpuSets`, + `RelationsWithoutProcessors`, `UnreportedRelations`, `DescribedRelations`, + `OverlappingWalkRelations`, `ProcessorAttributeConflicts`, `NumaDomainsWithConflictingLabels`, + `NumaDomainsUnreported` and `MeasuredButCountsAbsent` all read fields sitting on `Observation` + in plain sight. Deleting one of those push sites lets `verdict()` reach `agree` with + `blocking_states` silent. Reported across three review rounds against two wordings of the claim; + the claims in [src/topology/invariant.rs](src/topology/invariant.rs) and in + `every_numa_counter_branch_...` were narrowed on 2026-09-13 to stop overstating the coverage, + which is why this item is the fix rather than the discovery. + + **Target:** each gains a `BlockingState` variant, a `blocking_states` branch, a `codes_for` arm, + a perturbation in the invariant tests, and a corpus shape in + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs) so + `the_corpus_reaches_every_blocking_state` still holds. **Do not add a variant without its corpus + shape** -- that test is what stops the mapping being written and never exercised, which is the + defect this whole area keeps producing. + + Apply the tautology test to each before adding it: a state whose only source is the cross-check's + own arithmetic does NOT belong, and the honest outcome for such a one is a line in the module + header saying so by name rather than a silent absence. + - [ ] **M2.5** -- Make the banner describe the read the body describes. A probe run performs **three** independent `MachineMemoryTopology::discover()` calls: `Fingerprint::discover()` for the banner, `measure()`'s own discovery for the body, and `Fingerprint::discover()` again. `attribution` diff --git a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md index 28a4b0ab6..93044d433 100644 --- a/crates/windows-platform-probes/COMPLETED-CHECKLIST.md +++ b/crates/windows-platform-probes/COMPLETED-CHECKLIST.md @@ -1028,7 +1028,7 @@ M4 below, six in M5. M2.18 is the exception, dissolved rather than moved. Default build re-verified by binary inspection: neither `serde` nor `serde_json` appears on a normal dependency edge, and `probe-topology.exe` contains no parser string. -## Moved 2026-09-13 -- M2.14: making the two authoring rules this branch earned actually bite +## Moved 2026-09-13 22:03:09 -07:00 -- M2.14: making the two authoring rules this branch earned actually bite ### M2.14 -- Make the two authoring rules this branch earned actually bite. *(completed 2026-09-13 22:03:09 UTC-07:00)* diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index e85c7213c..c5f275816 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1177,10 +1177,14 @@ repository instructions describe.) Counted in `src/report_oracle.rs` **as it stood before this decision**: of 38 top-level functions, ten were correspondence rules and four were comparison helpers. **Twenty-three existed only to extract values back out of rendered -text.** None of them survives: [src/report_oracle.rs](src/report_oracle.rs) reads -no rendered text at all now, and hand-writes no string scanning -- the row's +text.** None of them survives: [src/report_oracle.rs](src/report_oracle.rs) reads no +rendered PROSE at all now, and hand-writes no string scanning -- the row's well-formedness is a `serde_json` parse and its keys come from that parser's own -tokens. So the counts above are what the design cost, not what the file holds. +tokens. (This said "reads no rendered text at all", which a review correctly read +as contradicting the module: the prose reader is gone, the ROW parser is not, and +the row is rendered text. What changed is that nothing here infers a value from a +sentence -- the one parse left is of a format with a specification, performed by a +library rather than by this crate.) So the counts above are what the design cost, not what the file holds. (They are also the only counts kept here, because they describe a file that no longer exists in that form and so cannot drift; a count of the CURRENT file would be a census, and is deliberately absent.) @@ -1240,6 +1244,8 @@ observation, losing the parser in front of them. The containment work in decision, not less, because what it keeps out is now keeping it out of the contract artifact. -The work this implies is queued as M3 in [CHECKLIST.md](CHECKLIST.md). The -session that produced it is +The work this implies was M3, which is complete and archived in +[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). (This said "is queued as M3 in +CHECKLIST.md" until a review pointed out that the canonical design note was +advertising landed work as pending.) The session that produced it is [design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md](design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md). diff --git a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md index ca3b63d2f..a7cea84f1 100644 --- a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md +++ b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md @@ -41,7 +41,15 @@ Checked during the session, in the code as it stands on `main`: - The alarm is emitted by a `writeln!` into the prose, in `report`'s `PartitioningCache::Level` arm of [src/topology_report.rs](../src/topology_report.rs). -- The NDJSON row carries eighteen keys: `reason`, `arch`, `processors`, `groups`, +- The NDJSON row carries eighteen keys **as of this session** -- it carries nineteen now, + because the work this session set off added `disagreements` (in `77a83fc`, after + [4d7544e](../DESIGN-NOTES.md) recorded this). The count is left at eighteen on purpose: a + review reported it as a stale census, and it is not one. The diagnosis below turns on + what the row held AT THE TIME, so correcting the number to nineteen would make the next + bullet -- "there is no key for the alarm" -- read as an error rather than as the finding. + For the current schema read `MEASURED_ROW_KEYS` in + [src/topology_report.rs](../src/topology_report.rs), which is the one authority: + `reason`, `arch`, `processors`, `groups`, `packages`, `numa_domains`, `numa_domains_without_processors`, `cores`, `efficiency_classes`, `caches`, `outermost_partitioning_cache_level`, `outermost_partitioning_cache`, `policies`, `cross_check`, `not_compared`, diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index a89664278..a3e7386a6 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -7,7 +7,7 @@ //! ignore the instrument -- and the reports this runs against are the ones a //! fleet survey mines, so a false alarm is a false finding about a host. -use super::{RowDefect, check, keys, row}; +use super::{RowDefect, check, keys, malformation, row}; /// A well-formed row, in the shape the renderer emits. fn clean_row() -> String { @@ -96,6 +96,32 @@ fn a_trailing_separator_is_a_defect() { ); } +#[test] +fn valid_json_that_is_not_an_object_is_a_malformation() { + // **Pins the `Map` in `malformation`, which the corpus cannot reach.** The + // generated corruptions are one-character mutations of a row, and none can + // turn an object into a valid NON-object -- so a regression from + // `serde_json::Map` to `serde_json::Value` would have left every test green. + // Reported by a review. + // + // Called directly rather than through `check`, because `check` selects rows + // by a leading `{` and these never get that far: through the public path a + // bare list is `RowDefect::Missing`, not a malformed row. That makes the + // requirement defence in depth rather than a reachable case -- said plainly + // here, because the comment beside it reads as though `[1,2]` arrives, and + // the honest claim is that the type is what stops it ever mattering. + for not_an_object in [r#"[1,2]"#, "null", "3", r#""a string""#, "true"] { + assert!( + malformation(not_an_object).is_some(), + "{not_an_object} is valid JSON but carries no keys, so it is not a row" + ); + } + + // The control: the same call accepts an object, so the assertions above are + // not passing merely because `malformation` rejects everything. + assert_eq!(malformation(&clean_row()), None); +} + #[test] fn a_mismatched_closing_delimiter_is_a_defect() { // Also balanced by depth, also invalid: an object closed by a bracket. diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs index 9337e73e5..849b127a6 100644 --- a/crates/windows-platform-probes/src/row.rs +++ b/crates/windows-platform-probes/src/row.rs @@ -180,8 +180,30 @@ impl Row { /// /// Takes the name and the value together, which is the point: they cannot be /// reordered apart, and there is no position to miscount. + /// + /// # Panics + /// + /// If `name` is already present. A repeated top-level key is the one + /// malformation that SURVIVES a consumer's parse -- `serde_json` and + /// `JSON.parse` both accept it and silently keep the last value -- so a row + /// carrying one is not a broken artifact a survey discards but an ambiguous + /// one it mines, which is worse. The crate reports it as + /// [`RowDefect::RepeatedKey`](crate::report_oracle::RowDefect::RepeatedKey); + /// this is the writer being unable to produce it in the first place. + /// + /// **Why a panic and not a `Result`.** Every caller is a renderer in this + /// crate composing a fixed schema, so a repeat is a programming error at the + /// call site, not a condition to handle -- and a fallible builder would put + /// a `?` on nineteen infallible calls to describe a case that must never + /// happen. Reported by a review, which observed that this public writer + /// could emit a row the crate's own oracle faults. #[must_use] pub fn with(mut self, name: &'static str, value: impl Into) -> Self { + assert!( + !self.members.iter().any(|(present, _)| *present == name), + "the row already carries `{name}`, and a repeated key survives a \ + consumer's parse as whichever value happened to come last" + ); self.members.push((name, value.into())); self } diff --git a/crates/windows-platform-probes/src/row/tests.rs b/crates/windows-platform-probes/src/row/tests.rs index 260c6766f..9b329e6f0 100644 --- a/crates/windows-platform-probes/src/row/tests.rs +++ b/crates/windows-platform-probes/src/row/tests.rs @@ -4,6 +4,19 @@ use super::{Row, Value}; +#[test] +#[should_panic(expected = "the row already carries `processors`")] +fn the_writer_refuses_to_render_a_key_twice() { + // **The one malformation that survives a consumer's parse**, so the writer + // is made unable to produce it rather than the oracle being left to notice. + // `serde_json` and `JSON.parse` both accept a repeated key and keep the + // last, which turns a broken row into an AMBIGUOUS one -- mined rather than + // discarded. Reported by a review against this public builder. + let _ = Row::new("x") + .with("processors", 16_usize) + .with("processors", 32_usize); +} + #[test] fn a_row_renders_its_members_in_the_order_they_were_added() { let row = Row::new("x-probe-topology") diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 0599977c5..426c800d7 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -240,11 +240,21 @@ impl fmt::Display for Violation { /// enumerating them separately is that a deleted push site leaves the state /// here and the list empty -- so this is what notices, and the list could not. /// -/// Not every entry in `parse_incomplete` appears here, and that is deliberate -/// rather than an omission: some are derived counts whose only source IS the -/// cross-check's own arithmetic, so restating them would be the tautology this -/// module exists to avoid. What belongs here is a state readable from the -/// observation on its own terms. +/// **The converse does NOT hold, and the wording here used to imply it did.** +/// This said the absentees were "derived counts whose only source IS the +/// cross-check's own arithmetic", which would make their exclusion principled. +/// It is false for most of them: `CacheLevelsWithoutPartitions`, +/// `NumaDomainsOnlyInCpuSets`, `CoresOnlyInCpuSets`, `RelationsWithoutProcessors` +/// and the rest read fields that sit on [`Observation`] in plain sight. So +/// deleting one of THOSE push sites is not caught here -- the verdict can reach +/// `agree` with `blocking_states` silent -- and the guarantee above covers the +/// states actually listed below, not every condition `cross_check` can find. +/// Reported by three review rounds against two different variants of the claim. +/// +/// The tautology argument is still the right test for what belongs here; it +/// simply was not what excluded these. Closing the gap is queued as **M4.1** in +/// [CHECKLIST.md](../../CHECKLIST.md) rather than recorded only here, because a +/// decision written in a comment schedules nothing. #[must_use] pub fn blocking_states(observation: &Observation) -> Vec { let mut states = Vec::new(); diff --git a/crates/windows-platform-probes/src/topology/invariant/tests.rs b/crates/windows-platform-probes/src/topology/invariant/tests.rs index 6d537876c..b5fc36149 100644 --- a/crates/windows-platform-probes/src/topology/invariant/tests.rs +++ b/crates/windows-platform-probes/src/topology/invariant/tests.rs @@ -575,14 +575,23 @@ fn an_agreeing_verdict_requires_the_numa_counter_to_have_matched() { } #[test] -fn every_numa_branch_in_the_real_cross_check_is_one_this_module_forbids() { +fn every_numa_counter_branch_in_the_real_cross_check_is_one_this_module_forbids() { // **The claim in this module's header, checked rather than asserted**: a // push site deleted from `cross_check` fires a rule here. For each NUMA - // branch, the verdict the real `cross_check` draws must already be + // COUNTER branch, the verdict the real `cross_check` draws must already be // something other than `agree`, AND this module must forbid `agree` for the // same observation -- so the rule pins behaviour that exists rather than // demanding behaviour that does not. // + // **Named for the counter on purpose.** This was + // `every_numa_branch_...`, which was false: `cross_check` also pushes + // `NumaDomainsOnlyInCpuSets`, `NumaDomainsUnreported` and + // `NumaDomainsWithConflictingLabels`, none of which has a `BlockingState`, + // so deleting one of those push sites is not caught. A review found the + // name claiming the whole family while the body covered the two branches + // that read `raw_highest_numa_node`. The absentees are queued as M4.1; the + // name now says which half is guarded. + // // This is the pairing the other states get from // `every_blocking_state_is_one_the_real_cross_check_already_reports`; NUMA // had neither half until a review found the header's claim was false for From 40ea4f35c3cf5728d87270c248dde2d86736c187 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:06:38 -0700 Subject: [PATCH 38/46] feat(platform-probes): pin the row's value SHAPES, not just its key names `MEASURED_ROW_KEYS` pinned which names a row carries and in what order, and said nothing about what those names hold. So the machine contract this branch exists to establish was half-stated: a renderer could publish `"processors":"16"` and satisfy the key test, the well-formedness parse and every renderer assertion at once. Measured before changing anything, because a review reported it as reachable rather than as a defect it had found: rendering that one field through `.to_string()` -- a number becoming a string in the mined artifact -- left ALL 230 library tests and ALL 10 real-host integration tests green. `Shape` is the type-level counterpart of `Value`, and states each key's JSON shape: `Text`, `Number`, `NumberOrNull` for the genuinely-absent case, `ListOfNumbers`, `ListOfObjects`, `ObjectOfNumbers`, and `ListOfCoded` for the four diagnostic lists -- every element an object carrying a string `code`, because `code` is the discriminant a survey groups by and an entry without one is unmineable while still being valid JSON. **Stated independently rather than derived from the renderer.** A shape read back out of the `Value` the renderer produced would move whenever the renderer moved and so could never disagree with it. This is the same reasoning as the diagnostic goldens, and the same census-versus-schema distinction: a schema is not derivable from the thing it constrains, so writing it down is what makes it checkable at all. `row_schema!` declares names and shapes as ONE list and derives the key const from it, so a key cannot gain a shape without gaining a name or the reverse -- a `_KEYS` const beside a `_SHAPES` const would have been two statements of one schema, which is the restatement this crate keeps paying for. **Bound in both renderers**, beside the well-formedness assertion and for the same reason: every test that renders inherits it, including the ones nobody thought to write. Measured with the same sabotage after the change -- 21 library tests and 6 integration tests now fail, against 0 before -- and the failure names the defect directly: `processors` should be Number but is `"4"` Both schemas are exercised. The unmeasured row is its own schema rather than a subset, and a check that only saw the measured one would leave the failed-discovery artifact -- the row a fleet survey sees most on a broken host -- unconstrained. Two further findings from the same review were REJECTED, both claiming this branch fails to compile: that `match anomaly.kind` and `match observation.coherence` move a non-`Copy` field out of a shared reference. They do not. Every arm is a wildcard or `{ .. }`, which binds nothing, and the two arms that do bind take `usize` fields by copy. `cargo check --all-targets` is clean across the workspace and CI has been green on these lines for several commits. Verified by building rather than by reading, which is what the repository's Rust baseline rules require before reporting a compile error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/report_oracle.rs | 79 ++++++++++ crates/windows-platform-probes/src/row.rs | 37 +++++ crates/windows-platform-probes/src/tests.rs | 70 +++++++++ .../src/topology_report.rs | 142 ++++++++++++------ 4 files changed, 282 insertions(+), 46 deletions(-) diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index ff954dac7..7697f7595 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -293,6 +293,85 @@ pub fn row(report: &str) -> Option<&str> { report.lines().find(|line| line.starts_with('{')) } +use crate::row::Shape; + +/// Every way `row` departs from `schema`'s value SHAPES, as sentences. +/// +/// **The half the key contract was missing.** `MEASURED_ROW_KEYS` pins which +/// names appear and in what order, and says nothing about what they hold -- so +/// a renderer could publish `"processors":"16"` and satisfy the key test, the +/// well-formedness check and every renderer assertion at once. Measured before +/// this existed: rendering that one field through `.to_string()` left all 230 +/// library tests and all 10 real-host integration tests green. Reported by a +/// review. +/// +/// Keys are not re-checked here; that is the key test's job, and doing it in +/// both places would make one of them the copy. A key the schema names and the +/// row lacks is reported, because a shape cannot be checked against nothing. +#[must_use] +pub fn shape_violations(row: &str, schema: &[(&str, Shape)]) -> Vec { + use serde_json::Value as Json; + + let Ok(parsed) = serde_json::from_str::>(row) else { + return vec![format!("the row is not a JSON object: {row}")]; + }; + + let is_number = |value: &Json| value.is_u64() || value.is_i64(); + let coded = |value: &Json| { + value + .as_object() + .is_some_and(|entry| entry.get("code").is_some_and(Json::is_string)) + }; + + let mut found = Vec::new(); + for (name, shape) in schema { + let Some(value) = parsed.get(*name) else { + found.push(format!("`{name}` is missing, so its shape cannot hold")); + continue; + }; + + let holds = match shape { + Shape::Text => value.is_string(), + Shape::Number => is_number(value), + Shape::NumberOrNull => is_number(value) || value.is_null(), + Shape::ListOfNumbers => value.as_array().is_some_and(|l| l.iter().all(is_number)), + Shape::ListOfObjects => value + .as_array() + .is_some_and(|l| l.iter().all(Json::is_object)), + Shape::ListOfCoded => value.as_array().is_some_and(|l| l.iter().all(coded)), + Shape::ObjectOfNumbers => value + .as_object() + .is_some_and(|o| o.values().all(&is_number)), + }; + + if !holds { + found.push(format!("`{name}` should be {shape:?} but is `{value}`")); + } + } + + found +} + +/// [`shape_violations`], as an assertion. +/// +/// # Panics +/// +/// Panics listing every value whose shape the schema forbids. +pub fn assert_row_has_the_schemas_shapes(report: &str, schema: &[(&str, Shape)]) { + let row = row(report).unwrap_or_else(|| panic!("no single well-formed row in:\n{report}")); + let violations = shape_violations(row, schema); + assert!( + violations.is_empty(), + "the row departs from its schema in {} way(s):\n{}\n\n--- the row ---\n{row}", + violations.len(), + violations + .iter() + .map(|what| format!(" - {what}")) + .collect::>() + .join("\n"), + ); +} + /// [`check`], as an assertion, for tests that render a report. /// /// **Named `assert_corresponds` until 2026-09-13, and the name outlived what it diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs index 849b127a6..777d634fd 100644 --- a/crates/windows-platform-probes/src/row.rs +++ b/crates/windows-platform-probes/src/row.rs @@ -34,6 +34,43 @@ use std::fmt::Write as _; #[cfg(test)] mod tests; +/// The shape a row's value must have, as a schema states it. +/// +/// **The type-level counterpart of [`Value`], and the half the key list was +/// missing.** A schema of names alone pins WHICH fields a row carries and says +/// nothing about what they hold, so a renderer could publish `"processors"` as +/// a string and satisfy every check the crate had. Measured, before this +/// existed: `.with("processors", observation.online_processors.to_string())` +/// left all 230 library tests and all 10 real-host integration tests green. +/// Reported by a review. +/// +/// **Stated independently rather than derived from the renderer**, which is the +/// same reasoning as the diagnostic goldens: a shape read back out of the +/// `Value` the renderer produced would move whenever the renderer moved, and so +/// could never disagree with it. A schema is not derivable from the thing it +/// constrains -- writing it down twice is what makes it a schema rather than a +/// restatement. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Shape { + /// A JSON string. + Text, + /// A JSON number. + Number, + /// A number, or `null` where the answer is genuinely "there is none". + NumberOrNull, + /// A list whose every element is a number. + ListOfNumbers, + /// A list whose every element is an object. + ListOfObjects, + /// A list whose every element is an object carrying a string `code`. + /// + /// The diagnostic lists. `code` is the stable discriminant a survey groups + /// by, so an entry without one is unmineable even though it is valid JSON. + ListOfCoded, + /// An object whose every member is a number. + ObjectOfNumbers, +} + /// A value the row can carry. /// /// Deliberately not every JSON shape: there is no floating point, because every diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 9d1310bbc..a1f722574 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4854,6 +4854,76 @@ fn the_unmeasured_row_carries_exactly_its_own_contracts_keys() { ); } +#[test] +fn every_row_value_has_the_shape_its_schema_declares() { + // **The half the key contract could not state.** `MEASURED_ROW_KEYS` pins + // which names appear and in what order and says nothing about what they + // hold, so a renderer could publish `"processors":"16"` and satisfy the key + // test, the well-formedness check and every renderer assertion at once. + // Measured: rendering that one field through `.to_string()` left all 230 + // library tests and all 10 real-host integration tests green. Found by a + // review. + // + // Both shapes, because the unmeasured row is its own schema rather than a + // subset, and a check that only ever saw the measured one would leave the + // failed-discovery artifact -- the one a fleet survey sees most on a broken + // host -- unconstrained. + crate::report_oracle::assert_row_has_the_schemas_shapes( + &crate::topology_report::report(BANNER, &clean_observation()), + crate::topology_report::MEASURED_ROW_SHAPES, + ); + + crate::report_oracle::assert_row_has_the_schemas_shapes( + &crate::topology_report::report_unmeasured( + BANNER, + &std::io::Error::other("the device is not ready"), + ), + crate::topology_report::UNMEASURED_ROW_SHAPES, + ); +} + +#[test] +fn a_diagnostic_entry_without_a_code_is_a_shape_violation() { + // The control for `ListOfCoded`, and the reason it is not merely + // `ListOfObjects`: `code` is the stable discriminant a survey groups by, so + // an entry lacking one is unmineable while still being valid JSON. + let row = crate::row::Row::new("x-probe-topology") + .with( + "parse_incomplete", + crate::row::Value::List(vec![crate::row::Value::Object(vec![( + "level", + crate::row::Value::Number(9), + )])]), + ) + .render(); + + let violations = crate::report_oracle::shape_violations( + &row, + &[("parse_incomplete", crate::row::Shape::ListOfCoded)], + ); + assert_eq!(violations.len(), 1, "{violations:?}"); + assert!(violations[0].contains("parse_incomplete"), "{violations:?}"); + + // And the same list WITH a code is accepted, so the rule is not simply + // rejecting every list of objects. + let coded = crate::row::Row::new("x-probe-topology") + .with( + "parse_incomplete", + crate::row::Value::List(vec![crate::row::Value::Object(vec![( + "code", + crate::row::Value::Text("not_measured".to_owned()), + )])]), + ) + .render(); + assert_eq!( + crate::report_oracle::shape_violations( + &coded, + &[("parse_incomplete", crate::row::Shape::ListOfCoded)] + ), + Vec::::new() + ); +} + #[test] fn the_two_row_shapes_are_distinguishable_by_their_keys() { // The guard that keeps the two schemas from drifting into each other. If diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index e0d1fe1df..de9372790 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -19,54 +19,96 @@ use std::io; use windows_placement_probe::fingerprint::{Fingerprint, banner_line_for}; -use crate::row::{Row, Value}; +use crate::row::{Row, Shape, Value}; -/// Every key a MEASURED topology row carries, in order. +/// Declares a row schema once, as names WITH shapes, and derives the key list. /// -/// **This is the contract, not a census of the code.** The anti-census rule this -/// crate keeps relearning is about restating facts that can be DERIVED -- a -/// count of placeholders, a tally of variants. A schema is not derivable from -/// anything: it IS the agreement with the survey that reads these rows, so -/// writing it down is what makes it checkable at all. -/// -/// It was missing, and the gap was measured: with `packages` deleted from the -/// builder entirely, the whole suite stayed green. `Row::keys` reports what the -/// builder happened to supply, so a test comparing the two only ever showed the -/// reader and the writer agreeing with each other -- never that a field the -/// survey depends on is still there. Found by a review. -/// -/// Changing this list is a breaking change to the row, and -/// `the_measured_row_carries_exactly_the_contracts_keys` is what makes that -/// visible in a diff rather than in a mining pass six months later. -pub const MEASURED_ROW_KEYS: &[&str] = &[ - "reason", - "arch", - "processors", - "groups", - "packages", - "numa_domains", - "numa_domains_without_processors", - "cores", - "efficiency_classes", - "caches", - "outermost_partitioning_cache_level", - "outermost_partitioning_cache", - "policies", - "cross_check", - "disagreements", - "not_compared", - "parse_incomplete", - "enumeration_anomalies", - "numa_domains_only_in_cpu_sets", -]; - -/// Every key an UNMEASURED topology row carries, in order. -/// -/// Deliberately short, and deliberately its own schema rather than a subset of -/// the one above: a row from a host whose discovery FAILED is a different shape, -/// and a survey must be able to tell it from a measured row that happens to be -/// missing fields. -pub const UNMEASURED_ROW_KEYS: &[&str] = &["reason", "arch", "cross_check", "discovery_error"]; +/// One list, so a key cannot gain a shape without gaining a name or the reverse. +/// The alternative -- a `_KEYS` const beside a `_SHAPES` const -- is two +/// statements of one schema, which is the restatement this crate keeps paying +/// for. +macro_rules! row_schema { + ( + $(#[$keys_doc:meta])* $keys:ident, + $(#[$shapes_doc:meta])* $shapes:ident { $($name:literal => $shape:expr,)+ } + ) => { + $(#[$keys_doc])* + pub const $keys: &[&str] = &[$($name,)+]; + + $(#[$shapes_doc])* + pub const $shapes: &[(&str, Shape)] = &[$(($name, $shape),)+]; + }; +} + +row_schema!( + /// Every key a MEASURED topology row carries, in order. + /// + /// **This is the contract, not a census of the code.** The anti-census rule + /// this crate keeps relearning is about restating facts that can be DERIVED + /// -- a count of placeholders, a tally of variants. A schema is not + /// derivable from anything: it IS the agreement with the survey that reads + /// these rows, so writing it down is what makes it checkable at all. + /// + /// It was missing, and the gap was measured: with `packages` deleted from + /// the builder entirely, the whole suite stayed green. `Row::keys` reports + /// what the builder happened to supply, so a test comparing the two only + /// ever showed the reader and the writer agreeing with each other -- never + /// that a field the survey depends on is still there. Found by a review. + /// + /// Changing this list is a breaking change to the row, and + /// `the_measured_row_carries_exactly_the_contracts_keys` is what makes that + /// visible in a diff rather than in a mining pass six months later. + MEASURED_ROW_KEYS, + /// The same schema with each key's value SHAPE, which is the half the key + /// list alone could not state. + /// + /// Names and order say WHICH fields a row carries; they say nothing about + /// what those fields hold. Measured before this existed: publishing + /// `processors` through `.to_string()` -- a number becoming a string in the + /// mined artifact -- left all 230 library tests and all 10 real-host + /// integration tests green. Found by a review. + /// + /// Changing a shape here is a breaking change to the row exactly as + /// changing a name is. + MEASURED_ROW_SHAPES +{ + "reason" => Shape::Text, + "arch" => Shape::Text, + "processors" => Shape::Number, + "groups" => Shape::Number, + "packages" => Shape::Number, + "numa_domains" => Shape::Number, + "numa_domains_without_processors" => Shape::Number, + "cores" => Shape::Number, + "efficiency_classes" => Shape::ListOfNumbers, + "caches" => Shape::ListOfObjects, + "outermost_partitioning_cache_level" => Shape::NumberOrNull, + "outermost_partitioning_cache" => Shape::Text, + "policies" => Shape::ObjectOfNumbers, + "cross_check" => Shape::Text, + "disagreements" => Shape::ListOfCoded, + "not_compared" => Shape::ListOfCoded, + "parse_incomplete" => Shape::ListOfCoded, + "enumeration_anomalies" => Shape::ListOfCoded, + "numa_domains_only_in_cpu_sets" => Shape::Number, +}); + +row_schema!( + /// Every key an UNMEASURED topology row carries, in order. + /// + /// Deliberately short, and deliberately its own schema rather than a subset + /// of the one above: a row from a host whose discovery FAILED is a + /// different shape, and a survey must be able to tell it from a measured + /// row that happens to be missing fields. + UNMEASURED_ROW_KEYS, + /// The same schema with each key's value shape. + UNMEASURED_ROW_SHAPES +{ + "reason" => Shape::Text, + "arch" => Shape::Text, + "cross_check" => Shape::Text, + "discovery_error" => Shape::Text, +}); use crate::topology::diagnostic::{described, published_anomaly}; use crate::topology::{ Disagreement, NotCompared, Observation, ParseIncomplete, PartitioningCache, Verdict, @@ -331,6 +373,9 @@ pub fn report_unmeasured(banner: &str, error: &io::Error) -> String { // well formed, which is not a thing a renderer can make less of. #[cfg(any(test, feature = "oracle-in-renderer"))] crate::report_oracle::assert_row_is_well_formed(&out); + // The SHAPES too, for the reason on eport below: names alone leave a + // consumer's field types unconstrained. + crate::report_oracle::assert_row_has_the_schemas_shapes(&out, UNMEASURED_ROW_SHAPES); out } @@ -933,5 +978,10 @@ pub fn report(banner: &str, observation: &Observation) -> String { // build is the one that matters and is the one pinned above. #[cfg(any(test, feature = "oracle-in-renderer"))] crate::report_oracle::assert_row_is_well_formed(&out); + // **And the schema's SHAPES, bound here for the same reason.** Well-formed + // says the row parses; the schema says processors is a number and each + // diagnostic entry carries a code. Measured: publishing processors`n // through .to_string() left all 230 library tests and all 10 real-host + // integration tests green before this line existed. Found by a review. + crate::report_oracle::assert_row_has_the_schemas_shapes(&out, MEASURED_ROW_SHAPES); out } From 80fe75d8b19ffaf92469abf1d89301e18fe090e5 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:10:25 -0700 Subject: [PATCH 39/46] docs(platform-probes): retire the "the key set is derivable" claim, which four places still made M3.4 concluded that a typed `Row` made the contract's key set derivable, so no expected-key list was needed. That was wrong, and the branch already knew it -- `MEASURED_ROW_KEYS` exists precisely because `Row::keys` reports what the builder happened to supply, so a row missing a required field is still perfectly self-consistent. Measured when the const was added: with `.with("packages", ...)` deleted from the renderer, the whole suite stayed green. The correction never propagated. A review found the superseded claim still standing in four places -- `row.rs`'s module header, the `row` unit test that demonstrates `keys`, the round-trip test in `tests.rs`, and by implication any future edit reading them -- each steering a reader back toward the vacuous reader-against-writer check. Textbook restatement drift, and the reason the repository's CONTRACT INTEGRITY rule says to sweep the class rather than the reported site. Each now says what its subject actually establishes: `keys` pins the writer's ORDER and membership, which is a property of the writer worth having; the contract is `MEASURED_ROW_KEYS` and its `_SHAPES` sibling, owned by the renderer that owes the fields and stated independently of it. Two stale facts in this crate's PLANS.md, both reported: it still described M1 as buffering each report into a `String` (M1 landed; probes stream through a `fmt::Write` adapter and `LineSink`), and still described the three diagnostic fields as arrays of condition codes (they are arrays of objects). The root PLANS.md carried the same second error and was corrected in d523f2b -- the same sentence in two files, which is the same drift again, and the sweep should have caught both. M4's preamble carried four bullets of re-scoping narrative for items listed thirty lines below it. CHECKLIST files are action-only, and a note a reader meets thirty lines before the item it governs is one they will not have in mind when they reach it -- the same defect already fixed for M2.4 in d523f2b. Each note now lives in its own item. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 47 ++++++++----------- crates/windows-platform-probes/PLANS.md | 2 +- crates/windows-platform-probes/src/row.rs | 24 +++++++--- .../windows-platform-probes/src/row/tests.rs | 11 ++++- crates/windows-platform-probes/src/tests.rs | 16 +++++-- 5 files changed, 60 insertions(+), 40 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index f79149251..79c59c474 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -8,32 +8,9 @@ for the crate belongs here instead. ## M4 -- Carried over from M2: the items M3 gated These were written under M2 and were blocked on M3, which is **now complete and archived** in -[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). They are unblocked. - -Each targeted the prose-against-row machinery M3 retired or relocated, so none was merely delayed -- -each was RE-SCOPED, and the re-scoping is what makes them safe to pick up. Those notes are below. -They were written in M3's preamble and moved here when M3 was archived, because they describe work -that is still open: an instruction for a pending item is not history, and leaving it in the archive -would have left this milestone pointing at a file it may not edit. - -- **M2.4** was re-scoped by M3.2. The exploration is still worth doing and its instrument is - unchanged, but what it hunts for changed: invariants over `Coherence`, `BracketOutcome` and - `Verdict` as VALUES, and facts the row fails to publish -- not correspondences between two - renderings. Its closing sentence, "promote only what proves meaningful into the oracle from M2.1", - now means the invariant set from M3.2. The open question attached to it -- whether this generalises - past this crate -- survives unchanged and is arguably sharpened, since a data-level invariant is - easier to share than a text reader. -- **M2.5** was gated by M3.1 and M3.3, both landed. Establishing that the middle of three discoveries - agreed produces a new FACT, which M3.1 says must reach the row rather than only the banner; and - M3.3 changed how the banner is built. Written before those, it would have been written into - machinery about to move. -- **M2.15** keeps its conclusion but loses its evidence. The five failures it cites were all - `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the correspondence M3.4 retired, - so they can no longer occur and a re-run now looks clean. The underlying point stands without - them: CI builds `aarch64` and never tests it, and architecture is the one shape dimension a corpus - cannot vary because it is fixed at compile time. Restate it on that basis when picked up. -- **M2.17** was re-scoped by M3.5: the dimensions worth crossing are the row's, and crossing prose - shapes that have since stopped being checked would have aimed at the retiring half. +[COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md). They are unblocked, and each was RE-SCOPED rather +than merely delayed -- so each item below carries its own re-scoping note, in the item, where +somebody executing the list will actually meet it. **The IDs keep their M2 numbers deliberately.** [COMPLETED-CHECKLIST.md](COMPLETED-CHECKLIST.md) is append-only and its entries are immutable, and two archived entries already cite M2.4 and M2.14 -- @@ -89,7 +66,14 @@ correctness in the archive. own arithmetic does NOT belong, and the honest outcome for such a one is a line in the module header saying so by name rather than a silent absence. -- [ ] **M2.5** -- Make the banner describe the read the body describes. A probe run performs +- [ ] **M2.5** -- Make the banner describe the read the body describes. + + Gated by M3.1 and M3.3, both landed: establishing that the middle of three discoveries agreed + produces a new FACT, which M3.1 says must reach the row rather than only the banner, and M3.3 + changed how the banner is built. Written before those, it would have been written into machinery + about to move. + + A probe run performs **three** independent `MachineMemoryTopology::discover()` calls: `Fingerprint::discover()` for the banner, `measure()`'s own discovery for the body, and `Fingerprint::discover()` again. `attribution` compares only the two endpoints, so equal endpoints print an unqualified banner without establishing @@ -122,6 +106,12 @@ correctness in the archive. - [ ] **M2.15** -- Run the probe suite on a second architecture in CI. + **Keeps its conclusion but loses its evidence.** The five failures cited below were all + `prose: "x86_64"` against `ndjson: "x86"` -- instances of exactly the correspondence M3.4 + retired, so they can no longer occur and a re-run now looks clean. The point stands without them: + CI builds `aarch64` and never tests it, and architecture is the one shape dimension a corpus + cannot vary because it is fixed at compile time. Restate it on that basis when picked up. + A reviewer asked whether the suite was portable and it was not: three renderer fixtures and the shape corpus' banner builder each hard-coded `x86_64` while the row they are compared against publishes `std::env::consts::ARCH`. Measured on `i686-pc-windows-msvc`: five failures, every one @@ -135,6 +125,9 @@ correctness in the archive. - [ ] **M2.17** -- Cross the corpus dimensions instead of varying one at a time. + Re-scoped by M3.5: the dimensions worth crossing are the ROW's. Crossing prose shapes that have + since stopped being checked would have aimed at the retiring half. + [tests/a_real_report_agrees_with_itself.rs](tests/a_real_report_agrees_with_itself.rs)'s `shapes()` builds each shape by taking `base()` and changing ONE thing. That makes every shape easy to read and is why the corpus found what it found -- but it means any renderer branch selected by TWO diff --git a/crates/windows-platform-probes/PLANS.md b/crates/windows-platform-probes/PLANS.md index af947af03..95765ff82 100644 --- a/crates/windows-platform-probes/PLANS.md +++ b/crates/windows-platform-probes/PLANS.md @@ -4,5 +4,5 @@ Design decisions are in [DESIGN-NOTES.md](DESIGN-NOTES.md). | Path to CHECKLIST.md | Status | Brief description | Design Notes | |---|---|---|---| -| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured. The report sink buffers each report into a `String`, so a termination that does not unwind -- Ctrl-C, or an abort during unwinding -- discards it, where the line-by-line printing it replaced kept it. Costs most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. **Complete and archived, ten items.** The three fields publish arrays of condition codes; the surviving correspondences became invariants over the observation rather than over two renderings; the row is emitted from a typed value through one writer with total escaping; the prose oracle and its parsers are gone, and no test extracts structured data from prose anywhere in the crate. The last four items came from reviews: three instruments asserted less than their names claimed, `BlockingState::ALL` was a census the compiler did not check while a doc comment said it did, and the hand-written JSON check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a false ACCEPT -- then replaced by `serde_json`, after which the remaining hand-written string scanners were deleted. M4 holds the four carried-over items M3 gated, now unblocked and re-scoped; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#d-correspondence-failures) | +| [CHECKLIST.md](CHECKLIST.md) | in progress | M1: stream a probe's report as it is measured; **done and archived**. The sink USED TO buffer each report into a `String`, so a termination that did not unwind -- Ctrl-C, or an abort during unwinding -- discarded it, where the line-by-line printing it replaced kept it; that cost most on `probe-cancel-io`, which runs about twenty seconds precisely when the wedge it hunts for occurs. Every probe now writes into the sink as it measures, through a `fmt::Write` adapter, and completed lines reach `Stdout` via `LineSink`. M2: check correspondence between a report's parts rather than each part alone, after a pull-request review found the renderer calling a state a bug while the verdict certified the same run as `agree`; complete -- the oracle, the renderer binding, the real-host test, the derived fact set, the partitioning discriminator and the shape corpus all landed, and its ten unrelated leftovers were re-sequenced into M4 and M5. M3: make the encoded row the contract and stop checking the prose against it. Re-reading M2's own evidence showed both defects that motivated the oracle were defects in the ROW -- and that the row published its three diagnostic lists as bare counts, so a survey could not tell a probe self-bug from host flakiness. The row gets the facts and the invariants; the prose gets review. **Complete and archived, ten items.** The three fields publish arrays of OBJECTS, each carrying a stable `code` plus the values its variant holds -- `{"code":"partitioning_summary_missing","level":9}` rather than the bare code of the superseded M3.1 form -- and each key's value SHAPE is now declared beside its name in `MEASURED_ROW_SHAPES`; the surviving correspondences became invariants over the observation rather than over two renderings; the row is emitted from a typed value through one writer with total escaping; the prose oracle and its parsers are gone, and no test extracts structured data from prose anywhere in the crate. The last four items came from reviews: three instruments asserted less than their names claimed, `BlockingState::ALL` was a census the compiler did not check while a doc comment said it did, and the hand-written JSON check was measured against a real parser over 1807 generated corruptions -- 159 disagreements, every one a false ACCEPT -- then replaced by `serde_json`, after which the remaining hand-written string scanners were deleted. M4 holds the four carried-over items M3 gated, now unblocked and re-scoped; M5 the six that nothing gates, which may be pulled forward at any time. | [DESIGN-NOTES.md](DESIGN-NOTES.md#d-buffered-report), [#d-encoded-row-is-the-contract](DESIGN-NOTES.md#d-encoded-row-is-the-contract), [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md#d-correspondence-failures) | | [../../CHECKLIST-thread-ambient.md](../../CHECKLIST-thread-ambient.md) | in progress | M27: create the crate, migrate this session's probes into it under the three-tier scheme, and queue migration of the nine earlier measurements that still live only in git-ignored scratch. | [DESIGN-NOTES.md](DESIGN-NOTES.md) | diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs index 777d634fd..90d68881a 100644 --- a/crates/windows-platform-probes/src/row.rs +++ b/crates/windows-platform-probes/src/row.rs @@ -21,13 +21,25 @@ //! yielded mislabelled data that still parses and that nothing downstream can //! detect. Here a name and its value are one pair, moved together or not at all. //! -//! # The key set becomes derivable +//! # What [`Row::keys`] derives, and what it cannot //! -//! [`Row::keys`] reads the value, so the well-formedness check no longer needs a -//! list of expected keys written beside it. That matters more than it sounds: -//! this component re-corrected the same hand-written census three times in one -//! day, and the last correction was falsified within the hour by a field being -//! added. A derived set cannot rot. +//! It reads back the names a caller actually supplied, which is what the +//! writer's own tests need: a row renders the members it was given, in order, +//! and that property is derivable rather than restated. +//! +//! **It is NOT the contract, and this section used to say it was.** The claim +//! here was that the well-formedness check "no longer needs a list of expected +//! keys written beside it". That is false, and falsifiably so: `keys` reports +//! what the builder happened to supply, so a row missing a required field is +//! still perfectly self-consistent. Measured -- with `.with("packages", ...)` +//! deleted from the renderer, the whole suite stayed green. +//! +//! The contract is `topology_report::MEASURED_ROW_KEYS` and its `_SHAPES` +//! sibling, owned by the renderer that owes those fields and stated +//! independently of it. A schema is not derivable from the thing it constrains; +//! the anti-census rule is about facts that CAN be derived, and this is not one. +//! Reported by a review, which found this passage still steering a reader back +//! toward the vacuous check. use std::fmt::Write as _; diff --git a/crates/windows-platform-probes/src/row/tests.rs b/crates/windows-platform-probes/src/row/tests.rs index 9b329e6f0..e71bfa943 100644 --- a/crates/windows-platform-probes/src/row/tests.rs +++ b/crates/windows-platform-probes/src/row/tests.rs @@ -31,7 +31,16 @@ fn a_row_renders_its_members_in_the_order_they_were_added() { #[test] fn the_key_set_is_derived_from_the_value() { - // The property that lets the well-formedness check stop carrying a census. + // **What `keys` is for, and what it is not.** It reports the names the + // builder was given, in order -- a property of the writer, worth pinning + // because the row's key ORDER is part of the contract. + // + // This said it was "the property that lets the well-formedness check stop + // carrying a census". It is not: a row missing a required field is still + // self-consistent, so a check reading `keys` back would only ever watch the + // writer agree with itself. The contract lives in + // `topology_report::MEASURED_ROW_KEYS` and its `_SHAPES` sibling, checked + // against the renderer that owes those fields. Found by a review. let row = Row::new("x-probe-topology") .with("arch", "x86_64") .with("cores", 8_usize); diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index a1f722574..0ef3eed3f 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4797,11 +4797,17 @@ fn a_discovery_error_reaches_the_row_as_a_field() { #[test] fn the_rendered_row_carries_exactly_the_keys_the_value_declares() { - // **What M3.4 deferred to here.** The well-formedness check could not assert - // a key set without a list written beside it, and a hand-written list is a - // census -- this component re-corrected the same one three times in a day, - // and the last correction was falsified within the hour by a field being - // added. `Row::keys` derives it, so the two cannot drift. + // **The READER against the WRITER, and nothing more than that.** What this + // pins is that `report_oracle::keys` reads back exactly the names the value + // declared, in order -- a round-trip through the renderer and the parser. + // + // It is deliberately NOT the contract check, and this comment used to claim + // it was: it said a hand-written key list was a census that `Row::keys` + // derives away. False, and the next test is the correction -- a row missing + // a required field is still self-consistent, so two sides agreeing says + // nothing about WHICH keys the survey is owed. That is + // `MEASURED_ROW_KEYS`, checked below. Found by a review, which read this + // paragraph as still steering future edits back to the vacuous check. let row = crate::row::Row::new("x-probe-topology") .with("arch", "x86_64") .with("cross_check", "agree"); From b68a12412ece962fe6b04af4c4e625c183edf62b Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:17:02 -0700 Subject: [PATCH 40/46] fix(platform-probes): gate BOTH schema assertions, not just the first 40ea4f3 broke `cargo run --bin`, and the mechanism is one this crate has already paid for twice. A `#[cfg]` attribute governs the single statement that follows it. Adding the shape assertion beneath the gated well-formedness call left the new call UNGATED, and `report_oracle` is compiled only under `cfg(any(test, feature = "oracle-in-renderer"))` -- so a default build has no such module and every probe binary failed with E0433. **Nothing local could have caught it, which is the part worth recording.** This crate carries a dev-dependency on itself with `oracle-in-renderer` enabled, so `cargo test` and `cargo check --all-targets` both compile with the feature ON. The feature-off arm is never exercised here; CI's `cargo run -p ... --bin probe-error-mode --locked` is the only build that reaches it. Verified the fix the same way -- `cargo build -p windows-platform-probes --bins --locked`, which is the default-feature shape, now clean. Both calls now sit in one `cfg`-gated block, so a third assertion cannot repeat this by being appended. The same edit also mangled two comments: a PowerShell double-quoted string turned the backtick in `` `report` `` into a carriage return and `` `n `` into a literal escape, splitting one comment across two lines and corrupting another. Repaired, and swept -- zero CR bytes and zero literal backtick escapes across all 25 files this branch changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/topology_report.rs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index de9372790..ffa1f9490 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -371,11 +371,14 @@ pub fn report_unmeasured(banner: &str, error: &io::Error) -> String { // correspondences apply". There are no correspondences left to apply -- M3 // retired the prose/row relation, and what is checked is that the row is // well formed, which is not a thing a renderer can make less of. + // + // The SHAPES are bound here too, for the reason given on `report` below: + // names alone leave a consumer's field types unconstrained. #[cfg(any(test, feature = "oracle-in-renderer"))] - crate::report_oracle::assert_row_is_well_formed(&out); - // The SHAPES too, for the reason on eport below: names alone leave a - // consumer's field types unconstrained. - crate::report_oracle::assert_row_has_the_schemas_shapes(&out, UNMEASURED_ROW_SHAPES); + { + crate::report_oracle::assert_row_is_well_formed(&out); + crate::report_oracle::assert_row_has_the_schemas_shapes(&out, UNMEASURED_ROW_SHAPES); + } out } @@ -976,12 +979,25 @@ pub fn report(banner: &str, observation: &Observation) -> String { // case, because the assertion's message carries the whole report -- what is // lost is the NDJSON row a survey would have mined, which is why the default // build is the one that matters and is the one pinned above. + // + // **The schema's SHAPES are bound here for the same reason.** Well-formed + // says the row parses; the schema says `processors` is a number and each + // diagnostic entry carries a code. Measured: publishing `processors` through + // `.to_string()` left all 230 library tests and all 10 real-host integration + // tests green before this line existed. Found by a review. + // + // **Both inside ONE `cfg` block, which the first attempt got wrong.** A + // `#[cfg]` attribute governs the single statement that follows it, so + // adding a second call beneath the gated one left that call ungated -- and + // `report_oracle` does not exist in a default build. Nothing local caught + // it: this crate's dev-dependency on itself turns `oracle-in-renderer` on + // for every `cargo test` and `cargo check --all-targets`, so the + // feature-off arm is never compiled here. CI's `cargo run --bin` is, and + // that is where it broke. #[cfg(any(test, feature = "oracle-in-renderer"))] - crate::report_oracle::assert_row_is_well_formed(&out); - // **And the schema's SHAPES, bound here for the same reason.** Well-formed - // says the row parses; the schema says processors is a number and each - // diagnostic entry carries a code. Measured: publishing processors`n // through .to_string() left all 230 library tests and all 10 real-host - // integration tests green before this line existed. Found by a review. - crate::report_oracle::assert_row_has_the_schemas_shapes(&out, MEASURED_ROW_SHAPES); + { + crate::report_oracle::assert_row_is_well_formed(&out); + crate::report_oracle::assert_row_has_the_schemas_shapes(&out, MEASURED_ROW_SHAPES); + } out } From 399cc3963dede667064b2bc72ea51f3eb6a10360 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:32:57 -0700 Subject: [PATCH 41/46] fix(platform-probes): check the whole diagnostic entry through the renderer, and unbreak the default doc build Three findings from the review of 40ea4f3, two of them mine from that commit. **`published_codes` discarded every payload field**, so the only end-to-end renderer test would have stayed green if `topology_report` dropped or reshaped `parsed`, `counter`, `level` or an anomaly's metadata. The per-variant tests cover `published()` in isolation; nothing covered it THROUGH the renderer. `the_row_carries_each_diagnostic_entry_whole_and_not_only_its_code` now compares each rendered entry, for all four diagnostic lists, across every corpus shape. The expectation is derived from `published()` rather than written out, so it cannot drift from the publisher; what it pins is that the renderer carries that value through unchanged. **The first sabotage of it was wrong, and the correction is the useful part.** Deleting `offset` from `published_anomaly` left the new test green -- correctly, because both sides derive from that function, so sabotaging the PUBLISHER moves them together. The defect the reviewer described is in the RENDERER, and sabotaging there -- mapping the anomalies back to bare codes, the superseded M3.1 form -- reddens it along with four neighbours. A test that derives its expectation can only be sabotage-verified at the layer it actually constrains. **The `RowDefect::RepeatedKey` intra-doc link broke `cargo doc`.** `row` is always built, `report_oracle` is compiled only under `cfg(any(test, feature = "oracle-in-renderer"))`, so the link cannot resolve in a default documentation build and emitted a broken-intra-doc-link warning. Demoted to plain code text, with the reason recorded beside it. Confirmed by running `cargo doc -p windows-platform-probes --lib`, which is now clean. That is the same feature-gating trap as b68a124 in a third guise -- module, statement, and now doc link. Everything local compiles with the feature ON because of this crate's dev-dependency on itself, so each variant has to be caught by a build that does not. **The design-session key list** read as a current-schema list missing `disagreements`, which was a fair reading of how d523f2b introduced it: a sentence pointing at `MEASURED_ROW_KEYS` as the authority, followed by a colon and the historical eighteen. The framing now says plainly that the list is the eighteen as they stood at that session and is not reproduced as current. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...-2026-09-12-what-the-oracle-should-read.md | 8 +- crates/windows-platform-probes/src/row.rs | 10 ++- .../tests/a_real_report_agrees_with_itself.rs | 88 +++++++++++++++++++ 3 files changed, 102 insertions(+), 4 deletions(-) diff --git a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md index a7cea84f1..56676e502 100644 --- a/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md +++ b/crates/windows-platform-probes/design-sessions/DESIGN-SESSION-2026-09-12-what-the-oracle-should-read.md @@ -47,8 +47,12 @@ Checked during the session, in the code as it stands on `main`: review reported it as a stale census, and it is not one. The diagnosis below turns on what the row held AT THE TIME, so correcting the number to nineteen would make the next bullet -- "there is no key for the alarm" -- read as an error rather than as the finding. - For the current schema read `MEASURED_ROW_KEYS` in - [src/topology_report.rs](../src/topology_report.rs), which is the one authority: + For the current schema read `MEASURED_ROW_KEYS` and `MEASURED_ROW_SHAPES` in + [src/topology_report.rs](../src/topology_report.rs), which are the one authority; the list + that follows is **the eighteen as they stood at this session** and is not reproduced here as + current. (A review read it as a current-schema list missing `disagreements`, which is a fair + reading of how it was introduced -- hence this sentence.) + `reason`, `arch`, `processors`, `groups`, `packages`, `numa_domains`, `numa_domains_without_processors`, `cores`, `efficiency_classes`, `caches`, `outermost_partitioning_cache_level`, diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs index 90d68881a..8b3b902e9 100644 --- a/crates/windows-platform-probes/src/row.rs +++ b/crates/windows-platform-probes/src/row.rs @@ -237,8 +237,14 @@ impl Row { /// `JSON.parse` both accept it and silently keep the last value -- so a row /// carrying one is not a broken artifact a survey discards but an ambiguous /// one it mines, which is worse. The crate reports it as - /// [`RowDefect::RepeatedKey`](crate::report_oracle::RowDefect::RepeatedKey); - /// this is the writer being unable to produce it in the first place. + /// `report_oracle::RowDefect::RepeatedKey`; this is the writer being unable + /// to produce it in the first place. + /// + /// (Deliberately not an intra-doc link. `report_oracle` is compiled only + /// under `cfg(any(test, feature = "oracle-in-renderer"))` while this module + /// is always built, so a link here cannot resolve in a default `cargo doc` + /// and emits a broken-intra-doc-link warning. Found by a review, and + /// confirmed by running `cargo doc -p windows-platform-probes --lib`.) /// /// **Why a panic and not a `Result`.** Every caller is a renderer in this /// crate composing a fixed schema, so a repeat is a programming error at the diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index c4dbd0600..c477ab953 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -463,6 +463,94 @@ fn published_codes(text: &str, keys: &[&str]) -> Vec { .flat_map(|key| report_oracle::list_codes(row, key)) .collect() } +/// Every diagnostic entry in `text`'s row, as rendered JSON, for one key. +/// +/// **The whole object, not just its `code`.** `published_codes` above discards +/// every payload field, so the renderer-to-row path stayed green if +/// `topology_report` dropped or reshaped `parsed`, `counter`, `level` or an +/// anomaly's metadata. The per-variant tests cover `published()` in isolation; +/// nothing covered it through the renderer until this. Found by a review. +fn published_entries(text: &str, key: &str) -> Vec { + let Some(row) = report_oracle::row(text) else { + panic!("no single well-formed row in:\n{text}"); + }; + + let parsed: serde_json::Value = serde_json::from_str(row).expect("the row parses"); + parsed[key] + .as_array() + .unwrap_or_else(|| panic!("`{key}` should be a list in:\n{row}")) + .iter() + .map(ToString::to_string) + .collect() +} + +/// One diagnostic value as the row writer renders it. +/// +/// Derived from `published()` rather than written out, so the expectation +/// cannot drift from the publisher it is checking; what this pins is that the +/// RENDERER carries that value through unchanged. +fn as_rendered(value: windows_platform_probes::row::Value) -> String { + let row = windows_platform_probes::row::Row::new("x").with("entry", value); + let text = row.render(); + let parsed: serde_json::Value = serde_json::from_str(&text).expect("the row parses"); + parsed["entry"].to_string() +} + +#[test] +fn the_row_carries_each_diagnostic_entry_whole_and_not_only_its_code() { + use windows_platform_probes::topology::diagnostic::{ + Disagreement, NotCompared, ParseIncomplete, published_anomaly, + }; + + for shape in shapes() { + let text = report(&banner_for(&shape.observation), &shape.observation); + let check = shape.observation.cross_check(); + + for (key, expected) in [ + ( + "disagreements", + check + .disagreements + .iter() + .map(Disagreement::published) + .collect::>(), + ), + ( + "not_compared", + check + .not_compared + .iter() + .map(NotCompared::published) + .collect::>(), + ), + ( + "parse_incomplete", + check + .parse_incomplete + .iter() + .map(ParseIncomplete::published) + .collect::>(), + ), + ( + "enumeration_anomalies", + shape + .observation + .enumeration_anomalies + .iter() + .map(published_anomaly) + .collect::>(), + ), + ] { + assert_eq!( + published_entries(&text, key), + expected.into_iter().map(as_rendered).collect::>(), + "{}: the row's `{key}` must carry each entry whole", + shape.what + ); + } + } +} + /// Whether `text`'s row publishes a condition for an observation in a blocking /// state. /// From af82a22b66b355bd0e7b96eeffc94715b908dafe Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:35:00 -0700 Subject: [PATCH 42/46] docs(platform-probes): note that `enumeration_anomalies` names a code and a row key A review read `codes_for(State::EnumerationAnomalies)` as naming the row KEY `enumeration_anomalies`, concluded that `publication_holds` could not find it because `DIAGNOSTIC_LISTS` deliberately excludes that list, and reported the assertion as failing for the `anomalies` corpus shape. It does not fail. The string is a condition CODE carried inside `parse_incomplete`: `cross_check` pushes `ParseIncomplete::EnumerationAnomalies` when the observation records any, and that variant's code is this same word. So `publication_holds` finds it while scanning the three diagnostic lists, and `every_state_that_blocks_agreement_reaches_the_row` exercises exactly that shape -- all 11 tests in the target are green, before and after this commit. The collision is real even though the conclusion was not: one word names both a row key and a condition code, and a careful reader took it for the wrong one. Noted at the arm, since the next reader has the same chance of being misled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/a_real_report_agrees_with_itself.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs index c477ab953..9592ddf7d 100644 --- a/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs +++ b/crates/windows-platform-probes/tests/a_real_report_agrees_with_itself.rs @@ -598,6 +598,16 @@ fn codes_for(state: invariant::BlockingState) -> &'static [&'static str] { use invariant::BlockingState as State; match state { State::PartitioningSummaryMissing => &["partitioning_summary_missing"], + // **A CODE inside `parse_incomplete`, not the row key of the same + // name.** `cross_check` pushes `ParseIncomplete::EnumerationAnomalies` + // when the observation records any, and that variant's code is this + // string -- so `publication_holds` finds it while scanning + // `DIAGNOSTIC_LISTS`, which deliberately excludes the + // `enumeration_anomalies` LIST. A review read this as the key and + // concluded the assertion must fail for the `anomalies` shape; it does + // not, and `every_state_that_blocks_agreement_reaches_the_row` covers + // exactly that shape. Noted here because the collision is real even + // though the conclusion was not. State::EnumerationAnomalies => &["enumeration_anomalies"], State::NotMeasured => &["not_measured"], State::NoCacheLevels => &["no_cache_levels"], From 0c4aced1283d5e2fa5c4fdfe29bfa707fd3c1612 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 13 Sep 2026 23:51:04 -0700 Subject: [PATCH 43/46] fix(platform-probes): make the row's guarantees hold at every level, not just the first Two gaps a review found in last round's work, both measured before being fixed and both the same shape of defect: a guarantee stated for the ARTIFACT and enforced only for its outermost level. **Duplicate keys were rejected at the top level only.** `Row::with` refused a repeat among the row's own members, so a nested entry could still render `{"code":"a","code":"b"}` -- measured, exactly that string -- which `serde_json` and `JSON.parse` both accept while silently keeping one value. The oracle could not report it either, because `keys` reads top-level names by design. `Row::with` now walks the whole value tree. Uniqueness is per OBJECT, not across the row: every diagnostic entry carries its own `code`, and a rule forbidding that would reject every real report. A sibling-objects control pins that, so the check cannot be tightened into nonsense later. **`caches` promised only "a list of objects", so `[{}]` satisfied it.** The renderer publishes numeric `level` and `domains`; nothing checked either. Measured: an empty cache object produced zero violations. (`[{}]` is also, exactly, the bogus shape a design session claimed the row emitted and which was corrected earlier on this branch. It was never a real rendering -- but it was reachable through the checker.) `Shape::ListOfObjectsWith(&[(name, shape)])` replaces both `ListOfObjects` and `ListOfCoded`, which generalises rather than adding a special case: the coded lists become `ListOfObjectsWith(CODED)` and `caches` names its two numeric members. Extra members stay legal -- what a schema owes a consumer is that the fields it PROMISES are present and typed, and forbidding additions would make every new field a breaking change to the checker rather than to the contract. The checker is now recursive and reports WHERE rather than only THAT: `caches[0].level should be Number but is "2"` instead of one sentence about the whole list. Sabotage-verified, each against its own claim: - A nested duplicate now panics in the writer, naming the member; the sibling control still renders both `code`s. - Publishing `cache.level` through `.to_string()` fails 19 library tests naming `caches[0].level`, against 0 before. The `caches` test takes its schema FROM `MEASURED_ROW_SHAPES` by filtering for the key, rather than restating the shape beside it. Written out, it would have pinned a shape of its own and passed while the declared schema said something else -- the copy checking the copy, which is the defect this branch keeps finding. The first draft did exactly that, with a doc comment claiming it was derived. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/report_oracle.rs | 81 ++++++++++++----- crates/windows-platform-probes/src/row.rs | 59 +++++++++++-- .../windows-platform-probes/src/row/tests.rs | 40 +++++++++ crates/windows-platform-probes/src/tests.rs | 88 +++++++++++++++++-- .../src/topology_report.rs | 19 ++-- 5 files changed, 246 insertions(+), 41 deletions(-) diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 7697f7595..570976250 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -316,40 +316,73 @@ pub fn shape_violations(row: &str, schema: &[(&str, Shape)]) -> Vec { return vec![format!("the row is not a JSON object: {row}")]; }; - let is_number = |value: &Json| value.is_u64() || value.is_i64(); - let coded = |value: &Json| { - value - .as_object() - .is_some_and(|entry| entry.get("code").is_some_and(Json::is_string)) - }; - let mut found = Vec::new(); for (name, shape) in schema { let Some(value) = parsed.get(*name) else { found.push(format!("`{name}` is missing, so its shape cannot hold")); continue; }; + check_shape(name, *shape, value, &mut found); + } - let holds = match shape { - Shape::Text => value.is_string(), - Shape::Number => is_number(value), - Shape::NumberOrNull => is_number(value) || value.is_null(), - Shape::ListOfNumbers => value.as_array().is_some_and(|l| l.iter().all(is_number)), - Shape::ListOfObjects => value - .as_array() - .is_some_and(|l| l.iter().all(Json::is_object)), - Shape::ListOfCoded => value.as_array().is_some_and(|l| l.iter().all(coded)), - Shape::ObjectOfNumbers => value - .as_object() - .is_some_and(|o| o.values().all(&is_number)), - }; + found +} - if !holds { - found.push(format!("`{name}` should be {shape:?} but is `{value}`")); - } +/// Whether `value` has `shape`, appending one sentence per departure. +/// +/// **Recursive, and reporting WHERE rather than only THAT.** A lone +/// "`caches` should be a list of objects" cannot tell a reader which element +/// lost which member -- and the nested contract is the half a review found +/// unchecked, because `[{}]` satisfied "a list of objects" while publishing +/// none of the cache fields a survey mines. +fn check_shape(at: &str, shape: Shape, value: &serde_json::Value, found: &mut Vec) { + use serde_json::Value as Json; + + fn is_number(value: &Json) -> bool { + value.is_u64() || value.is_i64() } - found + let holds = match shape { + Shape::Text => value.is_string(), + Shape::Number => is_number(value), + Shape::NumberOrNull => is_number(value) || value.is_null(), + Shape::ListOfNumbers => value.as_array().is_some_and(|l| l.iter().all(is_number)), + Shape::ObjectOfNumbers => value.as_object().is_some_and(|o| o.values().all(is_number)), + Shape::ListOfObjectsWith(required) => { + let Some(entries) = value.as_array() else { + found.push(format!("`{at}` should be a list but is `{value}`")); + return; + }; + + for (index, entry) in entries.iter().enumerate() { + let Some(members) = entry.as_object() else { + found.push(format!( + "`{at}`[{index}] should be an object but is `{entry}`" + )); + continue; + }; + + for (member, member_shape) in required { + let Some(held) = members.get(*member) else { + found.push(format!("`{at}`[{index}] is missing `{member}`")); + continue; + }; + check_shape( + &format!("{at}[{index}].{member}"), + *member_shape, + held, + found, + ); + } + } + + return; + } + }; + + if !holds { + found.push(format!("`{at}` should be {shape:?} but is `{value}`")); + } } /// [`shape_violations`], as an assertion. diff --git a/crates/windows-platform-probes/src/row.rs b/crates/windows-platform-probes/src/row.rs index 8b3b902e9..ea6018d82 100644 --- a/crates/windows-platform-probes/src/row.rs +++ b/crates/windows-platform-probes/src/row.rs @@ -72,13 +72,20 @@ pub enum Shape { NumberOrNull, /// A list whose every element is a number. ListOfNumbers, - /// A list whose every element is an object. - ListOfObjects, - /// A list whose every element is an object carrying a string `code`. + /// A list whose every element is an object carrying at least these members, + /// each with the shape named beside it. /// - /// The diagnostic lists. `code` is the stable discriminant a survey groups - /// by, so an entry without one is unmineable even though it is valid JSON. - ListOfCoded, + /// **Recursive, because "a list of objects" was not a contract.** That is + /// what this replaced, and `[{}]` satisfied it -- so `caches` could stop + /// publishing `level` and `domains` with the shape oracle green, and a + /// diagnostic entry could lose the `code` a survey groups by. Reported by a + /// review, and measured: an empty cache object produced zero violations. + /// + /// Extra members are allowed. What a schema owes a consumer is that the + /// fields it promises are present and typed; forbidding additions would + /// make every new field a breaking change to the checker rather than to the + /// contract. + ListOfObjectsWith(&'static [(&'static str, Shape)]), /// An object whose every member is a number. ObjectOfNumbers, } @@ -107,6 +114,42 @@ pub enum Value { Object(Vec<(&'static str, Value)>), } +/// Panics if any object anywhere inside `value` repeats a member name. +/// +/// **Every level, because the row's guarantee is about the artifact, not about +/// its first level.** `Row::with` rejects a repeat among the row's own members; +/// this is the same rule applied to what those members contain. Without it a +/// diagnostic entry could render `{"code":"a","code":"b"}` -- measured, exactly +/// that -- which `serde_json` and `JSON.parse` both accept while keeping one +/// value, and which the oracle cannot report either, because `keys` reads +/// top-level names by design. Reported by a review. +/// +/// Uniqueness is PER OBJECT, not across the row: every diagnostic entry carries +/// its own `code`, and a rule that forbade that would reject every real report. +fn assert_unique_names(at: &str, value: &Value) { + match value { + Value::Object(members) => { + let mut seen: Vec<&str> = Vec::new(); + for (name, held) in members { + assert!( + !seen.contains(name), + "the object at `{at}` already carries `{name}`, and a repeated \ + key survives a consumer's parse as whichever value happened \ + to come last" + ); + seen.push(name); + assert_unique_names(name, held); + } + } + Value::List(entries) => { + for entry in entries { + assert_unique_names(at, entry); + } + } + Value::Text(_) | Value::Number(_) | Value::Null => {} + } +} + impl From<&str> for Value { fn from(text: &str) -> Self { Self::Text(text.to_owned()) @@ -259,7 +302,9 @@ impl Row { "the row already carries `{name}`, and a repeated key survives a \ consumer's parse as whichever value happened to come last" ); - self.members.push((name, value.into())); + let value = value.into(); + assert_unique_names(name, &value); + self.members.push((name, value)); self } diff --git a/crates/windows-platform-probes/src/row/tests.rs b/crates/windows-platform-probes/src/row/tests.rs index e71bfa943..a11624850 100644 --- a/crates/windows-platform-probes/src/row/tests.rs +++ b/crates/windows-platform-probes/src/row/tests.rs @@ -17,6 +17,46 @@ fn the_writer_refuses_to_render_a_key_twice() { .with("processors", 32_usize); } +#[test] +#[should_panic(expected = "already carries `code`")] +fn the_writer_refuses_a_repeated_name_inside_a_nested_object_too() { + // **The other half of the duplicate-key guarantee.** `with` rejected a + // repeat at the TOP level only, so a nested entry could render + // `{"code":"a","code":"b"}` -- measured, exactly that string -- and the + // oracle could not see it either, because `keys` reads top-level names by + // design. A consumer's parser keeps whichever came last. + // + // Reported by a review as the gap left by the top-level check, which is + // what it was: the guarantee was stated for the row and enforced for one + // level of it. + let _ = Row::new("x").with( + "parse_incomplete", + Value::List(vec![Value::Object(vec![ + ("code", Value::Text("a".to_owned())), + ("code", Value::Text("b".to_owned())), + ])]), + ); +} + +#[test] +fn a_repeated_name_in_two_sibling_objects_is_fine() { + // The control. Uniqueness is per object, not across the row -- every + // diagnostic entry carries its own `code`, and a check that forbade that + // would reject every real report. + let row = Row::new("x").with( + "parse_incomplete", + Value::List(vec![ + Value::Object(vec![("code", Value::Text("a".to_owned()))]), + Value::Object(vec![("code", Value::Text("b".to_owned()))]), + ]), + ); + + assert_eq!( + row.render(), + r#"{"reason":"x","parse_incomplete":[{"code":"a"},{"code":"b"}]}"# + ); +} + #[test] fn a_row_renders_its_members_in_the_order_they_were_added() { let row = Row::new("x-probe-topology") diff --git a/crates/windows-platform-probes/src/tests.rs b/crates/windows-platform-probes/src/tests.rs index 0ef3eed3f..2ad53f00c 100644 --- a/crates/windows-platform-probes/src/tests.rs +++ b/crates/windows-platform-probes/src/tests.rs @@ -4890,9 +4890,9 @@ fn every_row_value_has_the_shape_its_schema_declares() { #[test] fn a_diagnostic_entry_without_a_code_is_a_shape_violation() { - // The control for `ListOfCoded`, and the reason it is not merely - // `ListOfObjects`: `code` is the stable discriminant a survey groups by, so - // an entry lacking one is unmineable while still being valid JSON. + // The control for the coded schema, and the reason a list of objects was + // not enough on its own: `code` is the stable discriminant a survey groups + // by, so an entry lacking one is unmineable while still being valid JSON. let row = crate::row::Row::new("x-probe-topology") .with( "parse_incomplete", @@ -4905,7 +4905,10 @@ fn a_diagnostic_entry_without_a_code_is_a_shape_violation() { let violations = crate::report_oracle::shape_violations( &row, - &[("parse_incomplete", crate::row::Shape::ListOfCoded)], + &[( + "parse_incomplete", + crate::row::Shape::ListOfObjectsWith(&[("code", crate::row::Shape::Text)]), + )], ); assert_eq!(violations.len(), 1, "{violations:?}"); assert!(violations[0].contains("parse_incomplete"), "{violations:?}"); @@ -4924,12 +4927,87 @@ fn a_diagnostic_entry_without_a_code_is_a_shape_violation() { assert_eq!( crate::report_oracle::shape_violations( &coded, - &[("parse_incomplete", crate::row::Shape::ListOfCoded)] + &[( + "parse_incomplete", + crate::row::Shape::ListOfObjectsWith(&[("code", crate::row::Shape::Text)]) + )] ), Vec::::new() ); } +#[test] +fn a_cache_entry_missing_its_numeric_members_is_a_shape_violation() { + // **`[{}]` used to satisfy `caches`.** The shape said "a list of objects" + // and stopped there, so the renderer could drop `level` and `domains`, or + // publish them as strings, with the schema oracle green -- measured, zero + // violations for an empty cache object. Reported by a review. + // + // `[{}]` is also, exactly, the bogus shape a design session claimed the row + // emitted and which was corrected earlier on this branch. It was never a + // real rendering; it was reachable through the checker. + let empty = crate::row::Row::new("x-probe-topology") + .with( + "caches", + crate::row::Value::List(vec![crate::row::Value::Object(Vec::new())]), + ) + .render(); + let violations = crate::report_oracle::shape_violations(&empty, &measured_caches()); + assert_eq!(violations.len(), 2, "{violations:?}"); + assert!( + violations + .iter() + .any(|what| what.contains("is missing `level`")), + "{violations:?}" + ); + + // A wrongly TYPED member, not merely an absent one. + let stringly = crate::row::Row::new("x-probe-topology") + .with( + "caches", + crate::row::Value::List(vec![crate::row::Value::Object(vec![ + ("level", crate::row::Value::Text("L1".to_owned())), + ("domains", crate::row::Value::Number(8)), + ])]), + ) + .render(); + let violations = crate::report_oracle::shape_violations(&stringly, &measured_caches()); + assert_eq!(violations.len(), 1, "{violations:?}"); + assert!(violations[0].contains("caches[0].level"), "{violations:?}"); + + // The control: a real cache entry passes, so the rule is not rejecting + // everything. + let good = crate::row::Row::new("x-probe-topology") + .with( + "caches", + crate::row::Value::List(vec![crate::row::Value::Object(vec![ + ("level", crate::row::Value::Number(1)), + ("domains", crate::row::Value::Number(8)), + ])]), + ) + .render(); + assert_eq!( + crate::report_oracle::shape_violations(&good, &measured_caches()), + Vec::::new() + ); +} + +/// The `caches` entry OF the measured schema, taken from the schema rather than +/// restated beside it. +/// +/// Written out, this test would have pinned a shape of its own and passed while +/// `MEASURED_ROW_SHAPES` declared something else -- the copy checking the copy. +fn measured_caches() -> Vec<(&'static str, crate::row::Shape)> { + let entry: Vec<_> = crate::topology_report::MEASURED_ROW_SHAPES + .iter() + .copied() + .filter(|(key, _)| *key == "caches") + .collect(); + + assert_eq!(entry.len(), 1, "the schema declares `caches` exactly once"); + entry +} + #[test] fn the_two_row_shapes_are_distinguishable_by_their_keys() { // The guard that keeps the two schemas from drifting into each other. If diff --git a/crates/windows-platform-probes/src/topology_report.rs b/crates/windows-platform-probes/src/topology_report.rs index ffa1f9490..cb62f5850 100644 --- a/crates/windows-platform-probes/src/topology_report.rs +++ b/crates/windows-platform-probes/src/topology_report.rs @@ -21,6 +21,15 @@ use windows_placement_probe::fingerprint::{Fingerprint, banner_line_for}; use crate::row::{Row, Shape, Value}; +/// What every entry in one of the four diagnostic lists must carry. +/// +/// `code` is the stable discriminant a survey groups by, so an entry without one +/// is unmineable even though it is valid JSON. The payload beside it is +/// per-variant and so is not a member of this schema -- it is checked against +/// the typed publisher by +/// `the_row_carries_each_diagnostic_entry_whole_and_not_only_its_code`. +const CODED: &[(&str, Shape)] = &[("code", Shape::Text)]; + /// Declares a row schema once, as names WITH shapes, and derives the key list. /// /// One list, so a key cannot gain a shape without gaining a name or the reverse. @@ -81,15 +90,15 @@ row_schema!( "numa_domains_without_processors" => Shape::Number, "cores" => Shape::Number, "efficiency_classes" => Shape::ListOfNumbers, - "caches" => Shape::ListOfObjects, + "caches" => Shape::ListOfObjectsWith(&[("level", Shape::Number), ("domains", Shape::Number)]), "outermost_partitioning_cache_level" => Shape::NumberOrNull, "outermost_partitioning_cache" => Shape::Text, "policies" => Shape::ObjectOfNumbers, "cross_check" => Shape::Text, - "disagreements" => Shape::ListOfCoded, - "not_compared" => Shape::ListOfCoded, - "parse_incomplete" => Shape::ListOfCoded, - "enumeration_anomalies" => Shape::ListOfCoded, + "disagreements" => Shape::ListOfObjectsWith(CODED), + "not_compared" => Shape::ListOfObjectsWith(CODED), + "parse_incomplete" => Shape::ListOfObjectsWith(CODED), + "enumeration_anomalies" => Shape::ListOfObjectsWith(CODED), "numa_domains_only_in_cpu_sets" => Shape::Number, }); From c1927a2817f601658db6f6ddc3924d239cf62e25 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 00:55:54 -0700 Subject: [PATCH 44/46] docs(platform-probes): put the supersedence marker where a reader meets it Two documentation findings, both correct against the repository's own rules, and one rejection. **The retired correspondence-oracle decision stated its supersedence twelve lines into the body.** The rule is explicit that the marker goes adjacent to the title "even when the fuller explanation already appears later in the decision's body", precisely so a reader cannot take a superseded section for a current one by reading the top of it. Added beneath the anchors, matching the shape the buffered-report decision already uses, and the fuller explanation lower down is left as it was. Swept rather than fixing the reported site alone: this file has exactly two superseded decisions and both now carry an adjacent marker. **An inline-code file reference was not clickable**, three lines above a linked reference to the same file. Linked. Swept: no unlinked `src/...` references remain in the file. **Rejected: `match observation.coherence` "fails to compile".** It does not, and this is the third round reporting this shape -- twice against `diagnostic.rs`, now against `invariant.rs`. Matching a place expression behind a shared reference is a move only when a PATTERN BINDING moves a non-`Copy` value; all three arms here are unit-like or `{ .. }` and bind nothing. `cargo check --all-targets` is clean across the workspace and has been on every commit these lines have existed. Left as it is rather than adding a no-op `&` to satisfy the claim -- but a note now sits at the match, because three rounds spent on one false positive is itself evidence the code should say why it is correct. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/DESIGN-NOTES.md | 4 +++- crates/windows-platform-probes/src/topology/invariant.rs | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/windows-platform-probes/DESIGN-NOTES.md b/crates/windows-platform-probes/DESIGN-NOTES.md index c5f275816..5417ffd1f 100644 --- a/crates/windows-platform-probes/DESIGN-NOTES.md +++ b/crates/windows-platform-probes/DESIGN-NOTES.md @@ -1030,6 +1030,8 @@ scheduled by this decision. +**Superseded by [The encoded row is the contract; the prose is not](#d-encoded-row-is-the-contract).** + **Moved to Tier 2: [DESIGN-RATIONALE.md](DESIGN-RATIONALE.md).** The anchors above are kept here so every existing link still lands somewhere that says where the content went. @@ -1174,7 +1176,7 @@ repository instructions describe.) ### What the text-reading design cost -Counted in `src/report_oracle.rs` **as it stood before this decision**: of 38 +Counted in [src/report_oracle.rs](src/report_oracle.rs) **as it stood before this decision**: of 38 top-level functions, ten were correspondence rules and four were comparison helpers. **Twenty-three existed only to extract values back out of rendered text.** None of them survives: [src/report_oracle.rs](src/report_oracle.rs) reads no diff --git a/crates/windows-platform-probes/src/topology/invariant.rs b/crates/windows-platform-probes/src/topology/invariant.rs index 426c800d7..8eac6d82b 100644 --- a/crates/windows-platform-probes/src/topology/invariant.rs +++ b/crates/windows-platform-probes/src/topology/invariant.rs @@ -307,6 +307,14 @@ pub fn blocking_states(observation: &Observation) -> Vec { // compiler-exhaustive and a fourth variant cannot be silently folded into // whichever arm happens to be nearest -- which is what the `!= Agreed` form // did to `NotCollected`. + // + // **No `&` on the scrutinee, and none is needed.** Three review rounds have + // reported this and the two like it in `diagnostic.rs` as moving a + // non-`Copy` field out of a shared reference. Matching a place expression + // behind a `&` is a move only when a PATTERN BINDING moves a non-`Copy` + // value; every arm below is unit-like or `{ .. }`, so nothing is bound at + // all. `cargo check --all-targets` is clean across the workspace, and has + // been on every commit these lines have existed. match observation.coherence { Coherence::Agreed => {} Coherence::Disagreed { .. } => states.push(BlockingState::EnumerationsDisagreed), From c8f7e6317bab4888dce084fb5387f2746c023ec7 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 01:21:50 -0700 Subject: [PATCH 45/46] chore(ioring): record why the in-flight precondition holds, since a review read it backwards `already_queued < width` sets `caught_in_flight`, and a review argued it does not prove a read was outstanding at the attach: a read finishing between the attach and the poll would, on that reading, let the test pass while exercising only the already-completed path. The direction is what settles it, and it runs the other way. `drain_to_empty` loops until `try_pop` reports the queue EMPTY -- no cap, no early exit -- so `already_queued` is the total observed at a moment strictly after the attach. A read finishing in that window is therefore COUNTED, which pushes `already_queued` toward `width` and makes the branch LESS likely to be taken. The already-completed case the finding warns about is exactly the case where all `width` are drained and the flag is never set. So the error this can make is a false NEGATIVE, never a false positive. And completion is monotonic: a read still outstanding at the later poll was outstanding at the earlier attach. The escalation over widths and attempts, and the final assertion, exist for the false negatives. The one real assumption is that `try_pop` reports emptiness truthfully -- a ring claiming empty while holding completions would forge this precondition. That is a defect in the crate under test rather than in the test, and `contract.assert_quiescent()` is what would catch it. Written down because it is the only way this argument fails. Comment only; no behaviour change. Typed `chore` because `tools/check-commit-scope.ps1` flagged it riding along with unrelated `windows-platform-probes` work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-ioring-sys/tests/handover.rs | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/windows-ioring-sys/tests/handover.rs b/crates/windows-ioring-sys/tests/handover.rs index 097ed155f..89023636a 100644 --- a/crates/windows-ioring-sys/tests/handover.rs +++ b/crates/windows-ioring-sys/tests/handover.rs @@ -538,6 +538,30 @@ fn attaching_while_unbuffered_reads_are_still_in_flight_strands_nothing() { // that never contained it. attached.elapsed(), )); + // **Why this proves the precondition, stated because a review read + // it the other way round.** The concern was that a read finishing + // between the attach and this poll makes the test pass while only + // exercising the already-completed case. It cannot, and the + // direction is what settles it. + // + // `drain_to_empty` loops until `try_pop` reports the queue EMPTY -- + // no cap, no early exit -- so `already_queued` is the total observed + // at a moment strictly AFTER the attach. A read that finishes in + // that window is therefore COUNTED, which pushes `already_queued` + // toward `width` and makes this branch LESS likely to be taken. The + // already-completed case it warns about is exactly the case where + // all `width` are drained and the flag is never set. + // + // So the error this can make is a false NEGATIVE, never a false + // positive -- and completion is monotonic, so a read outstanding at + // the (later) poll was outstanding at the (earlier) attach. The + // escalation over widths and attempts exists for the false + // negatives. + // + // The one assumption is that `try_pop` reports emptiness truthfully. + // A ring that claimed empty while holding completions would forge + // this precondition -- but that is a defect in the crate under test, + // and `contract.assert_quiescent()` below is what would catch it. if already_queued < width { caught_in_flight = true; } From a4a2f742599bf48e843e6335bcaa482663c9ca92 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Mon, 14 Sep 2026 01:22:11 -0700 Subject: [PATCH 46/46] fix(platform-probes): make `keys` refuse a row the oracle rejects `keys` called `deserialize_map` and never `Deserializer::end()`. `serde_json` stops at the end of the first value and does not care what follows, so two public readers disagreed about the same artifact. Measured before fixing: keys(r#"{"reason":"x-probe-topology"}garbage"#) -> ["reason"] check(...) -> Malformed { what: "trailing characters at line 1 column 30" } A caller reading keys directly was told a malformed row was readable. Reported by a review. `keys` now returns nothing when tokens follow the object, with a control asserting the same row WITHOUT the garbage still reads normally. The sabotage manifest anchored on the two lines this changed, so its entry was rewritten and re-verified: all eight anchors resolve, and entry 3 still compiles and is CAUGHT (nine tests fail) rather than becoming unviable. The explanatory comment moved above the block so the anchor stays pure code -- a substitution that has to skip a comment is one that silently stops matching when the comment is edited. **A stale motivation in `cross_check`, written in the present tense.** The paragraph over the `MeasuredButCountsAbsent` push said a zero parse beside a failed read "is filed as `not_compared` -- leaving `parse_in_doubt` false". That described the gap the push CLOSES, sitting directly above the push that closes it, so it read as current behaviour. Put in the past tense, with the effect on `parse_in_doubt` stated. **M2.5's acceptance criteria still named the deleted M2.1 prose oracle**, the same defect M2.4 carried and the same fix: the relation belongs in M3.2's invariant set and the row schema, checked through the renderer binding. Swept -- no remaining item ASKS for the retired machinery. Two newly added prose dates lost their bare form in favour of relative wording ("when M3 was archived", "in the same review round that reported this"), which is both offset-free and more durable than either date form. Two further findings were REJECTED. The stub links `[completed 2026-09-13]` use a bare date because that IS the stub format the repository rules specify; and `COMPLETED-CHECKLIST.md`'s `## Moved 2026-09-12` heading predates the timestamp rule, which says in terms that existing headings "are fine and are not worth going back to change" -- and editing it would breach that file's append-only invariant. The same heading was raised and answered two rounds ago. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/windows-platform-probes/CHECKLIST.md | 17 ++++++++----- crates/windows-platform-probes/sabotage.json | 8 +++++- .../src/report_oracle.rs | 18 ++++++++++++- .../src/report_oracle/tests.rs | 25 +++++++++++++++++++ .../windows-platform-probes/src/topology.rs | 16 +++++++++--- 5 files changed, 72 insertions(+), 12 deletions(-) diff --git a/crates/windows-platform-probes/CHECKLIST.md b/crates/windows-platform-probes/CHECKLIST.md index 79c59c474..68d9862ea 100644 --- a/crates/windows-platform-probes/CHECKLIST.md +++ b/crates/windows-platform-probes/CHECKLIST.md @@ -28,7 +28,7 @@ correctness in the archive. set from M3.2. Re-scoped by M3.2; the note at the top of this milestone gives the reasoning. **The item text - above was rewritten on 2026-09-13 to match**: it still asked for "the same correspondence + above was rewritten when M3 was archived, to match**: it still asked for "the same correspondence failures" and for promotion "into the oracle from M2.1", both retired by M3, so a reader working the list linearly would have been sent after the half that no longer exists. Found by a review -- and the lesson generalises, since a re-scoping note 25 lines above an item does not reach someone @@ -52,7 +52,8 @@ correctness in the archive. in plain sight. Deleting one of those push sites lets `verdict()` reach `agree` with `blocking_states` silent. Reported across three review rounds against two wordings of the claim; the claims in [src/topology/invariant.rs](src/topology/invariant.rs) and in - `every_numa_counter_branch_...` were narrowed on 2026-09-13 to stop overstating the coverage, + `every_numa_counter_branch_...` were narrowed in the same review round that reported this, + to stop overstating the coverage, which is why this item is the fix rather than the discovery. **Target:** each gains a `BlockingState` variant, a `blocking_states` branch, a `codes_for` arm, @@ -95,14 +96,18 @@ correctness in the archive. comparison that is itself new prose able to drift. The endpoint reads still earn their place: they catch structural change across the wider window that the counter bracket cannot see. - **This belongs to M2 rather than beside it:** "the banner describes the measured read" is a - correspondence invariant, so it should be expressed in the M2.1 oracle and checked on every rendered - report, not asserted once in a single test. + **Express it where M3 put the invariants, not in the M2.1 oracle.** "The banner describes the + measured read" is an invariant over the OBSERVATION, so it belongs in the invariant set from M3.2 + and, if the fact reaches the artifact, in the row schema -- checked on every rendered report through + the renderer binding rather than asserted once in a single test. **These two paragraphs were + rewritten when M3 landed**: they asked for the relation to be expressed in the M2.1 prose oracle, + which M3.4 deleted, so an executor would have gone looking for machinery that no longer exists. + Found by a review, and the same defect M2.4 carried. Reviewer disagreement is recorded deliberately, because it is evidence about the instrument rather than noise: across two rounds one reader raised this twice while two others cleared it, one of them explicitly after being pointed at the question. Nothing in the suite decides it either way, which is - itself the argument for the oracle. + itself the argument for making it an invariant rather than a test. - [ ] **M2.15** -- Run the probe suite on a second architecture in CI. diff --git a/crates/windows-platform-probes/sabotage.json b/crates/windows-platform-probes/sabotage.json index 3da271fc3..9ef82648b 100644 --- a/crates/windows-platform-probes/sabotage.json +++ b/crates/windows-platform-probes/sabotage.json @@ -51,7 +51,13 @@ "why": "The obvious simplification, and it is wrong twice over: a serde_json::Map SORTS its names and silently keeps the LAST of a repeated key -- so this loses the row's key ORDER, which is part of the contract MEASURED_ROW_KEYS states, and it deletes the evidence for RowDefect::RepeatedKey, the one malformation that survives a consumer's parse. This entry exists so that the reason the visitor is not a map cannot be forgotten.", "find": [ " let mut reader = serde_json::Deserializer::from_str(row);", - " serde::Deserializer::deserialize_map(&mut reader, TopLevelNames).unwrap_or_default()" + " let Ok(names) = serde::Deserializer::deserialize_map(&mut reader, TopLevelNames) else {", + " return Vec::new();", + " };", + " if reader.end().is_err() {", + " return Vec::new();", + " }", + " names" ], "replace": [ " serde_json::from_str::>(row)", diff --git a/crates/windows-platform-probes/src/report_oracle.rs b/crates/windows-platform-probes/src/report_oracle.rs index 570976250..b2590f620 100644 --- a/crates/windows-platform-probes/src/report_oracle.rs +++ b/crates/windows-platform-probes/src/report_oracle.rs @@ -231,8 +231,24 @@ pub fn keys(row: &str) -> Vec { } } + // **`end` matters, and its absence made two public functions disagree.** + // `serde_json` stops at the end of the first value and does not care what + // follows, so `{"reason":"x"}garbage` yielded `["reason"]` here while + // `check` reported `Malformed { what: "trailing characters ..." }` for the + // same row. A caller reading keys directly was told a malformed artifact + // was readable. Measured, then fixed; reported by a review. + // + // The block below is one sabotage anchor and is deliberately free of + // comments, so replacing the visitor with a parsed map stays a single + // contiguous substitution that still compiles. let mut reader = serde_json::Deserializer::from_str(row); - serde::Deserializer::deserialize_map(&mut reader, TopLevelNames).unwrap_or_default() + let Ok(names) = serde::Deserializer::deserialize_map(&mut reader, TopLevelNames) else { + return Vec::new(); + }; + if reader.end().is_err() { + return Vec::new(); + } + names } /// The `code` of every entry in `row`'s list-valued `key`. /// diff --git a/crates/windows-platform-probes/src/report_oracle/tests.rs b/crates/windows-platform-probes/src/report_oracle/tests.rs index a3e7386a6..487b1d33c 100644 --- a/crates/windows-platform-probes/src/report_oracle/tests.rs +++ b/crates/windows-platform-probes/src/report_oracle/tests.rs @@ -96,6 +96,31 @@ fn a_trailing_separator_is_a_defect() { ); } +#[test] +fn the_key_reader_and_the_oracle_agree_about_trailing_garbage() { + // **Two public readers disagreed about the same row.** `serde_json` stops + // at the end of the first value and does not care what follows, so without + // `Deserializer::end()` this row gave `keys` a clean `["reason"]` while + // `check` reported `Malformed { what: "trailing characters ..." }`. + // Measured before the fix, exactly that pair. A caller reading keys + // directly was told a malformed artifact was readable. Found by a review. + let row = r#"{"reason":"x-probe-topology"}garbage"#; + + assert!(malformation(row).is_some(), "the oracle rejects it"); + assert_eq!( + keys(row), + Vec::::new(), + "and the key reader must not read it as though it were whole" + ); + + // The control: the same row WITHOUT the garbage is read normally, so the + // rule is not simply refusing everything. + assert_eq!( + keys(r#"{"reason":"x-probe-topology"}"#), + vec!["reason".to_owned()] + ); +} + #[test] fn valid_json_that_is_not_an_object_is_a_malformation() { // **Pins the `Map` in `malformation`, which the corpus cannot reach.** The diff --git a/crates/windows-platform-probes/src/topology.rs b/crates/windows-platform-probes/src/topology.rs index 039a55d15..6ac61ebd4 100644 --- a/crates/windows-platform-probes/src/topology.rs +++ b/crates/windows-platform-probes/src/topology.rs @@ -597,10 +597,18 @@ impl Observation { // A running machine has processors and groups whatever the enumeration // said, so a MEASURED topology reporting none of either did not describe - // its host. Nothing below reaches this: both raw counters report failure - // as zero, so a zero parse beside a failed read is filed as - // `not_compared` -- leaving `parse_in_doubt` false, and the report free - // to state an impossible machine without a caveat. + // its host. + // + // **The gap this closes, in the past tense it belongs in.** Nothing else + // reaches the case: both raw counters report failure as zero, so a zero + // parse beside a failed read WAS filed as `not_compared` alone -- which + // left `parse_in_doubt` false and the report free to state an impossible + // machine without a caveat. The push below is what changed that, and it + // makes `parse_in_doubt` true for exactly this case. + // + // The paragraph above described the old behaviour in the present tense, + // so it read as though this rule did not exist while sitting directly + // over it. Found by a review. // // Stated over the LIST rather than once per count, so a third such count // joins the array instead of needing its own rule to be remembered.