|
| 1 | +# P-022 step 7a (#260) — the sweep: five pinned repositories, large-solution controls, examples, path forms |
| 2 | + |
| 3 | +> **Scope of this note.** It is the record of #260's **final acceptance |
| 4 | +> measurement** — the part of its test matrix the acceptance surfaces over the |
| 5 | +> committed corpus deliberately did not take: the five pinned OSS repositories |
| 6 | +> of #243 at their pinned commits, the large/multi-project solution controls, |
| 7 | +> the `examples/` tree, and the Windows path forms. The contract it runs under |
| 8 | +> is D-4..D-7, B-2, B-3, R-1 and R-2 in the |
| 9 | +> [owner-decision ledger](p022-shadow-infra-owner-decisions.md); this note may |
| 10 | +> not reopen any of them. What the committed corpus already proves is |
| 11 | +> [the acceptance note](p022-shadow-acceptance.md); this is its §6 first two |
| 12 | +> bullets being taken rather than restated. |
| 13 | +
|
| 14 | +**Where the numbers are.** Every count this sweep *produced* — documents, |
| 15 | +outcomes, denominators per target, byte sizes, wall-clock — lives in |
| 16 | +[`docs/generated/p022-shadow-sweep.md`](../generated/p022-shadow-sweep.md), |
| 17 | +rendered by `scripts/render_checkpoint_status.py` from the recorded run, and is |
| 18 | +reached from here by link. The inventory below is the other kind of number: the |
| 19 | +*inputs* measured from the checkouts before anything ran, so that the matrix |
| 20 | +this note promises is a matrix somebody measured rather than one somebody |
| 21 | +estimated. Those are stated here and also carried as data in |
| 22 | +[`docs/evidence/p022-shadow-sweep.json`](../evidence/p022-shadow-sweep.json). |
| 23 | + |
| 24 | +--- |
| 25 | + |
| 26 | +## §1 — The targets, the pins, and what this environment can actually do |
| 27 | + |
| 28 | +### 1.1 The five, and the pin rule |
| 29 | + |
| 30 | +The targets and commits are #243's, **reused verbatim** and never re-resolved |
| 31 | +against upstream HEAD. They are copied from |
| 32 | +[`docs/notes/precision-remeasure-2026-07-12-data/`](precision-remeasure-2026-07-12-data/), |
| 33 | +which is the machine-readable record of the last sweep that used them, not from |
| 34 | +the prose of the note beside it. |
| 35 | + |
| 36 | +| target | upstream | pinned commit | |
| 37 | +|---|---|---| |
| 38 | +| ShareX | `ShareX/ShareX` | `0df9ca4d83eed9d2489048c539d7d1fc2860fdec` | |
| 39 | +| MahApps.Metro | `MahApps/MahApps.Metro` | `72099e310bac2d12ac98fd7560b69679252519f5` | |
| 40 | +| MaterialDesignInXamlToolkit | `MaterialDesignInXAML/MaterialDesignInXamlToolkit` | `ef3a5ea434e39182b1848f5e11aaea6b3890581f` | |
| 41 | +| AvalonEdit | `icsharpcode/AvalonEdit` | `ed0bd149059469ac9bd39b13cf8a341b12a6c1da` | |
| 42 | +| ClosedXML | `ClosedXML/ClosedXML` | `4e89dcedd83cad553e84d2d97f77fc3d7deb630f` | |
| 43 | + |
| 44 | +**The pin rule.** A checkout is materialized by fetching *the commit itself* |
| 45 | +(`git fetch --depth 1 origin <sha>`, then `git checkout --detach FETCH_HEAD`), |
| 46 | +so what lands is the pin rather than a branch tip that happens to contain it. |
| 47 | +Before anything else runs, `git rev-parse HEAD` is compared with the pin. **A |
| 48 | +mismatch is a hard failure of that target, not a warning and not a newer |
| 49 | +measurement.** A target that moved has not been measured; it has been replaced. |
| 50 | + |
| 51 | +The same rule applies to the *port's* half. The adapter is named in every |
| 52 | +recorded comparison by `sha256` and byte length as well as by path, and |
| 53 | +`OWN_SHADOW_ENGINE` is always set explicitly — a driver that found |
| 54 | +`rust/target/debug/own-shadow-engine` because the environment was unset |
| 55 | +compared *an* engine, not *the* engine. |
| 56 | + |
| 57 | +### 1.2 What this environment is |
| 58 | + |
| 59 | +This environment **can** run the whole matrix, so it does: the G.0 fallback |
| 60 | +(take the measurement through the workflow and read its artifacts back) is not |
| 61 | +used, and the recorded run is a local one whose commands, commits and artifact |
| 62 | +identities are on the record. What made that possible, measured rather than |
| 63 | +assumed: |
| 64 | + |
| 65 | +| capability | what is present | |
| 66 | +|---|---| |
| 67 | +| .NET SDK | 10.0.400 — builds the extractor's pinned `net8.0` target | |
| 68 | +| .NET runtimes | `Microsoft.NETCore.App` 8.0.30 and `Microsoft.WindowsDesktop.App` 8.0.30 | |
| 69 | +| Roslyn | pinned by the extractor itself (`Microsoft.CodeAnalysis.CSharp` 4.9.2), so the SDK version does not move the parse | |
| 70 | +| Rust | 1.98.1 (`x86_64-pc-windows-msvc`), installed for this work; MSVC 14.44 + Windows SDK 10.0.26100 were already present | |
| 71 | +| host | Windows 11 — which is why the path-form leg of the matrix is a *local* measurement here and a `windows-latest` job in CI | |
| 72 | + |
| 73 | +The reference pack is materialized exactly the way `ci.yml`'s |
| 74 | +`corpus-benchmark` job does it — a scratch `net8.0-windows` project with |
| 75 | +`UseWPF` / `UseWindowsForms` / `EnableWindowsTargeting`, `dotnet restore`, then |
| 76 | +the resolved `ref/net8.0` directory exported as `OWN_EXTRA_REF_DIRS`. It |
| 77 | +resolves to `microsoft.windowsdesktop.app.ref` **8.0.30** here; the 2026-07-12 |
| 78 | +remeasure resolved **8.0.28**. Both carry 47 DLLs. That difference is recorded |
| 79 | +rather than pinned away because **it cannot bias this measurement**: whatever it |
| 80 | +does to a facts document, both engines receive that document as the same |
| 81 | +bytes. It would matter to a precision remeasure; this is not one. |
| 82 | + |
| 83 | +### 1.3 The disk and wall-clock the matrix costs |
| 84 | + |
| 85 | +Measured on this machine before the matrix was promised, one target extracted |
| 86 | +first (AvalonEdit, the smallest) and then the rest: |
| 87 | + |
| 88 | +| item | measured | |
| 89 | +|---|---| |
| 90 | +| the five checkouts, on disk | ~147 MB total (AvalonEdit ~3 MB, ClosedXML ~26 MB, MahApps.Metro ~32 MB, ShareX ~41 MB, MaterialDesign ~45 MB) | |
| 91 | +| extraction, all ten documents | ~98 s total; the slowest single document is ShareX's directory walk at ~20 s | |
| 92 | +| the ten facts documents | ~1.1 MB total; the largest is ~250 KB | |
| 93 | +| compare, per document | well under a second — the committed-corpus gate runs 104 documents in ~0.7 s | |
| 94 | + |
| 95 | +The consequence for the timeout policy is in §2.4: nothing here is near the |
| 96 | +driver's default, and the default is therefore kept and *recorded* rather than |
| 97 | +raised on a guess. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## §2 — The matrix, exactly as it will be run |
| 102 | + |
| 103 | +### 2.1 Which targets carry a solution — measured, from the checkout |
| 104 | + |
| 105 | +Counted by resolving each `.sln`'s `Project(...)` entries to `.csproj` members |
| 106 | +that exist on disk, and counting the `.cs` files under those members: |
| 107 | + |
| 108 | +| target | solutions found | member projects | `.cs` under members | |
| 109 | +|---|---|---|---| |
| 110 | +| ShareX | `ShareX.sln` | 13 | 1126 | |
| 111 | +| | `ShareX.ImageEditor.sln` | 2 | 433 | |
| 112 | +| ClosedXML | `ClosedXML.sln` | 6 | 921 | |
| 113 | +| MahApps.Metro | `src/MahApps.Metro.sln` | 4 | 328 | |
| 114 | +| AvalonEdit | `ICSharpCode.AvalonEdit.sln` | 3 | 248 | |
| 115 | +| | `ICSharpCode.AvalonEdit.Documentation.sln` | 1 | 214 | |
| 116 | +| MaterialDesignInXamlToolkit | **none** — `MaterialDesignToolkit.Full.slnx` only | — | — | |
| 117 | + |
| 118 | +**MaterialDesignInXamlToolkit has no classic solution at its pinned commit.** |
| 119 | +It carries the XML solution format (`.slnx`), and the extractor's solution |
| 120 | +resolver reads the classic `Project("{...}") = "name", "path"` form only. That |
| 121 | +is a *measured gap*, recorded here and nowhere else acted on: teaching the |
| 122 | +extractor `.slnx` would be a production change, and this task changes no |
| 123 | +production behaviour. The target is therefore covered by its directory walk |
| 124 | +alone, and the record says so per target rather than averaging it away. |
| 125 | + |
| 126 | +### 2.2 The large-solution controls taken |
| 127 | + |
| 128 | +#260 asks for "selected large/multi-project solutions" and this task's brief for |
| 129 | +"at least the two largest". Taken: **the largest solution of every target that |
| 130 | +has one** — `ShareX.sln` (13 members), `ClosedXML.sln` (6), `MahApps.Metro.sln` |
| 131 | +(4), `ICSharpCode.AvalonEdit.sln` (3). The two largest are the first two; the |
| 132 | +other two are nearly free and make the mode's coverage per-target rather than |
| 133 | +per-sample. `ShareX.ImageEditor.sln` and the AvalonEdit documentation solution |
| 134 | +are proper subsets of the solutions already taken and are skipped for that |
| 135 | +reason, which is a choice on the record rather than a silent omission. |
| 136 | + |
| 137 | +The solution documents are **not** the directory-walk documents under another |
| 138 | +name. Measured on ShareX: the two documents carry the same 106 components and |
| 139 | +74 functions, in a **different order** — the fan-out enumerates the source set |
| 140 | +project by project, the walk enumerates it in directory order — so they have |
| 141 | +identical byte length and different digests, and they are two documents, not |
| 142 | +one. That is exactly the property a large-solution control is for: a different |
| 143 | +extractor path, a differently ordered document, and per-layer ordering |
| 144 | +semantics that are *declared* rather than normalized away. |
| 145 | + |
| 146 | +### 2.3 The ten documents, and the commands, verbatim |
| 147 | + |
| 148 | +Ten documents, each extracted **exactly once**. `scripts/own-check.sh` runs the |
| 149 | +extractor once into a temporary file and `--emit-facts <path>` persists exactly |
| 150 | +that file; stage 2 (`python -m ownlang ownir`) is the reference's *verdict* path |
| 151 | +and is neither a second extraction nor the comparison. The comparison is the |
| 152 | +driver's, taken from the emitted bytes. |
| 153 | + |
| 154 | +```bash |
| 155 | +# per document, with <REFDIR> the materialized WindowsDesktop ref pack (§1.2) |
| 156 | +OWN_EXTRA_REF_DIRS=<REFDIR> scripts/own-check.sh \ |
| 157 | + --format sarif --severity warning \ |
| 158 | + --emit-facts <FACTS> -- <INPUT> |
| 159 | +``` |
| 160 | + |
| 161 | +| document | mode | `<INPUT>` | |
| 162 | +|---|---|---| |
| 163 | +| `ShareX.repo` | directory walk | `<checkout>/ShareX` | |
| 164 | +| `MahApps.Metro.repo` | directory walk | `<checkout>/MahApps.Metro` | |
| 165 | +| `MaterialDesignInXamlToolkit.repo` | directory walk | `<checkout>/MaterialDesignInXamlToolkit` | |
| 166 | +| `AvalonEdit.repo` | directory walk | `<checkout>/AvalonEdit` | |
| 167 | +| `ClosedXML.repo` | directory walk | `<checkout>/ClosedXML` | |
| 168 | +| `ShareX.sln` | `.sln` fan-out | `<checkout>/ShareX/ShareX.sln` | |
| 169 | +| `ClosedXML.sln` | `.sln` fan-out | `<checkout>/ClosedXML/ClosedXML.sln` | |
| 170 | +| `MahApps.Metro.sln` | `.sln` fan-out | `<checkout>/MahApps.Metro/src/MahApps.Metro.sln` | |
| 171 | +| `AvalonEdit.sln` | `.sln` fan-out | `<checkout>/AvalonEdit/ICSharpCode.AvalonEdit.sln` | |
| 172 | +| `examples` | directory walk | `<Own.NET>/examples` | |
| 173 | + |
| 174 | +The driver then runs **once** over a manifest naming all ten: |
| 175 | + |
| 176 | +```bash |
| 177 | +cd rust && cargo build --release -p own-shadow --bin own-shadow-engine |
| 178 | +OWN_SHADOW_ENGINE=<the adapter just built> \ |
| 179 | + python scripts/shadow_compare.py --engine compare \ |
| 180 | + --manifest <manifest.json> --out <run dir> --quiet |
| 181 | +``` |
| 182 | + |
| 183 | +and the Windows path-form leg is the committed-corpus gate, on this host, with |
| 184 | +the adapter built on this host: |
| 185 | + |
| 186 | +```bash |
| 187 | +OWN_SHADOW_ENGINE=<the adapter just built> \ |
| 188 | + python scripts/shadow_compare.py --engine compare --corpus --quiet |
| 189 | +OWN_SHADOW_ENGINE=<the adapter just built> OWN_SHADOW_COMPARE_REQUIRED=1 \ |
| 190 | + python tests/test_shadow_compare.py |
| 191 | +``` |
| 192 | + |
| 193 | +### 2.4 What "covered" means here, and the timeout policy |
| 194 | + |
| 195 | +**A repository is not covered because extraction succeeded, and a solution is |
| 196 | +not covered because some project inside it emitted OwnIR.** Coverage is a |
| 197 | +recorded, non-empty set of OwnIR documents fed byte-identically to both engines |
| 198 | +and judged by compare mode — *per target, with the denominator on the record*. |
| 199 | +This is the direct descendant of the fifth failure mode in #250: a green gate |
| 200 | +over an empty set is worse than a red one, because a red one at least says it |
| 201 | +is awake. Hence three rules the driver enforces rather than this note asserting |
| 202 | +them: |
| 203 | + |
| 204 | +* a run that compared **zero** documents is a **failure**, never agreement; |
| 205 | +* a **target** whose compare-attempted count is zero is a **failed target**, |
| 206 | + never a passed repository; |
| 207 | +* every document's `facts_sha256` in the manifest is checked against the bytes |
| 208 | + the driver actually read, and every document is validated **before any engine |
| 209 | + runs**. |
| 210 | + |
| 211 | +**Timeouts.** Explicit per document and recorded per document. §1.3 measured the |
| 212 | +whole matrix as sub-second per compare, three orders of magnitude inside the |
| 213 | +driver's 120 s default, so the default is what the manifest carries — stated as |
| 214 | +a number in the manifest rather than inherited silently, so that raising it for |
| 215 | +a future target is a visible edit. A timeout is an **execution failure with a |
| 216 | +report** (R-2), never a silent skip. |
| 217 | + |
| 218 | +### 2.5 The sweep ledger schema |
| 219 | + |
| 220 | +Two documents, the same split every campaign in this tree uses — a |
| 221 | +**definition** that says what should be measured and a **result** that records |
| 222 | +one actual run of it, with one interpreter reading both. |
| 223 | + |
| 224 | +`docs/evidence/p022-shadow-sweep.json` — the definition: |
| 225 | + |
| 226 | +```text |
| 227 | +schema, sweep, description |
| 228 | +adapter_build_command how the port's half is produced |
| 229 | +driver_version the shadow_compare_version this definition expects |
| 230 | +timeout_seconds the default the manifest carries |
| 231 | +targets[] { target, upstream, pinned_commit, solutions[], slnx[], notes } |
| 232 | +documents[] { id, target, extraction_mode, extraction_command, input, |
| 233 | + timeout_seconds } |
| 234 | +``` |
| 235 | + |
| 236 | +`docs/evidence/p022-shadow-sweep.result.json` — one run: |
| 237 | + |
| 238 | +```text |
| 239 | +schema, sweep, definition, definition_sha256 |
| 240 | +source_commit, recorded_at, host, workflow_run_url (null for a local run) |
| 241 | +adapter { path, sha256, bytes } |
| 242 | +driver_version |
| 243 | +documents[] { id, source, target, target_commit, extraction_mode, |
| 244 | + extraction_command, facts_sha256, raw { digest, bytes }, |
| 245 | + canonical { digest, bytes }, outcome, timeout_seconds, |
| 246 | + reduction_outcome, by_kind{}, by_acceptance{}, |
| 247 | + derived_outcome, wall_clock_seconds } |
| 248 | +targets[] { target, documents_extracted, compare_attempted, agreed, |
| 249 | + diverged, execution_failures, input_refusals, |
| 250 | + input_disagreements, declared_boundary_observations, |
| 251 | + acceptance_unexplained_observations } |
| 252 | +totals { the same fields, summed } |
| 253 | +``` |
| 254 | + |
| 255 | +`tests/shadow_sweep.py` is the single interpreter of the pair. |
| 256 | +`scripts/render_checkpoint_status.py` renders |
| 257 | +`docs/generated/p022-shadow-sweep.md` from it and |
| 258 | +`tests/test_checkpoint_status.py` gates that the committed fragment equals the |
| 259 | +projection — the same pipeline every campaign count in this repository already |
| 260 | +goes through. **A re-run replaces the result whole; it is never patched.** |
| 261 | + |
| 262 | +### 2.6 The churn budget, written before anything moved |
| 263 | + |
| 264 | +* `tests/fixtures/` — **nothing**, unless a divergence produces an artifact |
| 265 | + small enough to be a control, in which case it is added insertion-stable and |
| 266 | + the finding that produced it is on the record with it. The frozen goldens do |
| 267 | + not move for a measurement. |
| 268 | +* `docs/evidence/` — gains the sweep definition and one recorded result, plus |
| 269 | + one campaign definition and result over the driver's new pieces. |
| 270 | +* `docs/generated/` — gains `p022-shadow-sweep.md` and the campaign fragment |
| 271 | + regenerates. |
| 272 | +* **The five repositories' facts documents do not enter the repository.** They |
| 273 | + are large and exactly reproducible from the pins and the commands in §2.3; |
| 274 | + their *identities* (the raw digest and byte length of each, and the canonical |
| 275 | + identity both engines derived) are what the result records. |
| 276 | +* **Production behaviour changes nowhere.** Not in `ownlang/`, not in the |
| 277 | + analyses, not in the renderers, not in `own-check.sh`'s verdict path. The |
| 278 | + driver, the adapter, the workflow and the evidence pipeline are dev tooling. |
| 279 | +* **The comparison machinery is frozen for the measurement.** The canonical |
| 280 | + form, the artifact verifier, the trace, the reducer, `BOUNDARY_POLICY`, the |
| 281 | + derived-SARIF configuration and the driver's *judgement* do not move while |
| 282 | + the sweep runs. A divergence is a finding first; adjusting a normalizer to |
| 283 | + make one go away is a contract change that waits for the owner. |
0 commit comments