Skip to content

Commit 5b693ef

Browse files
committed
fix(build): repair the selective-component gate, which was red and built into /tmp (#2415)
Three defects, not one. (1) The gate had been RED since 2026-08-19, behind a green test count. #1889 legitimately gave Text.Json a public Collections.Core dependency -- fail-fast enumeration needs detail::MutationCounter and the boundary validator rejected the private declaration outright -- while forbidden_text_json_collections still asserted List.hpp was unreachable. It went unnoticed because that script is not part of CLAUDE.md rule 2's gate. The fixture is RETARGETED rather than deleted, because CLAUDE.md names it as an invariant. List.hpp was only ever a proxy for "Collections"; the proxy moves to BlockingCollection, the type that sentence is actually about. That is strictly stronger: Collections.Blocking publicly needs Threading, so the include can only compile if Text.Json acquired a Threading requirement -- pinning by COMPILATION what assert_target_absent pinned by target name alone. (2) A second, independent defect in the same script: MATRIX_ROOT was a bare mktemp -d with nothing setting TMPDIR, so every run built eight selective component trees into /tmp -- the one place the build-resource policy exists to keep builds out of, and build-tmp/ is in the closed directory list described as exactly this redirect. The mechanism was designed and never wired up. (3) Nothing ran the script, which is why (1) survived a day. It now runs last in local_ci_check.sh, costs a measured ~10 minutes at two jobs stated in the script rather than left as a surprise, and is deliberately not behind an opt-out: a check that can be skipped is the check that rotted. Script exits 0, with the renamed fixture genuinely exercised. Gate unchanged at 17,700/38 -- no production code touched.
1 parent c897758 commit 5b693ef

9 files changed

Lines changed: 197 additions & 11 deletions

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

NEXT.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,37 @@
139139
> and each is itemised in §2 below. **§4b says where to look next, and the method that found all
140140
> three of today's tickets.**
141141

142+
## 2026-08-20 — #2415: the selective-component gate was red, and building into `/tmp`
143+
144+
**Gate unchanged at 17,700 / 38** — no production code was touched. This repairs a *gate*, and the
145+
build-policy violation that gate was committing on every run.
146+
147+
**Three defects, not one.**
148+
149+
1. **Red since 2026-08-19, behind a green test count.** #1889 legitimately gave `Text.Json` a
150+
**public** `Collections.Core` dependency — fail-fast enumeration needs `detail::MutationCounter`,
151+
and the boundary validator *rejected the private declaration outright* — while
152+
`forbidden_text_json_collections` still asserted `List.hpp` was unreachable. **Retargeted rather
153+
than deleted**, because CLAUDE.md names the fixture as an invariant: `List.hpp` was only a
154+
**proxy** for "Collections", and the proxy moves to `BlockingCollection`, the type that sentence
155+
is actually about. **Strictly stronger** — `Collections.Blocking` publicly needs `Threading`, so
156+
the include can only compile if `Text.Json` acquired a `Threading` requirement, pinning **by
157+
compilation** what `assert_target_absent sharp_runtime_threading` pinned by target name.
158+
159+
2. **A second, independent defect in the same script.** `MATRIX_ROOT="$(mktemp -d)"` with nothing in
160+
the repository setting `TMPDIR`, so every run built **eight selective component trees into
161+
`/tmp`** — the one place the build-resource policy exists to keep builds out of, and `build-tmp/`
162+
is in the closed directory list described as exactly this redirect. **The mechanism was designed
163+
and never wired up.**
164+
165+
3. **Nothing ran the script**, which is why (1) survived a day. `local_ci_check.sh` ran the boundary
166+
validator, the seam checker and the negative-fixture checker but not this. It now runs **last**,
167+
costs a **measured ~10 minutes at two jobs** stated in the script rather than left as a surprise,
168+
and is deliberately **not** behind an opt-out — a check that can be skipped is the check that
169+
rotted.
170+
171+
`docs/Migration-SelectiveComponentCheckRepair.md`.
172+
142173
## 2026-08-20 — #1945: four `XmlConvert` arguments accepted and discarded
143174

144175
**Gate: 17,700 / 38, 0 failed, 0 skipped** (+6, `Xml` 518 → 524). Graph **41/94 → 41/95**,
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
<!-- Copyright (c) Robert Vokac and contributors -->
3+
4+
# `check_selective_components.sh` was red, and built into `/tmp`#2415
5+
6+
**No production code changed.** This is a repair of a gate, and of the build-policy violation that
7+
gate was committing on every run. The test count is unmoved at **17,700 / 38**.
8+
9+
## Defect 1 — red since 2026-08-19, behind a green test count
10+
11+
`scripts/check_selective_components.sh` failed:
12+
13+
```
14+
FAIL: forbidden consumer fixture forbidden_text_json_collections compiled
15+
```
16+
17+
and it failed **identically on a clean tree** — verified by stashing the in-flight change and
18+
re-running, so it was not caused by the work that surfaced it.
19+
20+
**Cause.** `test/consumer/forbidden_text_json_collections.cpp` included
21+
`System/Collections/Generic/List.hpp` beside `JsonDocument.hpp` and asserted the pair must not
22+
compile in a selective `Text.Json` build. **#1889 made that boundary legitimate**: `JsonArray` and
23+
`JsonObject` gained fail-fast enumeration, which needs
24+
`System::Collections::detail::MutationCounter`, and the module-boundary validator **rejected the
25+
private declaration outright** — so `modules/text-json` took `Collections.Core` as a **public**
26+
dependency. The fixture then asserted a boundary that had deliberately been moved.
27+
28+
**Why it went unnoticed for a day.** This script is **not part of CLAUDE.md rule 2's gate**, so a
29+
green 17,68x reading said nothing about it. Every checkpoint since #1889 was recorded as green while
30+
this was red.
31+
32+
**The repair retargets rather than deletes**, because CLAUDE.md names the fixture as an invariant:
33+
*"BlockingCollection<T> belongs to Collections.Blocking; do not add its Threading requirements back
34+
to Collections.Core or weaken the Text.Json isolation fixture."* `List.hpp` was only ever a **proxy**
35+
for "Collections"; the proxy moves to the type that sentence is actually about, and the file is
36+
renamed so its name still says what it asserts.
37+
38+
**The new proxy is strictly stronger.** `Collections.Blocking` declares `PUBLIC_DEPENDENCIES
39+
Collections.Core Core.Base Threading`, so the include can only compile if `Text.Json` has acquired a
40+
`Threading` requirement — which is exactly what the surrounding `assert_target_absent
41+
sharp_runtime_threading` exists to prevent, now pinned **by compilation** rather than by a target
42+
name alone.
43+
44+
## Defect 2 — the script built into `/tmp`
45+
46+
`MATRIX_ROOT="$(mktemp -d)"`, with **nothing in the repository setting `TMPDIR`**. CLAUDE.md's
47+
build-resource policy says in terms: *"Never create a build tree under `/tmp`, `/var/tmp`, or
48+
`/dev/shm` … Redirect `mktemp`-based scripts through a repository-local `TMPDIR` (this repository
49+
uses `build-tmp/`)"* — and `build-tmp/` is in the closed list of build directories, described as
50+
exactly this. **The mechanism was designed and never wired up.**
51+
52+
This script configures and builds **one selective component tree per matrix entry**, so every run
53+
was putting eight build trees in the one place the policy exists to keep builds out of. Now:
54+
55+
```bash
56+
mkdir -p "$REPO_ROOT/build-tmp"
57+
MATRIX_ROOT="$(TMPDIR="$REPO_ROOT/build-tmp" mktemp -d)"
58+
```
59+
60+
The script's existing `EXIT` trap still removes its own tree, so nothing accumulates.
61+
62+
## Defect 3 — nothing ran it
63+
64+
The proximate cause of defect 1 surviving a day is that **no routine invoked this script**.
65+
`scripts/local_ci_check.sh` — which its own header calls a pre-push gate — ran the boundary
66+
validator, the seam checker and the negative-fixture checker, but not this.
67+
68+
It now runs, **last**, so the cheap checks and the full suite still report first. **It costs about
69+
ten minutes** (measured 2026-08-20 at two jobs), and that is stated in the script rather than left
70+
as a surprise. It is deliberately **not** behind an opt-out: a check that can be skipped is the
71+
check that rotted.
72+
73+
## Evidence
74+
75+
`scripts/check_selective_components.sh` exits **0**, with the log showing the renamed fixture
76+
genuinely exercised:
77+
78+
```
79+
negative fixture forbidden_text_json_collections_blocking rejected
80+
negative fixture forbidden_text_json_object_model rejected
81+
```
82+
83+
Gate unchanged at **17,700 / 38, 0 failed, 0 skipped** — no production code was touched. Module
84+
graph **41 / 95**. Negative fixture set **53 / 269**, unchanged.

plan.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,37 @@ exactly, this batch's own tests, no regression**. Graph **41 / 92**, seams **3 /
155155
sole member is now remediated**; CCF-019 open; CCF-021/#2131 and CCF-022/#2109 unminted. Doxygen,
156156
`ccache` and `/rv` absent. Maximum compiler parallelism **2 jobs**.*
157157

158+
## 2026-08-20 — #2415: the selective-component gate was red, and building into `/tmp`
159+
160+
**Gate unchanged at 17,700 / 38** — no production code was touched. This repairs a *gate*, and the
161+
build-policy violation that gate was committing on every run.
162+
163+
**Three defects, not one.**
164+
165+
1. **Red since 2026-08-19, behind a green test count.** #1889 legitimately gave `Text.Json` a
166+
**public** `Collections.Core` dependency — fail-fast enumeration needs `detail::MutationCounter`,
167+
and the boundary validator *rejected the private declaration outright* — while
168+
`forbidden_text_json_collections` still asserted `List.hpp` was unreachable. **Retargeted rather
169+
than deleted**, because CLAUDE.md names the fixture as an invariant: `List.hpp` was only a
170+
**proxy** for "Collections", and the proxy moves to `BlockingCollection`, the type that sentence
171+
is actually about. **Strictly stronger** — `Collections.Blocking` publicly needs `Threading`, so
172+
the include can only compile if `Text.Json` acquired a `Threading` requirement, pinning **by
173+
compilation** what `assert_target_absent sharp_runtime_threading` pinned by target name.
174+
175+
2. **A second, independent defect in the same script.** `MATRIX_ROOT="$(mktemp -d)"` with nothing in
176+
the repository setting `TMPDIR`, so every run built **eight selective component trees into
177+
`/tmp`** — the one place the build-resource policy exists to keep builds out of, and `build-tmp/`
178+
is in the closed directory list described as exactly this redirect. **The mechanism was designed
179+
and never wired up.**
180+
181+
3. **Nothing ran the script**, which is why (1) survived a day. `local_ci_check.sh` ran the boundary
182+
validator, the seam checker and the negative-fixture checker but not this. It now runs **last**,
183+
costs a **measured ~10 minutes at two jobs** stated in the script rather than left as a surprise,
184+
and is deliberately **not** behind an opt-out — a check that can be skipped is the check that
185+
rotted.
186+
187+
`docs/Migration-SelectiveComponentCheckRepair.md`.
188+
158189
## 2026-08-20 — #1945: four `XmlConvert` arguments accepted and discarded
159190

160191
**Gate: 17,700 / 38, 0 failed, 0 skipped** (+6, `Xml` 518 → 524). Graph **41/94 → 41/95**,

plan.sqlite3

0 Bytes
Binary file not shown.

scripts/check_selective_components.sh

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@ set -euo pipefail
66
export PYTHONDONTWRITEBYTECODE=1
77

88
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
9-
MATRIX_ROOT="$(mktemp -d)"
9+
# CLAUDE.md's build-resource policy forbids a build tree under /tmp and names `build-tmp/` as
10+
# this repository's local TMPDIR for exactly this kind of mktemp-based script. THE MECHANISM WAS
11+
# DESIGNED AND NEVER WIRED UP: this line was a bare `mktemp -d`, so every run of this script --
12+
# which configures and builds one selective component tree per matrix entry -- built into /tmp,
13+
# the one place the policy exists to keep builds out of. Found and repaired by #2415.
14+
mkdir -p "$REPO_ROOT/build-tmp"
15+
MATRIX_ROOT="$(TMPDIR="$REPO_ROOT/build-tmp" mktemp -d)"
1016
trap 'rm -rf "$MATRIX_ROOT"' EXIT
1117

1218
# Resolve the same repository job budget as local CI and export it to every
@@ -111,7 +117,7 @@ check_component() {
111117
echo "FAIL: Text.Json unexpectedly configured ZLIB" >&2
112118
exit 1
113119
fi
114-
expect_consumer_failure Text.Json forbidden_text_json_collections.cpp
120+
expect_consumer_failure Text.Json forbidden_text_json_collections_blocking.cpp
115121
expect_consumer_failure Text.Json forbidden_text_json_object_model.cpp
116122
elif [ "$component" = "Xml.Linq" ]; then
117123
expect_consumer_failure Xml.Linq forbidden_xml_diagnostics.cpp

scripts/local_ci_check.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,17 @@ echo " build clean: 0 warnings, 0 errors"
6060
echo "==> Running tests"
6161
scripts/run_component_tests.sh "$BUILD_DIR"
6262

63+
# ADDED BY #2415, AND THE REASON IS THAT ITS ABSENCE WAS THE DEFECT. The selective-component
64+
# check verifies an invariant nothing else does -- that a component built on its own drags in no
65+
# more than it declares -- and because NOTHING RAN IT, it sat red for a day behind a green test
66+
# count: #1889 legitimately gave Text.Json a public Collections.Core dependency while a fixture
67+
# still asserted the opposite, and CLAUDE.md rule 2's gate says nothing about either.
68+
#
69+
# IT COSTS ABOUT TEN MINUTES (measured 2026-08-20, two jobs), because it configures and builds one
70+
# selective tree per matrix entry. That is stated here rather than left as a surprise. It runs
71+
# LAST, so the cheap checks and the full suite still report first, and it is deliberately NOT
72+
# behind an opt-out: a check that can be skipped is the check that rotted.
73+
echo "==> Checking selective component isolation (~10 min)"
74+
scripts/check_selective_components.sh
75+
6376
echo "==> Local CI check passed"

test/consumer/forbidden_text_json_collections.cpp

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// SPDX-License-Identifier: MIT
2+
// Copyright (c) Robert Vokac and contributors
3+
//
4+
// RETARGETED BY #2415, AND RENAMED SO THE NAME STILL SAYS WHAT IT ASSERTS.
5+
//
6+
// This fixture used to include `System/Collections/Generic/List.hpp` and assert that a selective
7+
// `Text.Json` build could not reach it. **#1889 made that boundary legitimate**: `JsonArray` and
8+
// `JsonObject` gained fail-fast enumeration, which needs
9+
// `System::Collections::detail::MutationCounter`, and the module-boundary validator REJECTED the
10+
// private declaration outright -- so `modules/text-json` took `Collections.Core` as a PUBLIC
11+
// dependency. The fixture then asserted a boundary that had deliberately been moved, and it has
12+
// been failing since 2026-08-19 behind a green test count.
13+
//
14+
// IT IS RETARGETED RATHER THAN DELETED, because CLAUDE.md names it as an invariant:
15+
// "BlockingCollection<T> belongs to Collections.Blocking; do not add its Threading requirements
16+
// back to Collections.Core or weaken the Text.Json isolation fixture." `List.hpp` was only ever a
17+
// PROXY for "Collections"; the proxy moves to the type that sentence is actually about.
18+
//
19+
// AND THE NEW PROXY IS STRICTLY STRONGER than the old one. `Collections.Blocking` declares
20+
// `PUBLIC_DEPENDENCIES Collections.Core Core.Base Threading`, so this include can only compile if
21+
// `Text.Json` has acquired a `Threading` requirement -- which is precisely what the surrounding
22+
// `assert_target_absent sharp_runtime_threading` exists to prevent, now pinned by compilation
23+
// rather than by a target name alone.
24+
#include "System/Text/Json/JsonDocument.hpp"
25+
#include "System/Collections/Concurrent/BlockingCollection.hpp"
26+
27+
int main() {
28+
return 0;
29+
}

0 commit comments

Comments
 (0)