Skip to content

docs: contract admission design (index + package floor) + pin fresh-install mcpp to 0.0.102#268

Merged
Sunrisepeak merged 2 commits into
mainfrom
docs/contract-admission-design
Jul 22, 2026
Merged

docs: contract admission design (index + package floor) + pin fresh-install mcpp to 0.0.102#268
Sunrisepeak merged 2 commits into
mainfrom
docs/contract-admission-design

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary

Design document only — no engine changes. Plus one CI fix.

.agents/docs/2026-07-22-contract-admission-design.md specifies the shape the index→client version contract should have, covering both the existing index-level floor (index.toml min_mcpp) and the future descriptor-level one (mcpp = { min_mcpp = ... }).

Key positions:

  • Three responsibilities, three functions. Contract evaluation must split into a pure predicate, an idempotent decision, and the single stateful reporter. Fusing them into one optional<string> (today's check_index_floor) makes the decision non-idempotent — the dedup set silently turns the second and later calls into "no violation".
  • The contract is a candidate-set filter, not a check on the read path. It is a property of the index tree, evaluated where trees are enumerated — which is what roadmap W5 already called the open-time check.
  • I7 — a contract miss must never be a reason to try another candidate. Only at the source layer (index trees, which are interchangeable) does it show up as filtering; once identity is fixed (a specific package's descriptor), it is terminal for that entity. This is the direct lesson of mcpp new --template imgui fails on all platforms: package 'imgui@1.92.8' has no mcpp.toml #265, where "contract miss → guess another namespace" resolved a different package.
  • A refused index is excluded, never fatal. A too-new private index must not take down mcpp toolchain install or the upgrade path.
  • Precise attribution on a miss: "X is present in index 'mcpplibs', but that index requires mcpp >= N" instead of pointing the user at mcpp index update, which cannot fix it.
  • Rollout order is forced for the package-level key. Unknown mcpp-segment keys are skipped today (xpkg.cppm:1519-1528), so if an index adopts min_mcpp before clients know the key, it is silently ignored by exactly the clients it addresses. Client support must ship and propagate first.
  • Package-level floors also remove the collateral damage class: mcpp-index Record GL runtime PR status #107 raised the whole-tree floor for one package's grammar, which is what put an unrelated imgui template in the blast radius.

#265 is filed as the empirical input (appendix §6), not as a defect fixed here. It is only observable in the out-of-contract state ("client older than index") that this mechanism redefines, and the red CI was a version skew. The residual — an out-of-contract client silently using descriptors it cannot read on the dependency path — is recorded and gets removed with the mechanism.

CI change

ci-fresh-install now installs an explicit mcpp@${MCPP_PIN} (workflow-level env, 0.0.102) at all four install sites instead of "newest in the runner's index copy".

Why: in run 29873685262 the wait-index guard reported index tracks 0.0.102 at 22:30:02 and the jobs installed xim:mcpp@0.0.100 ten seconds later — the guard polls raw.githubusercontent.com while the runner resolves through its own index copy, so a green guard does not mean the version is installable. That 0.0.100 then met an index whose floor was 0.0.101, which is how #265 surfaced. Pinning turns a silent downgrade into an explicit version not found.

Bump MCPP_PIN together with the .xlings.json workspace pin at each release. ci-aarch64-fresh-install is deliberately left unpinned — its stated purpose is to simulate a brand-new user running bare xlings install mcpp.

Test plan

  • Workflow YAML parses; env.MCPP_PIN visible to all five jobs (bash ${MCPP_PIN} ×3, pwsh $env:MCPP_PIN ×1)
  • ci-fresh-install on the next release / manual dispatch — expect mcpp 0.0.102 in the "Install mcpp and config mirror" step
  • No engine code touched, so the build/test suites are unaffected

Follow-ups (not here)

  1. Implement the mechanism per §3 of the document (with the test gaps it lists: the stateful half of index_contract has no unit coverage, and the floor has no e2e at all).
  2. Index-side lint (§2.5): a descriptor must not use grammar newer than its own declared min_mcpp.
  3. Ops invariant (§5): do not raise index.toml min_mcpp before the release channel can serve a client that satisfies it.

CI restructuring (second commit)

Warm-cache PR runs before this change: ci-windows 20.4 min, ci-linux 18.2 min, both single sequential jobs. Everything below is wall-clock, not work.

Shared setup, extracted

  • .github/actions/bootstrap-mcpp — the cache lineage (mcpp sandbox + xlings + target/) and the xlings bootstrap, so split jobs share one definition instead of a 40-line copy-pasted preamble each. The target/ cache is now keyed per job: the toolchain legs clean and rebuild, so a shared key had them overwriting each other's lineage (today ci-linux saves an LLVM-built target/ under the key everything else restores from).
  • .github/actions/setup-macos-llvm — same for the macOS-specific setup (xlings LLVM + a sandbox wired to that exact package).

Splits

workflow before after
ci-linux 1 job, 18.2 min: build → unit → gcc → musl → llvm → xlings 4 independent jobs: build-test, toolchain-gcc, toolchain-cross (musl+llvm), integration-xlings~7–8 min
ci-windows 1 job, 20.4 min (e2e alone was 9.7) build-test (+package) and toolchains (+regressions) → ~8 min; e2e moved to ci-windows-e2e
ci-windows-e2e new, 2 shards → ~8 min
ci-macos 1 job, 8.4 min (e2e 3.4) e2e removed → ~5 min
ci-macos-e2e new, single job → ~5 min (not sharded: 3.5 min is under the setup+build floor)
ci-linux-e2e 1 job, 8.5 min 2 shards → ~5.5 min

No needs: between the split jobs — each pays one warm mcpp build (~2.5 min), which is cheaper than serialising them behind an uploaded artifact would be.

e2e sharding

tests/e2e/run_all.sh gains E2E_SHARD="<index>/<total>" (1-based), a round-robin slice of the suite. Round-robin rather than contiguous ranges because per-test durations are wildly uneven — the runner's existing "slowest first" timing report exists for exactly that reason. The slice is computed on the full file list before capability filtering, so a test's shard does not move when host capabilities differ. 147 tests split 74/73.

Coverage changes (deliberate, call them out in review)

  • Dropped the duplicate mcpp test in the Linux GCC leg: same suite, same toolchain, same driver binary as build-test, differing only in incremental state. The cold-build assurance lives in the mcpp build half, which is kept.
  • Windows packaging now zips the binary from the default build rather than the one left behind by the LLVM self-host rebuild. Same toolchain resolution, one less rebuild in the packaging path.
  • Windows integration keeps its original position before the LLVM rebuild that invalidates $MCPP_SELF.

Test plan

  • All workflow + composite YAML parses; job graph verified
  • Shard split verified locally (74/73 of 147)
  • This PR's own CI is the proof — compare the new critical path against the 20.4 min baseline

… pin fresh-install mcpp

Design only — no engine changes.

The contract mechanism ("is this index/descriptor newer than I can
trust?") is specified as two levels: the existing index.toml floor, and
a future `mcpp = { min_mcpp = ... }` descriptor floor. The document
fixes the shape both must share:

- contract evaluation splits into pure predicate / idempotent decision /
  the one stateful reporter — fusing them makes the decision itself
  non-idempotent
- the contract is a candidate-set filter, not a check on the read path
- I7: a contract miss must never be a reason to try another candidate.
  Only at the source layer (index trees, interchangeable) does it show
  up as filtering; once identity is fixed, it is terminal for that entity
- a refused index is excluded, never fatal — a too-new private index
  must not take down `mcpp toolchain install` or the upgrade path
- the package-level key must enter the client vocabulary BEFORE any
  index uses it: unknown mcpp-segment keys are skipped today, so an
  early rollout would be silently ignored by exactly the clients it
  addresses

#265 is filed as the empirical input (appendix), not as a defect to fix
separately: it is only observable in the out-of-contract state this
mechanism redefines, and the red CI was a version skew.

CI: ci-fresh-install now installs an explicit `mcpp@${MCPP_PIN}` (0.0.102)
instead of "newest in the runner's index copy". On 2026-07-21 the
wait-index guard reported "index tracks 0.0.102" and the jobs installed
0.0.100 ten seconds later — the guard polls raw.githubusercontent.com
while the runner resolves through its own index copy. Pinning turns a
silent downgrade into an explicit `version not found`. Bump with the
.xlings.json workspace pin at release. ci-aarch64-fresh-install stays
unpinned by design (it simulates a brand-new user).
…x job

Critical path was max(ci-windows 20.4m, ci-linux 18.2m) on a warm PR run.
Both were single sequential jobs; everything below is wall-clock, not work.

- .github/actions/bootstrap-mcpp: composite action for the cache lineage
  (mcpp sandbox + xlings + target/) and the xlings bootstrap, so the split
  jobs share ONE definition instead of a 40-line copy-pasted preamble each.
  Target cache is now keyed per job — the toolchain legs `clean` and rebuild,
  so a shared key had them clobbering each other's lineage.
- .github/actions/setup-macos-llvm: same idea for the macOS-specific setup
  (xlings LLVM + sandbox wired to that exact package).

- ci-linux.yml: one job -> four independent jobs (build+unit, cold gcc
  self-host, musl+llvm, xlings integration). No `needs:` — each pays one warm
  `mcpp build` (~2.5m), which is cheaper than serialising behind an artifact.
  Dropped the duplicate `mcpp test` in the gcc leg: same suite, same
  toolchain, same driver binary as build-test, differing only in incremental
  state. ~18m -> ~7-8m.
- ci-windows.yml: e2e moved out (9.7m of 20.4m); remainder split into
  build+test+package and toolchains+regressions. Integration keeps its
  original position before the llvm rebuild that invalidates $MCPP_SELF.
  ~20m -> ~8m.
- ci-windows-e2e.yml, ci-macos-e2e.yml: new, mirroring ci-linux-e2e.yml.
  macOS is not sharded — 3.5m is under this workflow's setup+build floor.
- ci-macos.yml: e2e step removed, setup replaced by the composite; the clang
  probe/module validation steps stay here.

- run_all.sh: E2E_SHARD="<index>/<total>" runs a round-robin slice. Round
  robin rather than contiguous ranges because per-test durations are wildly
  uneven (the existing timing report exists for exactly that reason). The
  slice is computed on the full file list before capability filtering, so a
  test's shard does not move with host capabilities. Linux and Windows e2e
  run 2 shards; 147 tests split 74/73.

Expected critical path: ~20.4m -> ~8m.
@Sunrisepeak
Sunrisepeak force-pushed the docs/contract-admission-design branch from a07cb40 to 9cd0cfe Compare July 22, 2026 09:38
@Sunrisepeak
Sunrisepeak merged commit a768f48 into main Jul 22, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant