[scripts, CI, docs] (P7): the 184-symbol link-closure ratchet lands as a gate

CONTRACT-P6.md 12.1 has carried "a ratchet recomputes the 184 in CI and asserts it only
ever falls" as an obligation since P6. The roadmap handed it to P6.5; the P6.5 line never
received it, and scripts/ci/ has never had anything that computes this number. ID-P7-7
gives it to P7 wave 0. This is it.

scripts/link_ratchet.py mechanises a6's hand experiment: partition the per-object output
of the single MobileGL target into SERVER / FRONTEND / SHARED, then compute
(SERVER undefined) - (SERVER defined) - (SHARED defined) intersect (FRONTEND defined),
which is the undefined-symbol list a real link of a server-only image would print. The
partition is an explicit table with a rationale per row, because it IS the module boundary
the build system does not have (SOURCE_FILES is one flat list; boundaries are P13's work).
An object matching no row is a hard failure, as is a build whose CMakeCache lacks the three
disaggregated options, or an empty role set - every way this gate could go green without
having measured anything is an explicit exit 2.

Measured on 78b7d6be: 186 symbols. The three backend buckets reproduce a6's hand-made
101 / 14 / 60 exactly, as do its per-object referrer and definer distributions
(UniformManager 90, VulkanRenderer 60, Managers 53, DirectGLES 51; TextureObject 59,
SamplerObject 30, BufferObject 18, FramebufferObject 16, PipeFill 9). Nothing from a6's
184 has been cleared; the +2 are both P6.5's:
  Client::ClientSession::StartSpawned()                 - fork-per-session
  GLState::ProgramArtifactsSchemaFingerprint()          - archive v2 caps

The baseline stores the LIST, not a count (ID-P7-7): a count-only ratchet goes green on the
day one symbol is cleared and another added. --assert-monotone fails only on symbols absent
from the baseline and names each one's defining object and every referring object; symbols
that disappeared are reported with an instruction to re-baseline in the same commit, and
never fail.

The CI step goes in build-linux-split, not build-linux: build-linux takes the three options
from MOBILEGL_CI_DISAGGREGATED, which is ON only on feat/disaggregated and
codex/fix-github-ci, so on any other branch it builds a monolith and the gate would measure
a SERVER set with no transport half. The JSON is uploaded even when the gate fires.

Two findings for the integrator, both written up in the note:
  - a6's four published bucket totals sum to 181 against its own headline of 184. These
    buckets are a strict partition and always sum to the headline; the difference is in
    a6's core bucket, which it listed selectively.
  - 5 of the 14 "P3b/P4b" symbols are MG_Pipe slot-allocator and apply-thread plumbing
    that sits under MG_Impl/Pipe/ by file location alone. P3b/P4b cannot clear them by
    migrating Espryt functionality; they need P13's module move. That bucket's floor is
    5, not 0, so the plan's 0.5-day Tier 1 estimate for "D12's 14 symbols" should be
    re-cut against 9. All 7 symbols from that object carry a `# P13` annotation.

red-once (R-16) is recorded verbatim in the note. Worth knowing: the plan's own example
(SamplerObject::GetMinFilter) does NOT turn this gate red, because it is already in the
baseline - the ratchet reddens on new symbols, so a negative control has to pick one that
is not in the 186. A reach to GetBorderColorForm() from DirectGLES/Utils.cpp does, naming
the symbol, its definer and its referrer, exit 1; reverted and green again.
This commit is contained in:
2026-09-22 14:26:04 -04:00
parent 1de8f02ebe
commit 8bb6309a1c
5 changed files with 1340 additions and 1 deletions
+42
View File
@@ -987,6 +987,48 @@ jobs:
fi
echo "libMobileGL.so defines ${remote} MG_Remote symbol(s) and the MGPipe applier (${defined} defined symbols)"
# The link-closure ratchet (CONTRACT-P6.md 12.1, ID-P7-7): the set of frontend symbols a
# server-only image would still have to link, recomputed from the per-object partition in
# scripts/link_ratchet.py and compared against the stored LIST.
#
# THIS job and not `build-linux`: build-linux passes the same three options from
# ${MOBILEGL_CI_DISAGGREGATED}, which is ON only on feat/disaggregated and
# codex/fix-github-ci, so on every other branch it builds a monolith - MG_Remote would not
# be compiled at all, the SERVER half of the partition would be empty, and the gate would
# go green having measured nothing. This job turns the three options on unconditionally,
# and the nm step immediately above has just proved they took. The script re-reads them
# from CMakeCache.txt anyway and refuses the run if they are not all ON.
#
# Only NEW symbols fail. A symbol that has DISAPPEARED is the ratchet turning and is
# printed with an instruction to re-baseline in the same commit - the baseline stores the
# list rather than a count precisely so that clearing one symbol while adding another does
# not walk through at an unchanged total (ID-P7-7).
#
# It reads the per-object output of the MobileGL target, so it must run before anything
# that cleans the build tree; the packaging step below excludes *.o from the tarball but
# does not delete them.
- name: The server image reaches no further into the frontend than the baseline
run: |
mkdir -p ci-artifacts
python3 scripts/link_ratchet.py --self-test
python3 scripts/link_ratchet.py \
--build-dir "${BUILD_DIR}" \
--baseline scripts/data/link_ratchet_baseline.txt \
--assert-monotone \
--bucket \
--json ci-artifacts/link-ratchet.json
# Uploaded even when the gate fired: a red ratchet is exactly the moment someone needs the
# per-symbol definer/referrer attribution the JSON carries, and a job that failed before
# uploading its evidence is a job nobody can act on.
- name: Upload link ratchet
if: always()
uses: actions/upload-artifact@v7
with:
name: link-ratchet
path: ci-artifacts/link-ratchet.json
if-no-files-found: warn
- name: Show ccache stats
if: always()
run: ccache --show-stats