Skip to content

Tags: mcpp-community/mcpp

Tags

v2026.9.28.2

Toggle v2026.9.28.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
release canaries: run each command under the step's bash, by path (#731)

The first release run of the canaries (36363585412) failed every GalTranslPP command before building: release_canaries.py started bash by name, and a Windows program that does so gets System32's WSL launcher, which the loader finds before PATH. The gate held; no tag was created. The workflow names the step's bash (CANARY_BASH, cygpath -w on Windows) and the runner uses it; tests/scripts/test_release_canaries.py covers the runner and runs in the Linux static checks. Dispatched on this branch, the canaries built xlings and mcppls with the candidate, and the GalTranslPP build ran instead of failing at its first command.

v2026.9.28.1

Toggle v2026.9.28.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.28.1: eight reports (#717, #718, #720, #722-#726), one downloa…

…d progress, and an index floor that is a tip (#727)

* docs: eight reports after 2026.9.27.1, the design and the implementation plan

* fix: a host-module lib root is ordered with the units of its package (#720)

The lib root was placed at the head of the host-module compile list before
the list was sorted by imports, so a lib root that imports a sibling unit
was compiled first and failed. The lib root is now the first node of the
same sort; it is emitted first whenever it imports nothing of its package,
so existing packages keep their order. e2e 807 covers an explicit and a
conventional lib root.

* fix: an index that requires a newer mcpp is a closing tip, not an error

The read site printed the E0006 text as error: at the start of every run
that read such an index, including runs that then resolved every package
elsewhere and exited 0. The fact is now recorded without printing:
- a run that fails carries the E0006 text in the message that stops it;
- a run that refreshed an index and met a floor ends with one tip: line,
  printed after the command's output (closing notices, mcpp.ui), and an
  envelope reports it as the note MCPP_INDEX_REQUIRES_NEWER_MCPP;
- mcpp self doctor lists every index whose floor this mcpp does not meet;
- a tree recorded as unusable answers no later lookup either;
- the E0006 upgrade note starts on its own line.
Unit tests for the read site, the hint, the notices; e2e 812.

* docs: regenerate the design-record index

* W1 (mcpp#725): a rooted workspace reaches its own path dependency, and -p resolves the package first

A rooted workspace (root [package] + [workspace]) built as itself never set
state.wsManifest / state.runtimeWorkspaceRoot, so a member reached through the
root's own [dependencies] path entry was loaded as an ordinary path dependency:
its x.workspace = true entries were refused or silently unresolved, and it
received none of [workspace.package] / [workspace.build]. The workspace context
is now set in that branch too, before any dependency is loaded, matching the
member-switch branch and satisfying "a member is a member however it is
reached" (graph.cpp's depIsMember).

-p, --package <NAME> promised a package but both resolvers
(manifest.cpp's inline loop and project.cppm's resolve_member_dir) matched only
a member's directory basename or path. They are now one function in
project.cppm, resolving in order: a member's qualified name
(<namespace>.<name>), then its bare package name (refused, naming every match,
when shared by two or more members), then its directory path or basename (kept
as a fallback; a duplicate basename keeps today's first-match selection but now
warns naming the others). A value that is one member's package name and a
different member's directory selects the package, with a warning naming the
other member.

SPEC-004 §9 item 1 names the missing position and states that -p resolves the
package identity first; docs/07 §5.3 (en, zh) and the four `-p` help strings
state the resolution order.

Tests: six unit tests for the resolver in test_workspace_inheritance.cpp
(package/path/basename all selecting one member, same name under two
namespaces, package-outranks-directory with a warning, duplicate-basename
warning, the "not found" listing, and the unaffected no-filter cases); e2e 805
(a rooted workspace's own path dependency, with a non-latest pinned version,
an omitted package.version, a [workspace.build] flag, and a shared root
[toolchain], served from a project-local index so the build touches no
network) and e2e 806 (-p's three resolution steps end to end). Both e2e
scripts fail on the released 2026.9.27.1 binary and on 2026.9.26.1, and pass
with this fix.

Co-authored-by: speak-agent <agent@mcpp-community>

* W6 (#723): one destination, one content, one writer for a deploy target

Two or more source paths for one deploy destination no longer collide at
planning. `add_deploy` (src/build/plan.cppm) merges them into one
`DeployFile` entry instead of refusing a second source path; a `BuildPlan`
whose deploy has exactly one source still produces the byte-identical
`build.ninja` line it always has. The merged destination becomes one
`stage_file` edge with every source as an input (src/build/ninja_backend.cppm),
and `mcpp stage` places it once all sources agree byte-for-byte
(`stage_files`, src/build/stage.cppm), otherwise failing and naming every
source and the destination. `cmd_stage` (src/cli/cmd_build.cppm) accepts one
or more source positionals.

One destination, one writer: `place-dlls`'s post-link DLL placement
(src/pack/pack.cppm `place_runtime_dlls`, invoked from
src/cli/cmd_publish.cppm `cmd_place_dlls`) now receives the set of names the
merged deploy list already places beside a program, computed in
ninja_backend.cppm and passed through a per-edge `$placed` ninja variable so
the generated graph stays stable. `place_runtime_dlls` skips those names
instead of overwriting them, comparing content and reporting a difference as
a warning.

SPEC-007 R4.2 and R4.3 are amended to state the content check and the
single-writer rule; docs/04 (en, zh) is corrected to match. e2e 810 covers
the merge and the build-time refusal; e2e 811 (`# requires: windows`, not run
here) covers the single-writer rule; e2e 646's collision case is updated for
the new build-time message. Unit tests cover `stage_files`'s multi-source
behaviour and the deploy-list skip/warn logic in `place_runtime_dlls`.

Co-Authored-By: Claude <[email protected]>

* 2026.9.27.2: on Windows an xlings invocation leaves the process environment as it found it and starts in the registry's home (#726)

On Windows, build_command_prefix prepended the registry's subos/default/bin
to the process PATH and set XLINGS_HOME process-wide, and ran xlings in
mcpp's working directory. After a build installed a payload, every action
found xim:llvm's cl/link/lib/rc shims in front of MSVC's tools, and vcpkg's
compiler detection failed; a project with a .xlings.json at its root also
received the shims of mcpp's toolchain and payloads in its own SubOS.

ScopedInvocationEnv now applies XLINGS_HOME, the scope variables and the
PATH prefix for the invocation and restores them afterwards, and the Windows
prefix starts with `cd /d "<home>" &&`, as the POSIX prefix does.

Refs #726.

* T3 (#724 W3/W4/W5): a device source is not a compile unit; a failed build
program's own diagnostic survives; emit writes no project file

W3 (src/build/plan.cppm): the compile-unit loop now skips
SourceKind::Device graph units, so build.ninja carries no dead cxx_object
edge for a rule-claimed device source, and compile_commands.json / the S1
document agree without their own filter (unit_invocations already excluded
only NASM; nothing else needed to change). The source stays in `watch` and
still reaches the package's build program through MCPP_DEVICE_SOURCES,
since both read the manifest's sources glob directly, not plan.compileUnits.
Checked every other consumer of plan.compileUnits (prepare/plan.cpp's
dependency-cache collection keys by path, not by index, so it is unaffected
beyond a smaller artifact set for a package with device sources).

W4: a package whose build program failed under `emit`'s plan_only records
MCPP_BUILD_DATABASE_PROGRAM_FAILED and applies none of its directives
(state.cppm: new PrepareState::programFailedPackages, set at both call
sites in target_side.cpp and features.cpp). The device-source orphan check
in target_side.cpp now skips such a package outright, instead of reading
every device source as unclaimed and failing the whole member. Also: notes
a phase recorded before prepare_build's own failing return are no longer
silently dropped (driver.cpp: a thread_local sink in the same per-run-sink
style as mcpp::build::refusal, exported as
mcpp::build::take_notes_on_failure); cmd_build.cppm's emit failure path
folds any such note into the one diagnostic SPEC-005 R5.2 allows a wholly-
failed member (path stays the member's mcpp.toml, exactly one entry), so
the true cause is not lost behind a downstream symptom without violating
that invariant. hasProgram's existing exists(build.mcpp) check is now
correct by construction, since a failed program's package never reaches it.

W5 (src/build/prepare/xlings.cpp): ensure_project_index_dir's two calls
under a private work_dir are collapsed into the one call the
ownerRoot==workRoot branch always made, targeting workRoot in every case.
Previously the runtime-environment half (deps/subos/workspace) went to
runtimeSelection.ownerRoot, which is always the real project root
regardless of emit's private work_dir -- so `emit build-database` wrote
<root>/.mcpp/.xlings.json into a project that declares [xlings] deps.

SPEC-005: R3.7 names device sources beside NASM units (both absent from S1
and compile_commands.json, for different reasons -- NASM is a compile unit
excluded from export, a device source is never a compile unit at all). R5.2
gains the sentence that a check whose premise is a build program's
directives does not run for a package whose program failed in this pass.
R2.1 needed no change.

Tests: e2e 808 (device source: no dead ninja edge, absent from both
databases, and the rule still compiles it and the build still runs), e2e
809 (a device source plus a build.mcpp that does not compile: PROGRAM_FAILED
with path build.mcpp, no device-source mention, package still described),
e2e 817 (688's project-tree digest repeated on a stub-xlings fixture with
[xlings] deps: byte-identical tree, no .mcpp/.xlings.json, no write-project
effect, and the private work directory does gain one naming the
dependency). Each fails against the released 2026.9.27.1 binary and passes
on the fresh build. Full regression: all 15 emit/build-database e2e
scripts, 798, and four more that exercise the compile-unit loop (asm/GAS,
NASM, object-path-collision, multi-module) all still pass; `mcpp test`
(130 unit tests) passes.

* docs: #726 and #727 join the round (W13); the split moves to the last stage

* W10 (#724): the S1 document names what a rule generates (S1 0.3.0)

A set's ide.generated lists each output of its package's source-role
actions (header or source, with the step's id, inputs, arguments and
work directory) and each generated include directory its units name,
with the path the document names and the path a mcpp build of the same
selection writes. The profile version is 0.3.0 (Sunrisepeak/
mcpp-language-server#28); compile_commands.json is unchanged. SPEC-005
R3.12, docs 50; e2e 815, e2e 688 reads the new version.

* W11: one renderer for every acquisition; plain output off a terminal

- ProgressBar prints one start line and one finish line when stdout is not
  a terminal: no carriage return, no erase sequence, no repaint per frame;
  an item that does not complete says so instead of reporting it done.
- The index refresh runs through xlings interface update_packages and draws
  its progress and download events with the same renderer; an xlings that
  emits none shows its status line as before, and its terminal text no
  longer reaches the output.
- The clone of a git dependency passes --progress and draws the download
  phase, read as it is redrawn (run_streaming_bounded gains an opt-in rule
  that a lone carriage return ends a line); the output is kept whole for
  the failure message, and a clone silent for fifteen minutes is stopped.
- The sandbox bootstrap's hand-drawn spinner is the shared bar.
e2e stubs accept the interface refresh; unit tests for the git progress
parser, both render modes and the line splitting; e2e 816; docs 09.

* W11: draw one bar per index-refresh phase, not one per event message

* T7 (#717 W8): a graph-wide dialect switch under a target condition

[target.<selector>.build] dialect_cxxflags is now accepted, parsed into a
new ConditionalConfig member kept apart from BuildInputs (the key is
graph-wide, not a per-package additive input), and merged into the same
BuildConfig::dialectCxxflags every consumer already reads: the std BMI
prebuild, the scan, every TU and the fingerprint. Only the root of the
build renders it; a dependency's own value (conditional or not) reaches
no command and is now excluded from its own fingerprint contribution,
matching the rule that a key enters a fingerprint only where it reaches
a command. The build-program directive is deliberately not added.

SPEC-004 SS3.1 and SS9 item 10 state the new rule; docs/04 and docs/zh/04
document the conditional form (since 2026.9.28.1). Unit tests cover
parsing, the emptiness gate, and root-only resolution order. e2e 813
covers reach into the std BMI/scan/TUs, a non-matching selector, the
A-B-A std BMI rebuild, and a dependency's key being fingerprint-inert;
it fails on the released 2026.9.27.1 binary and passes on the fresh one.

* W11: an index sync bar names its repository

* W6: place-dlls decides the other writer's DLLs from the directory

The one-writer rule passed the deploy list's names to place-dlls on its
command line. The plan's deploy set reads runtime search directories that
a prepare action fills, so it differs between the first and the second
plan; the command changed and every build after the first re-ran the
placement (e2e 797, found by a differential run against 2026.9.27.1).
place-dlls now treats a DLL beside the program that it did not place,
and that a runtime search directory also offers, as another writer's;
the command is the one 2026.9.27.1 wrote.

* chore: version 2026.9.28.1 in both places (mcpp.toml and MCPP_VERSION)

* T8 (#718 W9): the CRT model is a property of the MSVC ABI, not of cl.exe

Every MSVC-ABI row now receives the same CRT model: cl spells it /MT or
/MD, clang++ targeting *-windows-msvc spells it -fms-runtime-lib=static
or =dll. One helper (msvc_abi_crt_word, dialect.cppm) decides the word
for the translation units, the std/std.compat BMIs and the link command
alike, closing #649 E10 (a compile-only flag never reached the clang
driver's own link-time choice of -defaultlib:).

toolchain-coupled (the dynamic CRT, with the toolset's own
vcruntime140.dll/msvcp140.dll staged beside the artifact) is now the
default for every role on this ABI (dist::msvc_abi_default_contract,
ContractStatement::msvcAbiDefault). A toolset with no VC\Redist\MSVC
directory defaults to host-coupled silently and refuses an explicit
toolchain-coupled, naming the missing directory (prepare/plan.cpp). A
free-form CRT word in cxxflags/dialect_cxxflags is checked against the
resolved model: agreeing is a warning, contradicting is a refusal
(dialect::check_crt_word, wired in prepare/scan.cpp).

The toolset's redistributable directory is carried as its own
Toolchain field (msvcRedistDir), populated for cl from vc_redist_dir
and for the LLVM row from its sysroot's tools directory
(vc_redist_dir_for_tools_dir) rather than from linkRuntimeDirs, which
holds LLVM's own runtime directories on that row. The staging gate and
the mcpp run/test search path both read this field, gated on the MSVC
ABI rather than on which compiler is in use. mcpp pack carries the
staged DLLs by default; an explicit --mode system now resolves a
defaulted (never-declared) toolchain-coupled contract to host-coupled
instead of refusing.

e2e 703 is inverted to the new default; e2e 814 covers the LLVM row's
import table, staged DLL, clean-PATH run, self-contained round trip,
BMI switching (A, B, A) and pack modes (Windows-only, unverified here).
Unit tests cover the CRT-word derivation, the free-form-word check, the
MSVC-ABI default/redistributable resolution and a compute_flags-level
property test across both rows; the link-line half of that test is
gated on mcpp.platform.is_windows, since link_shape resolves
LinkShape::PeLld only when current_link_host() is Windows and a
Linux-built mcpp cannot reach that branch regardless of the plan's
target triple.

docs/20, docs/zh/20 and SPEC-006 record the new default and the
upgrade; docs/04 needed no change (it only points at docs/20).

131/131 unit tests pass on Linux (gcc 16.1.0 and llvm 22.1.8 rows);
docs structure/style checks pass.

* docs(specs): SPEC-004 1.9, SPEC-005 1.5, SPEC-006 0.3, SPEC-007 0.4, and the index

* docs(changelog): 2026.9.28.1

* docs: implementation readings of the round (13.5)

* docs(50): the refusal token msvc-redist-unavailable (#718)

* chore: xlings pin 2026.9.28.1 (interface protocol 1.2, the progress events)

* review: every set of a package names what its build program generates; place-dlls comment matches its command; docs/20 names the --mode system refusal

* T6 (mcpp#722, W7): split phase13_finish (plan.cpp) into sub-steps

Verbatim extraction along the sections its own banners already name:
prebuilt dependencies, link forms, make_plan, the C++ runtime checks,
graph/schedule, declared build-graph actions, assembly units, Windows
resources, the global dependency cache, mcpp.lock, runtime provider
overrides, ABI enforcement, resolution.json, and the empty-link check.
Longest resulting function: 356 lines (step13_build_graph_actions).

* T6 (mcpp#722, W7): split phase4b_graph_worklist (graph.cpp) into sub-steps

The worklist step for one item is split into identity resolution, the
already-resolved / identity-adoption handling (with its own version-merge
sub-step), acquiring a fresh dependency's source and manifest, and
finalizing it (recording the package, recursing into children); the
per-item locals that cross those boundaries move into a phase-local
struct, WorklistItemCtx, the same PrepareState pattern one level deeper.
The post-loop cycle check is its own function. Preamble closures that
captured only `state` (or nothing) become static file-scope functions,
called with an explicit PrepareState& where they used to close over it;
this also fixes the one comment that had gone factually stale (activateFeatures's
group banner said 'defined as local lambdas, not file-scope functions' --
now they are file-scope, and are still safe because a static function
carries no external linkage into the module's exported interface).
Longest resulting function: 300 lines (step4b_identity_version_merge).

* T6 (mcpp#722, W7): split phase6_features_and_host_tools (features.cpp) into sub-steps

Along the sections its own banners already name: feature activation,
device extensions and rule application, the graph's [xlings.workspace]
provisioned before build.mcpp, host-module registration, host-tool
provisioning, the dependencies' build programs, and capability binding.
aggregatedRequest (needed by two of these sections) is promoted from a
local lambda to a file-scope function of PrepareState&. Two scoping
braces that had no matching close within their own section (opened to
wrap several sections at once) are dropped as redundant once the
content is distributed across separate functions, each of which
supplies its own scope.
Longest resulting function: 425 lines (step6_provision_host_tools).

* T6 (mcpp#722, W7): split phase9_target_side (target_side.cpp) into sub-steps

Gather the graph's target-side candidates into a phase-local struct
(TargetSideGather, the PrepareState pattern one level deeper), then
resolve and realise [c-abi], broadcast the include set, check kernel-abi
interfaces and layer requirements, apply the layer-conditional config
(L1b), decide each dependency's link form (#519), define the
graph_package_entry closure, run the root build.mcpp (L3), require every
device source to reach an action, and handle re-run inputs (R1.3). Two
scoping braces that wrapped several sections at once are dropped as
redundant, as in the two previous files.
Longest resulting function: 404 lines (step9_kernel_abi_interfaces_and_requirements).

* T6 (mcpp#722, W7): split phase4a_graph_load (graph_load.cpp) into sub-steps

The closures phase4a_graph_load assigns onto state (each captures only
state) split into two groups: split/identity closures, and candidate
selection closures. LoadedDep is hoisted to file scope so it stays
visible to state.loadVersionDep, which remains where it was.

state.loadVersionDep itself (509 lines) is not split further: its six
local closures (readLuaContent, findRawInstalled,
installedLayoutMatchesIndex, revisionIsCurrent, findCompleteInstalled,
markInstalled) mutually capture nine-odd shared locals by reference:
hoisting them to free functions would mean threading all of that
through explicit parameter lists for one closure, judged higher risk
than benefit within this round; noted as a residual in the T6 report.

* review: the round's CI failures and the three review angles

CI (PR #727):
- e2e 190/191 select the program by name; bin/ also holds the staged
  redistributable DLLs on an MSVC-ABI row (#718).
- A DLL found in a runtime search directory yields to a declared deploy of
  the same name, and a difference is warned at planning, where a successful
  build shows it (SPEC-007 R4.3, e2e 811; e2e 818 is its Linux-hosted form).
- build.mcpp that imports only a build rule compiles in the build directory
  under GCC (e2e 807).
- The runtime environment half of .xlings.json goes to the runtime's owner
  again; only plan_only redirects it to the planning directory (e2e 205, 817).

Review:
- Every set of a package names what its build program generates (R3.12).
- A dependency's contradicting CRT word is refused; debug CRT words are
  refused; the agreeing-word message names the key of the dynamic model; the
  dependency cache key carries the CRT word.
- PE deploy destinations compare without case; MSVC version directories
  compare numerically; a missing deploy source is named as missing.
- A failed index refresh draws "did not complete"; an automatic refresh that
  exhausts its retries warns; the floor tip names the version and the
  install-aware upgrade.
- -p compares paths as paths; the help names the qualified form.

* docs: global review and the first CI run of the round (13.6)

* T6 (mcpp#722, W7): split phase1_toolchain_spec_and_axes (toolchain.cpp) into sub-steps

Split at phase1's own banner boundaries: the closures phase1 assigns onto
state, the target/--static override resolution, and the device axis plus
the L1 conditional-section merge.

phase2_define_toolchain_resolver is not split: it is a single stored
closure, state.resolve_target_toolchain, whose ~1000-line body is one
sequential toolchain-resolution flow with dozens of interdependent
locals -- the same category of residual as state.loadVersionDep in
graph_load.cpp, noted in the T6 report. step1_target_and_static_overrides
(524 lines) is a smaller residual of the same kind.

* T6 (mcpp#722, W7 follow-on): split phase0_manifest_and_workspace and phase3_xlings_before_graph

Both were found over ~400 lines by the same sweep that produced the
seven functions #722 named (the issue's own "any other function under
src/build/prepare/ over ~400 lines"), and split cleanly along their own
banners: phase0's own "Workspace handling" section becomes
step0_workspace_handling (108 lines; phase0 itself drops to 372); the
host-toolchain closures phase3 assigns onto state, plus the index-refresh
section, become step3_define_host_tc_closures_and_refresh_index (298
lines; phase3 itself drops to 235).

* T6 (mcpp#722): add the function-size gate, verified but not wired into CI yet

.github/tools/check_function_sizes.sh runs clang-tidy's
readability-function-size (LineThreshold=400) over the compile database
mcpp produces for its own LLVM build (mcpp build --toolchain
[email protected]), restricted to files under src/build/prepare/. clang-tidy
is not part of the plain xim:llvm payload; it ships in the sibling
xim:llvm-tools package at the same version, which the script locates
under the xlings store.

Measured: at 7ccbc8d (before this round's split) it reports 10
functions over 400 lines -- the seven #722 named, plus
phase0_manifest_and_workspace, phase11_scan and
phase3_xlings_before_graph, found by the same sweep. After this round's
split it reports 6: step6_provision_host_tools (421),
phase4a_graph_load (518, its loadVersionDep closure), phase11_scan (773,
untouched), step9_kernel_abi_interfaces_and_requirements (401),
step1_target_and_static_overrides (522) and
phase2_define_toolchain_resolver (1006, untouched) -- see the T6 report
for why each remains.

Not wired into CI next to check_file_lengths.sh: it does not yet pass,
so adding the workflow step now would land a gate red on day one. Wire
it once the remaining residuals are split in a follow-up; the file
gate stays the only enforced one for this round, per the design's own
fallback for a working tool over an incomplete split.

* prepare: mcpp.lock and resolution.json move to records.cpp (plan.cpp under the file-length limit after the merge; byte-identical on the seven fixtures)

* T6 (mcpp#722, W7 residual 1/6): split step6_provision_host_tools (features.cpp)

The per-tool body of the outer loop (~421 lines) becomes HostToolCtx, a
phase-local struct following the WorklistItemCtx / TargetSideGather
pattern, and six sub-steps: target resolution, the override/self-request
checks, the store-key/cache-hit resolution, the sub-build, and publish.
The `record` closure the single-function version captured per tool is a
named helper called from all four sites that used it (an override, a
cache hit, a deferred plan-only tool, and a freshly built one).

Verified: byte-identical on the seven fixtures; check_function_sizes.sh
reports no finding in this file.

* T6 (mcpp#722, W7 residual 2/6): split the loadVersionDep closure (graph_load.cpp)

phase4a_graph_load's stored closure state.loadVersionDep (~518 lines) is
split the way phase2's single stored closure is: a local struct
(LoadVersionDepCtx) holds the parameters and the locals more than one
section reads, and the body becomes three named steps (locate an
already-installed copy, fetch one if none is installed, then read its
manifest) the closure calls in sequence. The stored closure stays the
entry point with the same signature, so its own recursive call for a
preinstall hook's dependencies is unchanged.

Two of its local lambdas (readLuaContent, findRawInstalled) are read from
two of the three steps (the initial read and a later re-check, or the
initial probe and the post-install one) and become named helpers for the
same reason `record` did in the previous commit; a third (markInstalled)
is promoted for the same reason though it has only one call site, since
the closure it replaced no longer has anywhere to live once the body it
was local to is split.

Verified: byte-identical on the seven fixtures; check_function_sizes.sh
reports no finding in this file.

* T6 (mcpp#722, W7 residual 3/6): split phase11_scan (scan.cpp)

Split at its own section boundaries into eight named steps (source scan
and validation, the dependency-standard scope check, the dialect-flag
gate, the MSVC CRT-word check, the package std-module source, the Apple
SDK C++ runtime rule, the std-module availability gate, the fingerprint
computation, and the std-module prebuild). needsStdModule is the one
value several steps read; it is computed by the first step and passed as
a plain bool parameter rather than through a struct, since it is the
scan's only shared local.

Verified: byte-identical on the seven fixtures; check_function_sizes.sh
reports no finding in this file.

* T6 (mcpp#722, W7 residuals 4-5/6): split step1_target_and_static_overrides
and phase2_define_toolchain_resolver (toolchain.cpp)

step1 (~522 lines): a phase-local struct (TargetOverrideCtx) holding the
parsed triple, its vocabulary-table row and the section lookup carries
seven sub-steps through one `--target` request's resolution (resolve the
request, validate its tier, the Apple SDK check, the wasm shared-lib
check, the host_can_serve diagnosis, capturing the display name and
canonicalising, and the row-pin/capability check). The `apply_target_section`
closure is a named helper, called from the resolved-request branch and
from the host-row branch that follows it.

phase2 (~1006 lines, a single stored closure, state.resolve_target_toolchain):
a phase-local struct (ToolchainResolveCtx) holds only the parsed spec and
the Windows installed-toolset probe -- the two locals the branch-selection
half of the closure shares. Every other local (the first-run defaults, the
explicit-spec resolution's own payload/frontend locals, and so on) stays
local to the one step that declares it, as it was in the single function.
The closure becomes twelve named steps: parsing the spec, each arm of the
compiler-resolution if/else-if chain, the Windows-first-run persist, toolchain
detection, the retargetable-driver fixup, the MSVC toolset bind, the Windows
runtime identity, the MSVC-ABI-without-MSVC repair, and the musl default
linkage. The stored closure stays the entry point with the same signature,
so its own recursive call (the target pass) is unchanged.

Verified: byte-identical on the seven fixtures; check_function_sizes.sh
reports no finding in this file.

* T6 (mcpp#722, W7 residual 6/6): split step9_kernel_abi_interfaces_and_requirements
(target_side.cpp); the gate's own false failure at zero findings

step9 (~401 lines) splits into five named steps at its own section
boundaries: the kernel-abi interface enumeration, the layering and
requirement checks (over TargetSideGather's `requirements`), the
pin-and-linkage diagnostics, the same-OS check and report, and the
platform-sdk closure visibility check. No phase-local struct is needed:
each step reads and writes only `state` (plus `gather` for the one step
that needs its `requirements`).

This was the last of the six residuals check_function_sizes.sh reported at
the base of this round; with it split, the gate ran clean over the whole
of src/build/prepare/ for the first time -- and immediately failed for an
unrelated reason: `--warnings-as-errors='*'` makes clang-tidy exit non-zero
on the two pre-existing readability-function-size findings in the bundled
third-party modules/libs/src/json/json.hpp, which the script's own
`relevant` filter already excludes from the pass/fail decision but which
its exit-code fallback then reads as "a diagnostic tool problem" -- a
false failure this repository's own tree could not previously reach, since
no earlier revision had zero findings under src/build/prepare itself.
Dropping the flag leaves clang-tidy's exit code meaningful only for an
actual tool failure (a bad compile command, a crash), which a plain
warning never produces; `relevant` remains the sole pass/fail signal, as
the surrounding comments already say it is.

Verified: check_function_sizes.sh reports "ok" over the whole of
src/build/prepare/; byte-identical on the seven fixtures.

* ci: the function-size gate runs after the LLVM self-build; clang-tidy is resolved at the version that wrote the compile database

* docs: #722 completed, the function-size gate in CI (changelog, 13.6)

* review: the function-size gate is run by hand until CI builds mcpp with clang (#729); a generated deploy source is compared after the link only

- ci-linux.yml: the gate step comes out again. The LLVM toolchain job's
  [email protected] self-build has never completed (libc++ 20's std module hides
  directory_iterator's comparison) and the step reads the resolution line,
  not the build's exit status; clang-tidy crashed over the partial database.
  Recorded as #729.
- check_function_sizes.sh: a finding is a diagnostic line ending in the
  bracketed check name; a crash dump no longer reads as a finding.
- prepare/plan.cpp: a declared deploy source that an action writes is left to
  the post-link comparison; at planning it may hold the previous build's bytes.

---------

Co-authored-by: speak-agent <[email protected]>
Co-authored-by: speak-agent <agent@mcpp-community>
Co-authored-by: Claude <[email protected]>

v2026.9.27.1

Toggle v2026.9.27.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.27.1: #704-#716 fixed, feature tools, artifacts edges, payload…

… revisions and glibc locale data, prepare.cppm decomposed (#719)

Fixes #704, #705, #707, #708, #709, #710, #711, #712, #713, #714, #715 and #716 are verified after release in an xlings subos sandbox before the issues are closed. See CHANGELOG.md for 2026.9.27.1.

v2026.9.26.2

Toggle v2026.9.26.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.26.2: one compile database per configuration, emit plans every…

… member, prepare actions and runtime search directories, DLLs beside a Windows program, link flags as words (#702)

Closes #699, #701, #703. Design record: .agents/docs/2026-09-26-compile-database-and-issue-699-design.md.

v2026.9.26.1

Toggle v2026.9.26.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.26.1: paths in UTF-8, a C standard per package, a graph link w…

…ithout the host, and the level that was declared (#698)

* 2026.9.26.1: paths in UTF-8, a C standard per package, a graph link without the host, and the level that was declared

#693. mcpp held paths in the Windows ANSI code page, and the JSON it writes
holds UTF-8 only, so every build under a non-ASCII project directory or home
failed with an internal JSON exception, including names the code page can
spell; a Latin-1 directory name fails the same way on Linux. mcpp.exe now
declares the UTF-8 code page (res/mcpp.rc), build programs are linked with the
same manifest, host tools default to it, and the new target key
`windows_code_page` states it for a project's own executables. A path with no
UTF-8 spelling is refused (project directory, MCPP_HOME), skipped and reported
(a name inside a project), or refused by key (a build.mcpp directive), never an
internal exception. The response files of the msvc dialect begin with a byte
order mark, which cl.exe, link.exe and lib.exe need to read UTF-8 (measured),
and Ninja's own encoding is checked when build.ninja is not ASCII.

#695. `[build] c_standard` reached every C unit of the graph through the
file-level $cflags, and a dependency's own value was not applied. Each package's
C units now compile at that package's standard; an undeclared package at c11.

#696. A link over a graph-supplied C library searched the host's library
directories, so `-lm` linked glibc objects into a musl image. Such a link on ELF
now carries --sysroot naming an empty directory, the hermetic check holds every
-L to the store, the build directory and the graph's packages, and an
unanswered -l fails with a note naming openkal-musl 0.19.2.

#694. The musl -Og workaround is removed; the compile and the Finished line
read one realised optimization level.

Plan and measurements: .agents/docs/2026-09-25-issues-693-696-triage-and-repair-plan.md

* xlings 2026.9.26.2, the key rule a test still stated, and the graph link measured on four legs

- kXlingsVersion and every xlings pin in .github move to 2026.9.26.2
  (openxlings/xlings#613): xlings.exe declares the UTF-8 code page and its
  main has an exception boundary, so a working directory or an MCPP_HOME
  outside the ANSI code page no longer ends it with 0xC0000409 and no output.
- test_cache_key stated the old rule, that a package's `c_standard = "c11"`
  is keyed; a package that spells the default and one that says nothing
  compile identically and now share a key. CI run 1 failed that test on every
  platform, and nothing else in the unit suites.
- e2e 778 pins each leg to the openkal-musl and openkal-llvm-runtime pair that
  belongs together (the runtime pins openkal-musl exactly, and a root that
  pins another is refused as irreconcilable), and adds the report's aarch64
  case under qemu-aarch64 and a host `-L` refused by the hermetic check. The
  openkal job asserts each leg ran.
- docs: a macOS file name is UTF-8; `allow_host_libs` lifts the graph-link
  refusal rather than turning it into a warning.
- The plan's implementation record carries CI run 1: every Windows row of the
  regression job passes, and the two xcode-27 legs fail as on main (#669).

* SPEC-002 rule three: a link over a graph-supplied C library searches no host directory (v1.1)

---------

Co-authored-by: speak-agent <[email protected]>

v2026.9.25.1

Toggle v2026.9.25.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.25.1: workspace members compile the same way in every position…

…, and a consumer's headers stay in the consumer (#690) (#691)

* docs: #690 design record and implementation plan

* fix(workspace): one inheritance pipeline for every member, keyed defines, one key table (#690)

A member reached as a dependency now inherits [workspace.package],
x.workspace = true entries and [workspace.build] at its load site, before
the conditional merge and the defines fold, as the root does. The snapshot
in makePackageRoot no longer inherits and refuses unfolded defines with an
internal error. A member of a git-hosted workspace inherits from its own
repository. [build] defines is a set keyed by macro name, and !NAME removes
an inherited name. The inheritable [build] subset is one exported table,
which accepts ios_deployment_target. The e2e harness links payloads per
version, so a version installed by a test no longer lands in the
developer's registry.

* fix(build): a consumer's include directories stay in the consumer (#690 W6)

The root's [build] include_dirs and include_dirs_after were written into the
file-level $cxxflags/$cflags/$asmflags/$nasmflags of build.ninja, which every
unit in the graph reads. A root header named like a system header shadowed it
inside a dependency, private_include_dirs included, and because no cache-key
axis contained the broadcast, a dependency object in the global cache could be
compiled against another project's root headers (measured: cJSON_Compare
answered 1 in an unrelated project).

- flags.cppm: no include directory is broadcast; every unit keeps its own
  package's directories through $local_includes (C, C++, GAS and NASM).
- cache_key.cppm: kCacheEpoch 3 -> 4, orphaning entries written while the
  broadcast existed.
- ninja_backend.cppm / execute.cppm: consumer_include_scope_advice names the
  consumer directory that holds a header a dependency now fails to find, on
  the full path and, through a sidecar beside build.ninja, on the fast path.
- tests: unit tests for the file-level channel, the unit channel's
  absolutisation, the advice and the sidecar; e2e 765.

* docs: workspace inheritance in every position, keyed defines, include scope; SPEC-004 1.6 (#690)

* docs: noun-phrase heading for the defines and include scope section

* docs: #690 sandbox verification script

* publish: the effective manifest, a normalised published form, and a reproducible archive (#690 W4, W5)

* test: renumber the published-form e2e scripts to 772 and 773

* fix: every reader of a member manifest reads the effective manifest; 2026.9.25.1 (#690 W4)

prepare_build loads through load_effective_manifest and no longer inherits a
preloaded host-tool manifest a second time. sbom, index list/update, the index
refresh of mcpp update, the fast-path identity and test discovery read the
effective manifest.

* chore: xlings pin 2026.9.16.1 -> 2026.9.20.1

* fix: a member inside an index archive inherits its workspace; publish reads the one key table (#690)

inherit_as_workspace_member is the one function for a sibling path
dependency, a git-hosted member and a member inside an index package's
archive (Form A pointer), searched no higher than the install root.
normalize.cppm writes [workspace.build] back through kWorkspaceBuildKeys
instead of a second copy of the key set. e2e 774.

* docs: publishing a workspace member, the index-archive member, CHANGELOG 2026.9.25.1, SPEC-004 criteria 13-14

* publish: the archive commit ignores commit.gpgSign and the manifest blob ignores .gitattributes

* fix: a host-tool sub-build merges its dependency's conditional sections once (#690 F12); self-review record

* test: host-spelled fixture path in 772, build-directory words excluded from 765 (c), cmd.exe quoting in 775

---------

Co-authored-by: speak-agent <[email protected]>
Co-authored-by: speak-agent <[email protected]>

v2026.9.24.1

Toggle v2026.9.24.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.24.1 — the MSVC toolset is the sysroot, the deployment target …

…follows the target, and frozen gcc headers are named (#688)

Closes #685. Closes #687. SPEC-006 (toolchain management, draft v0.2). See the PR description for the measurements.

v2026.9.21.3

Toggle v2026.9.21.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.21.3 — a token that did nothing, a count that was wrong, and a…

… build that measured the dependencies (#682)

* the count was four and it is two, and the grouping used the wrong key

THE RE-MEASUREMENT AFTER THE WITHDRAWAL REFUTED A NUMBER THIS REPOSITORY HAD
WRITTEN IN FIVE PLACES.

`cenv.cppm`, `predefines.cppm`, `docs/21`, `docs/22` and the CHANGELOG all
said the borrowed `__CYGWIN__` cost four members: archive, sqlite3, mimalloc,
c-ares. The 30-member run on 2026.9.21.2 says otherwise.

    total failures   10 -> 5
    newly failing    none
    the windows.h group   4 -> 2, not 4 -> 0

And the half that did not hold is the useful half, because it says the
DENOMINATOR was wrong:

    archive (xz)  __CYGWIN__   correct    cleared
    sqlite3       __CYGWIN__   correct    cleared
    c-ares        __CYGWIN__   WRONG      still fails
    mimalloc      __CYGWIN__   WRONG      still fails

c-ares reaches `windows.h` through `#ifdef HAVE_WINDOWS_H`, and that macro is
defined by mcpp-index's own recipe in its Windows branch --- a recipe defect,
the same shape as curl's `HAVE_LINUX_TCP_H`. mimalloc no longer reaches a
header at all.

ALL FOUR WERE GROUPED BY THEIR DIAGNOSTIC. Every one of them stopped at
`windows.h`, so they were recorded as one cause. GROUPING BY DIAGNOSTIC IS NOT
GROUPING BY CAUSE, and a count collected that way overstates what withdrawing
a name can fix.

This is the mirror of a lesson already in this repository: a conclusion gets
re-checked, its REASONS do not. The conclusion --- withdraw the borrowed name
--- was right and was re-verified. The number inside the reason was inherited
from an earlier record three times over, into engine comments, a changelog and
a pull request body, and nothing checked it until the measurement did.

AND mimalloc IS A FINDING OF ITS OWN:

    fatal error: error in backend:
      Target OS doesn't support __builtin_thread_pointer() yet.

`presents = "posix"` on Windows realises as `--target=x86_64-pc-cygwin`, and
LLVM does not implement that builtin for that OS. Every previous note about
the substitution discussed what the PREPROCESSOR sees --- the ABI unchanged,
the link unchanged, only macro visibility different. This is the first
measured cost that the CODE GENERATOR sees, and it is recorded rather than
fixed: the repair belongs either in mimalloc's recipe or in the choice of
substitute triple, and that is a measurement not yet taken.

A tooling defect surfaced with it. The measurement recorded mimalloc as
`fails: error: build failed`, because `compat.py`'s `first_diagnostic` matched
none of its three patterns --- a backend error carries no `file:line:` and no
`FAIL` --- and its fallback returns the LAST line, which is mcpp's own
summary. A fallback that returns something diagnostic-shaped when the match
failed is worse than returning nothing.

* plan: the two measurements do not overlap in TARGETS either, and that was unwritten

The division of labour said one measures the C surface and the other measures
integration. It did not say they ask about different targets:

  30-member measurement   x86_64-linux-gnu, x86_64-windows-gnu --- no macOS
  lsp-mcpp-private        three targets, including aarch64-macos

Measured this round: `archive` is `runs (posix)` on both columns of the
30-member measurement, and the same libarchive fails to link on
`lsp-mcpp-private`'s macOS with `memset_pattern16`. Both readings are true.
That symbol is an Apple libc function clang emits only for Apple targets; on
linux and windows-gnu it cannot appear.

So nothing in this ecosystem sweeps macOS at 30-member scale, and E1's
criterion --- 10 failures to 5, none newly failing --- holds ON THOSE TWO
COLUMNS. A defect of this class is invisible in that number by construction.

Adding the column needs a decision first: a Linux host can cross-build
aarch64-macos but cannot run it, so that column tops out at `builds`, which
RANK places below `runs`. A column that can only reach `builds` makes the
regression check permanently looser there than on the other two.

* C4 was closed on the wrong objects, and the possibility it dismissed was the right one

The closure measured zstd and xz --- the two the OLD report named --- and found
them clean. The member that fails is libarchive, and it was never measured.
`lsp-mcpp-private`'s `test_archive` does not link for aarch64-macos:

    ld64.lld: error: undefined symbol: memset_pattern16

referenced by `archive_read_support_format_7zip.o`. zstd and xz happen not to
trigger the idiom, so measuring them answered a different question.

THE RECORD LISTED THREE POSSIBILITIES AND PICKED THE FIRST. The third ---
"`-fno-builtin-memset_pattern16` is not enough to turn off LLVM's loop-idiom
pass" --- is the one that holds. A/B on the real compile command from
build.ninja, varying only that flag:

    as built (flag present)              1 reference   38200 bytes
    flag REMOVED                         1 reference
    -fno-builtin                         0             38888 (+1.8%)
    -ffreestanding                       0
    -mllvm -disable-loop-idiom-memset    0             38152

The flag changes nothing. And it cannot report that it changed nothing: clang
accepts `-fno-builtin-totally_not_a_function` silently, because
`memset_pattern16` is an LLVM TargetLibraryInfo libfunc rather than a clang
builtin. The preprocessed source contains the symbol zero times, which
confirms the call is generated rather than written.

TWO ENGINE-SIDE GAPS, AND THE SECOND IS WHY THE FIRST SURVIVED:

  1. the token `builtins = "iso"` emits is ineffective for the one case its
     own comment names;
  2. that token is not verified. In the same file, `-D` and `-U` are checked
     by the probe against `expectDefined`/`expectUndefined`, under a comment
     saying a `-D` that did not take effect is a verification failure rather
     than a silent one. `builtinsTokens` has no such list.

The criterion that follows: `builtinsTokens` needs the same probe check. A
mechanism that holds only under the assumption that it works needs an
assertion that it works.

The cost of each candidate repair is measured above rather than argued.

* the symbol belongs to no layer that carries it, which makes this an architecture choice

`memset_pattern16` is not a function programs call; it is a helper the code
generator emits, in the same family as `memcpy` and `__udivti3`. That family
belongs to the compiler runtime. Measured:

    grep -rln memset_pattern16  openkal-llvm-runtime/llvm/   ->  0 hits

LLVM carries no implementation anywhere, so compiler-rt has no fallback: the
call upstream emits is one only Apple's libSystem supplies.

`builtins = "iso"` therefore meets a case it can DECLARE and cannot ENFORCE.
The flag that would enforce it does not work, and LLVM offers no second one.
Four candidates, costs measured:

    -fno-builtin                       works, +1.8% here, disables ISO
                                       functions' optimisation too
    -mllvm -disable-loop-idiom-memset  works, -0.1%, not a stable interface
    supply it in openkal-musl's Apple   ~6 lines; port/src/mach/ exists;
      port                             contrary to the declaration's wording
    declare the member unbuildable     discards a combination that works

The third asks the better question. The declaration is about what the C
library PRESENTS, and a compiler-emitted helper is not an interface the
program requested --- it is closer to ABI. A C library presenting only ISO C
may still owe the code generator the helpers it assumes for that target, the
same way it owes `memcpy`.

This is a decision, not an implementation: it settles whether
`builtins = "iso"` means "turn off what the generator assumes" or "declare
the surface the program can see". Those diverge in other cases too.

* the token was accepted in silence and changed nothing

`[c-abi] builtins = "iso"` emitted `-fno-builtin-memset_pattern16` from this
mechanism's first revision. A/B on a real compile command, varying only that
flag, reads

    as built (flag present)              1 reference to memset_pattern16
    flag REMOVED                         1 reference
    -fno-builtin                         0
    -mllvm -disable-loop-idiom-memset    0

and clang accepts `-fno-builtin-totally_not_a_function` just as quietly: the
`-fno-builtin-<fn>` family is matched against clang's builtin table, while
`memset_pattern16` is an LLVM TargetLibraryInfo libfunc. The call is emitted by
LoopIdiomRecognize, which consults TLI, and the per-function attribute does not
reach it.

`-mllvm` is not chosen because it passes an internal LLVM option, which can be
renamed or removed between releases; when it is, the mechanism returns to
failing silently, which is the defect being repaired. The cost of the blunt
flag is measured rather than argued: on the translation unit that surfaced this
the object grows 38200 to 38888 bytes, 1.8 per cent.

THE NO-OP SURVIVED BECAUSE IT HAD NO CRITERION. `cenv` verifies its tokens
against a `-dM` dump, and a code-generation property is not visible there. The
criterion now lives in `openkal-cross.yml` and has three legs, on all three
hosts: no flag (the symbol MUST appear, or the probe measures nothing), the
per-function flag (it must still appear, pinning the defect), and an
`aarch64-macos` build over the openkal stack by the mcpp under test (zero
references). Run against the previous binary the step fails, at the link:

    ld64.lld: error: undefined symbol: memset_pattern16

* a target that cannot be run could only be measured by not building it

`mcpp test` for a target this host can neither execute nor reach through a
runner leaves every test `not run` and exits 2. That is the correct answer to
"do these tests pass" — mcpp did not find out — but 2 is also what a broken
runner returns, so a caller that wanted only the build cannot tell the two
apart and falls back to `mcpp build`.

AND `mcpp build` BUILDS THE PACKAGE. For a package whose only sources are
under `tests/` it compiles nothing of it at all. Measured on mcpp-index's
`archive` member, whose sources are two files under `tests/`:

    $ mcpp build --target aarch64-macos      # exits 0
       Compiling compat.lz4 / compat.xz / compat.zlib / compat.zstd …
    $ find target -name '*compression*' -o -name '*versions*'
       (only musl's versionsort.o)

The member's dependencies compiled; not one line of the member did. A
compatibility sweep reading that exit code records the member as building on
macOS, which is a reading about the dependencies with the member's name on it.

`--no-run` gives the narrower claim its own answer: every selected test is
compiled and linked for the target, none is executed, and the result says so.
`built` is counted apart from `not_run`, in the human summary and in the
machine interface, because `not_run` means mcpp tried and could not — the
question is open, the exit code is 2 — while `built` means it was told not to,
so the build was the whole question and the exit code is 0.

The criterion is `745_no_run_builds_the_tests_and_says_so.sh`, four legs. Its
runner is a name that is not a program: an unexecutable target would make the
test need a cross toolchain and a host that cannot run it, while a runner that
cannot be found produces the same situation on every host, for the native
target, with nothing installed. Run against a binary without the flag it fails
at leg B.

* the reader was a byte match and the hosts do not agree about binaries

Leg 1 of the builtins criterion failed on the macOS host with all three
readings 0, which is what leg 1 exists to report: the two legs behind it were
measuring nothing.

The cause is the reader, not the compiler. `grep -ac memset_pattern16` reads
1/0 correctly with GNU grep, and the macOS runner's grep is BSD, where what
`-a` promises about a binary file differs. The name being present in an
object's string table made a byte match look like it needed no tool; it needed
agreement about binary input instead.

`llvm-nm -u` is in the payload beside the clang already being used, costs the
same lookup, and asks the question the step is actually asking. Measured
unchanged on Linux: 1 / 1 / 0, engine 0, and the control against the previous
binary still fails at the link.

* the acceptance record gains the two criteria this round's closing found

C5 is the builtins token, whose first leg went red on macOS and reported that
the two behind it were measuring nothing. C6 is what `builds` meant on a
target with no runner: the member's dependencies, not the member.

* a count that stops at the member level leaves the same reading one level up

`--workspace --no-run` reported "ok. N member(s); 0 passed; 0 failed", which is
what a workspace with no tests at all reports — the false reading
`totalNotRun` was added to the same line to prevent, one level down.

The fan-out now carries `built` through to the workspace total and to
`workspace_summary` as `tests_built`, kept apart from `tests_not_run` for the
reason the per-member fields are: one is a question left open, the other is a
question that was not asked.

Leg E of 745 covers it: two members, one test each, `--workspace --no-run`,
exit 0 and "2 built, not run" in the total.

* 2026.9.21.3

Both version sites in one commit, which `01_help_and_version.sh` cross-checks.
The bootstrap pin in .xlings.json stays at 2026.9.21.1: it must name a version
that is published, mirrored and in the index, and this one is none of those
yet.

* the Apple list has one definition and three spellings

`os == "macos" || os == "ios"` is `Triple::is_apple()`. This module takes
`os` as a string rather than a `Triple` deliberately — it is pure, and
importing the toolchain model to reach one predicate would couple what that
choice decoupled — so the spelling stays and the comment names where the
canonical list lives and which other sites carry a copy.

* the sandbox verification gains the two sections this release adds

G reads an object file, because a code-generation property is in no `-dM`
dump — which is why the token it asserts was a silent no-op for the whole of
its first life. On 2026.9.21.2 the aarch64-macos link fails outright, so a
failed build there is the negative reading rather than an absent one.

H asserts `--no-run`; before this release the flag does not exist.

Dry-run on the host against both engines: both sections pass on this one and
fail on the previous, which is the property a CHANGE section has to have.

* the next batch's first item, with a number attached

aarch64-macos was measurable for the first time once a target with no runner
compiled the member's own tests: 20 of 30 build, 10 do not, and nine of the ten
are `#ifdef __APPLE__` reaching for the Apple C environment on a target where
`__APPLE__` is true and libSystem is not there.

It is the Windows problem mirrored, minus the lever: presenting POSIX on
Windows is realised as a cygwin triple and `_WIN32` goes away, while on macOS
the realisation adds `-D__unix__` and leaves `__APPLE__` standing because it
is correct. Nothing in the identity a source file sees there answers which C
library is underneath.

---------

Co-authored-by: speak-agent <[email protected]>

v2026.9.21.2

Toggle v2026.9.21.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.21.2: the macros mcpp owns are spelt in upper case, and the bo…

…rrowed name is withdrawn (#681)

* 2026.9.21.2: the macros mcpp owns are spelt in upper case, and the borrowed name is withdrawn

`__mcpp_target_<os>__` becomes `__MCPP_TARGET_<OS>__` and `__openkal__`
becomes `__OPENKAL__`. The spelling is still the triple's own `os` field, now
upper-cased; the engine still learns no operating-system name.

THE CONVENTION SPLITS BY WHAT A NAME IS, NOT BY WHO WRITES IT. A vendor or
product name is upper --- `__APPLE__`, `_WIN32`, `__MINGW32__`, `__GNUC__`. A
kind-of-system name is lower --- `__linux__`, `__unix__`, `__gnu_linux__`.
Every row this engine OWNS is of the first kind: it names mcpp, or it names
openkal. The kind-of-system question is answered by `__linux__` and its
family, which mcpp SUPPLIES rather than owns and which therefore keep their
lower-case spelling, for that exact reason.

The lower-case spelling shipped in 2026.9.21.1 on the reverse reading: that
these names sit beside `__linux__` in real guards, so matching it was
consistency. THAT CONFUSES ADJACENCY WITH KIND. `__APPLE__` sits in those
same guards and is upper, because it belongs to somebody.

`__MCPP_` is a prefix, not the whole rule: `__OPENKAL__` is owned and names
*openkal* rather than mcpp. The assertion in `test_predefines.cpp` therefore
checks the SPELLING CONVENTION --- upper case, `__`-wrapped --- instead of
the prefix, which would have judged `__OPENKAL__` a violation when it is not.

WITHDRAWING A MACRO IS DECIDED BY A COUNT, NOT BY READING

An entry in this contract is a published interface, and withdrawing one is
SILENT: a `#if` selects the other branch and compiles. No mechanism available
to a build tool makes that loud. So a withdrawal is decided by a measurement
that ENUMERATES READERS, and what it finds sets how many steps the withdrawal
takes. The two this project has performed came out differently:

    withdrawn                              readers found            steps
    __CYGWIN__                             4 third-party members,   3
                                           + 6 sites in the two
                                           headers we INSTALL
    __mcpp_target_<os>__, __openkal__      none                     1

The second row's denominator is every repository of this ecosystem, swept by
file type: no source file and no manifest reads either name, and the only
occurrences are this engine's emitter, its tests, and prose. Both names were
invented here, so no upstream code can hold one; exposure was three days for
`__openkal__` and a single release for the target macro. The rule did not
change between the two rows. The count did.

A new assertion takes its denominator from the target registry rather than
from a list written beside the test: every canonical triple is parsed and its
emitted macro checked to be a valid identifier. The spelling comes from the
`os` field, so an `os` carrying a dot or a version suffix would produce a
macro no compiler accepts, and the failure would land in a user's build.

__CYGWIN__ IS WITHDRAWN --- THE LAST STEP OF A THREE-REPOSITORY SEQUENCE

The Windows `presents = "posix"` realisation now adds `-U__CYGWIN__` and
`-U__CYGWIN32__` to the compile line. `--target=x86_64-pc-cygwin` stays: it
is what supplies `__unix__` and suppresses `_WIN32`, which is what presenting
POSIX means. Only the borrowed NAME was unwanted.

  1. 2026.9.21.1 defined mcpp's own name beside the borrowed one; this
     release re-spells it, while it still has no consumer. Purely additive.
  2. `[email protected]` and `[email protected]` read the new
     name and keep `|| defined(__CYGWIN__)`, so they build on an engine from
     either side of this change. PUBLISHED BEFORE THIS RELEASE.
  3. this release stops defining the borrowed name.

THE ORDERING IS MEASURED, NOT ASSERTED. It holds between repositories, so no
test in this one can check it. Building an openkal program for
`x86_64-windows-gnu` against the PUBLISHED `[email protected]` on
this engine fails on libunwind's two `static_assert`s --- `x86_64 registers
do not fit into unw_context_t`, `UnwindCursor<> does not fit in
unw_cursor_t`. That is step three taken first, and it is why step two ships
first. `setjmp.h` is the silent half: its own comment says a mismatch is
reported by nothing until the record overruns.

THE RESIDUAL WINDOW IS NAMED RATHER THAN CLAIMED AWAY. A project pinning
`openkal-musl` at 0.18.0 or earlier EXACTLY, and upgrading past this release,
gets that silent `#else`. Moving the index's `latest` onto 0.19.0 first keeps
the window to exact pins; nothing available here closes it, because the
engine cannot know which macros a package's installed headers read.

Bootstrap pin advances from 2026.9.20.1 to 2026.9.21.1.

* an unanswered requirement says so, and the openkal branch this engine is verified against is overridable

E3: A REQUIREMENT NOBODY ANSWERED READ EXACTLY LIKE A CONFIRMED ONE.

Three situations exist and two of them build: the resolved implementation
states a `provides-interfaces` list containing the requirement (build), states
a list without it (refused), or states nothing at all (build). The third is
deliberate --- the key postdates the implementations, and a graph that has not
adopted it must keep building --- but until now it produced output identical
to the first, so a consumer inspecting a green build could not tell "checked
and agreed" from "never asked".

    note kernel-abi interfaces: [email protected] states none, 2 requirements unchecked

The note names the implementation taken from the RESOLVED layer: a reader
told only that something went unchecked cannot act on it.

`tests/e2e/743` gains two legs rather than one. Leg C asserts the note is
present, leg D that it is absent when the provider does state its list.
WITHOUT LEG D, LEG C PASSES AGAINST AN ENGINE THAT PRINTS THE LINE
UNCONDITIONALLY, which measures nothing. Leg C also asserts the COUNT, the
one part of the message the fixture determines: a note reporting "1" or "0"
would satisfy every assertion that matches only an identifier.

THE OPENKAL BRANCH IS OVERRIDABLE FOR ONE RUN

This repository already has one half of a cross-verification protocol: the
ecosystem repositories build against an mcpp PR branch through
`MCPP_SOURCE_REF`, so an engine change is measured against them before it
merges. The reverse was hard-coded to `main`, which makes a change that
REQUIRES a coordinated ecosystem commit unverifiable until after that commit
lands --- and unmergeable until then, since `openkal-cross` is the job that
fails.

`__CYGWIN__`'s withdrawal is the case that showed it. `openkal-cross` builds
`openkal-llvm-runtime@main`, whose installed header read only the borrowed
name, so this engine's own CI reproduced the ordering constraint as a red
cell. The cell is CORRECT --- the packages must publish first --- but
verifying the engine before that publish needs the input. Left empty, nothing
changes.

E2 (the link-time set difference) is recorded in the plan as withdrawn from
this release, with its blocker corrected: it is not the workload the plan
claimed but a design decision. `SURFACE.txt` is not installed by any package,
and docs/22 states that the engine knows no member of either set --- so where
an interface-to-symbol map lives, and whether the engine can read one without
learning this ecosystem's vocabulary, has to be answered before the feature
has a shape.

* plan: C2 is two different things, and only one of them is C2

Reading the measured diagnostics and the recipes apart:

  curl        lib/setopt.c:31: 'linux/tcp.h' file not found
  cmp-module  asio/detail/config.hpp:899: 'linux/version.h' file not found

CURL IS A RECIPE DEFECT, the same shape as C3/expat. `compat.curl.lua`
generates `#define HAVE_LINUX_TCP_H 1` inside `#if defined(__linux__)`.
openkal runs on the Linux kernel, so `__linux__` is CORRECT; what is wrong is
the recipe reading it as "glibc's whole Linux userspace is installed". The
same block asserts `HAVE_GLIBC_STRERROR_R` --- openkal-musl is musl, so that
one is actively false --- along with `HAVE_SYS_EVENTFD_H`, `HAVE_FSETXATTR`
and a hard-coded host path in `CURL_CA_BUNDLE`. The honest test is
`__has_include(<linux/tcp.h>)`: standard C, and it asks the question being
asked rather than inferring which headers exist from which kernel.

CMP-MODULE IS THE REAL C2. asio's `#include <linux/version.h>` sits outside
every `ASIO_DISABLE_*` guard, so no configuration macro prevents it, and no
manifest key reaches inside a third-party header --- which is justification
one in `predefines.cppm`.

`members.toml` already has an `[excluded]` table whose stated semantics are
exactly "cannot be built in any openkal graph". What is missing is a second
reason category, not a second table.

E2's blocker is corrected in the same pass: not the workload the plan claimed
but a design decision, since SURFACE.txt is installed by nothing and docs/22
states the engine knows no member of either set.

* plan: the cross-verification protocol has a structural hole, found while using it

Both ecosystem packages went green against the mcpp PR branch, AND NEITHER
GREEN TOUCHED `__MCPP_TARGET_WINDOWS__`:

  openkal-musl          linux/gcc, linux/llvm, macos/llvm; the cross-link row
                        is Linux<->macOS
  openkal-llvm-runtime  x86_64-linux-gnu and riscv64-none-elf

No package's CI in this ecosystem builds `x86_64-windows-gnu`, and the whole
subject of this change is a macro that exists only on Windows targets. The
greens are real and they measure something else --- a criterion that ran,
passed, and whose object was not present.

The only job that builds that target is mcpp's own `openkal-cross`, and it
had the ecosystem branch hard-coded to `main`, so an engine change requiring
a coordinated ecosystem commit was unverifiable until after that commit
landed. `openkal_ref` closes it; empty means today's behaviour.

Consequence for ordering: the example in that job reaches the runtime through
`path = "../.."` and the runtime's manifest pins `openkal-musl = "0.19.0"`
from the INDEX, so musl 0.19.0 has to be registered before the input can be
used.

* plan: correct the Windows-coverage finding --- the runtime does have that leg

Read job by job instead of from the first job of a run:

  openkal-llvm-runtime  `host-dimension` has a Windows-host row that builds
                        every target, including x86_64-windows-gnu, and it
                        resolved [email protected] on the PR engine. The
                        Windows leg of E1 WAS verified.
  openkal-musl          linux/gcc, linux/llvm, macos/llvm, a Linux<->macOS
                        cross-link, and a start-on-the-other-system job. No
                        Windows cell at all.

I concluded from the first job alone that no package in the ecosystem builds
Windows-over-openkal. That was wrong and the conclusion it cast doubt on
stands.

The real gap is smaller and still a gap: the package whose change this round
is `bits/setjmp.h` --- an installed header whose only branch is about
Windows --- has no Windows cell of its own. It is verified today only
transitively, because the runtime's CI pulls musl in as a path dependency and
compiles it along the way.

* the second layer of __cxa_thread_atexit is located, and the first hypothesis was right

Following the criterion the finding itself wrote down --- instrument
`run_dtors` --- with `&dtors` printed at both sites:

    registered dtor, dtors=0x7ffffe994680, &dtors=0x7ffffe9946a8
    run_dtors called, dtors=0,             &dtors=0x7ffffe9946c8

`run_dtors` IS called, which retires one branch. `&dtors` DIFFERS three times
in one thread, which gives the other: `__thread` is emutls here, emutls keeps
its per-thread blocks behind a pthread key of its own, that key's destructor
had already released this thread's block, and every read afterwards allocates
a fresh zeroed one.

HYPOTHESIS ONE IN SECTION 4 WAS CORRECT AND WAS RECORDED AS REFUTED. What was
wrong was the probe: musl runs key destructors in creation order, and that
probe created its own key BEFORE first touching a thread-local, so emutls
outlived it. The real ordering is the reverse. A probe cannot report an
ordering it was constructed to avoid --- the predicate was right and the
object's construction excluded the condition under test.

Fixed in openkal-llvm-runtime 0.15.0 by keeping the list in the TLS key's own
value, which no other key's teardown can reach. Two conditions asserted, in
examples/cxx, on both targets: the thread_local is constructed, and its
destructor runs.

* plan: C1 is closed --- the only true unknown in the list

Both layers located and fixed, shipping in openkal-llvm-runtime 0.15.0. The
second layer: `__thread` is emutls under `-femulated-tls`, emutls keeps its
per-thread blocks behind a pthread key of its own, and that key's destructor
releases this thread's block before libc++abi's runs. Criterion: `&dtors`
differs three times in one thread.

It was closed by the criterion the finding document itself wrote down. And
the hypothesis that document recorded as refuted was correct --- the probe
created its own key before first touching a thread-local, and musl runs key
destructors in creation order, so emutls outlived it. The predicate was
right; the object's construction excluded the condition under test.

* the finding's status word is one the checker knows

`status: resolved` is not in the vocabulary the structure check accepts
(active | landed | superseded | abandoned), and the agents index had not been
regenerated. Both caught by `check_docs_structure.sh`, which is the check
that exists so a front-matter word nobody reads does not drift.

* plan: why the missing Windows cell is not a one-line matrix row

Measured rather than assumed. A minimal criterion --- a setjmp/longjmp
program with `_Static_assert(sizeof(jmp_buf) >= 32 * sizeof(unsigned long
long))`, declaring `openkal-musl` alone, built for x86_64-windows-gnu:

  compiles   which is the layer that matters, since a short jmp_buf is
             silent and a compile-time assertion is the only thing that
             makes it loud
  does not link   cpow.o and others leave references unresolved; openkal-musl
             alone is not a complete link, because the openkal implementation
             and compiler-rt are assembled by openkal-llvm-runtime

So a Windows cell there is either compile-only (which does catch this defect,
and is cheap) or pulls the runtime in as a path dependency --- the mirror of
what the runtime's own CI already does. Either is new work.

* a sandbox verification for this wave, with its control reading

Six sections. B and C are CHANGE and must fail on the previous release; E and
F are GUARD and must pass on both. Measured, on the host, against the genuine
published archive of 2026.9.21.1:

    2026.9.21.1   fails=2   B: no upper-case target macro
                            C: an unanswered requirement produced no note
    2026.9.21.2   fails=0

C'\''s second leg passes on both by design: it asserts the note is ABSENT when
the provider does state its list. Without it, the first leg would pass against
an engine that printed the line unconditionally --- a negative control is not
a hole in a CHANGE section.

B carries both directions in ONE translation unit, because either alone passes
for the wrong reason: an engine defining NEITHER spelling satisfies "the
lower-case one is gone", and one defining BOTH satisfies "the upper-case one
is here". Its second leg asks a freestanding target for its own macro, which
is what says the spelling is DERIVED from the triple rather than enumerated.

D and F need openkal-llvm-runtime 0.14.0 from the index and reported NOT RUN
rather than passing when it was not yet registered.

* plan: the criteria table carries status, and two rows split

E1b and E1c did not exist when the table was written; C2 turned out to be two
items with two different owners; E2 left this round with its blocker
corrected. Each row now says what state it is in rather than only what would
count as passing, because a table of criteria with no readings is a list of
intentions.

Recorded readings: the verification script's control run (2026.9.21.1
fails=2, 2026.9.21.2 fails=0), the nine green cells of openkal-cross, and the
two directions the A3 check was measured in.

---------

Co-authored-by: speak-agent <[email protected]>

v2026.9.21.1

Toggle v2026.9.21.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.21.1: the borrowed __CYGWIN__ is withdrawn, by measurement (#680)

* 2026.9.21.1: the borrowed __CYGWIN__ is withdrawn, by measurement

`presents = "posix"` on Windows realises as a Cygwin-flavoured target. The
previous release LEFT `__CYGWIN__`/`__CYGWIN32__` defined, so that portable
third-party code needing to know the OBJECT FORMAT would keep a name for "PE
format with a POSIX-presenting C environment", and it wrote its own condition
for reversal: a trade-off for the 30-member measurement to settle, flipping if
defining them produced more failures than it fixed.

IT PRODUCED FOUR AND FIXED NONE. Across 60 member-target combinations,
archive, sqlite3, mimalloc and c-ares each stopped at `#include <windows.h>`,
reached through a guard of the shape `#if defined(_WIN32) || defined(__CYGWIN__)`.
Nothing in the same run failed for want of the macro.

Upstream says what it means by the name. mimalloc puts it in the guard's own
comment --- `we use windows locks on cygwin, but otherwise treat it at unix`
--- and sqlite3 lists it in the `SQLITE_OS_WIN` detection set before including
`windows.h`. A BORROWED NAME MEANS WHAT THE LENDER'S HISTORY MADE IT MEAN, not
what the borrower intended by it.

The object-format question keeps no macro at all: a package asks
`cfg(os = "windows")`, which needs none. If a third party is ever found that
can only ask in the preprocessor, mcpp defines a name of its own.

WHAT CARRIES THE CHANGE IS NOT THE TWO `-U` TOKENS. It is the two entries added
to `expectUndefined`: `cenv_probe::verify` compares the realised
configuration's predefines against those lists and refuses on a mismatch, so a
`-U` that failed to take effect is a verification failure rather than a silent
one. Measured directly with the pinned clang, in the order mcpp emits them:

    echo | clang -dM -E -x c - -U__CYGWIN__ -U__CYGWIN32__ \
             --target=x86_64-pc-cygwin -U__CYGWIN__ -U__CYGWIN32__
      -> __unix__ defined, __CYGWIN__ absent, _WIN32 absent

The unit test and e2e 741 now assert the opposite side, and both record that
this module has held both answers and what flipped it. The pair appears twice
on a `.S` command line and once on `.c`/`.cpp`, because `cEnvTokens` reaches
asmflags directly and `-D`/`-U`/`-I`-shaped tokens also arrive through the
channel that carries defines into assembly; `-U X` twice is `-U X`, and the
probe is the judge, so no count is asserted.

Also in this release, as documents rather than engine changes:

  * `.agents/docs/2026-09-21-openkal-ecosystem-completion-and-acceptance.md`
    --- the nine items this wave did not implement, each with its criterion,
    its owner and what blocks it; why `lsp-mcpp-private` is the acceptance
    vehicle (its platform surface is six constants, none of them a POSIX
    facility, and it makes no direct POSIX call); and a cross-repository
    verification protocol: an engine PR is built by the ecosystem through
    `MCPP_SOURCE_REF` and both sides must be green BEFORE it merges. That
    protocol exists because this very change needed a second release --- a
    reading the thirty member graphs could have produced before the first one.

* C4's premise does not hold on this stack, so nothing is implemented for it

The plan listed `memset_pattern16` as an openkal-musl gap and the hard blocker
for macOS acceptance, on the strength of a report that the link still fails on
the new stack. The engine already does the thing that should prevent it:
`cenv.cppm` emits `-fno-builtin-memset_pattern16` for `builtins = "iso"` on
macOS and iOS, and openkal-musl declares exactly that.

AND THE FLAG REACHES THE PACKAGES NAMED. Measured by emitting a build database
for `aarch64-macos` with 2026.9.21.1: all 29 of zstd's translation units carry
it, including the one on the chain the report named.

Three explanations survive, none of them decidable here --- a Linux host
cannot compile for macOS, there being no SDK: the report predates the flag;
the synthesised idiom is `memset_pattern4`/`8`, for which the engine emits no
flag (clang's `-fno-builtin-` family is one flag per idiom) though the report
names `16`; or the flag does not reach LLVM's loop-idiom pass.

So the next step is a reading, not an implementation. `lsp-mcpp-private`'s
`aarch64-macos --profile release` leg answers it directly, and each surviving
explanation has its own follow-up. Adding the symbol to openkal-musl now would
be repairing a gap that may not exist.

* cross-verification stopped the withdrawal, so mcpp names the target itself

The first shape of this change simply withdrew `__CYGWIN__`. It was green
here and green in four of five ecosystem repositories built against this
branch; openkal-llvm-runtime failed, libunwind's `static_assert` reporting
that `Registers_x86_64` does not fit `unw_context_t`.

TWO INSTALLED HEADERS IN THIS ECOSYSTEM READ THAT MACRO ON PURPOSE, each
saying so in its own source: `openkal-musl`'s `bits/setjmp.h` sizes `jmp_buf`
by it, and `openkal-llvm-runtime`'s `__libunwind_config.h` sizes
`unw_context_t`. Both are INSTALLED --- an application's own compile reads
them --- so neither can use the package-private define its sibling `.S` files
use, and `__CYGWIN__` was the only name mcpp kept defined target-wide.

The 30-member measurement that justified withdrawal had counted THIRD-PARTY
readers of the macro and not ours. Ours are load-bearing, and wrong is silent
where it matters most: `setjmp.h`'s own comment says "a mismatch nothing
reports until the record overruns". libunwind having a `static_assert` is
what made this loud, not anything the measurement did.

So mcpp states the fact itself. `-D__mcpp_target_windows__=1` answers the
question those headers ask --- is this target Windows, whatever C environment
is presented above it --- and being mcpp's own name, its meaning is not
decided by anyone else's history. It is emitted only under this substitution;
an ordinary Windows build still has `_WIN64`. `expectDefined` carries it, so
a `-D` that failed to take effect is a verification failure rather than a
silent one.

The name differs from the design's proposed `__mcpp_format_pe__` because the
two consumers do not want the object format: they size Win64 register save
areas, which is the calling convention. The two co-vary on this target, and
naming it for the question actually asked is the honest form.

`__CYGWIN__` REMAINS DEFINED, as step one of three: this release adds the new
name, those packages move onto it while still accepting the old one, and only
then does a release stop defining the borrowed one. Step three taken first
would leave every published copy of those headers falling to its `#else` ---
the wrong record size, reported by nothing. Four loudly failing third-party
members is the better state to hold for one release.

* the macros this engine defines are a module, so the rule cannot drift

`src/toolchain/predefines.cppm` is the specification and the implementation of
one thing. The contract is data in that module (`kContract`), the emission is
a function beside it (`define_tokens`), and `tests/unit/test_predefines.cpp`
asserts the two agree in BOTH directions: a macro emitted and unlisted is a
promise nobody can rely on, a row listing a macro nothing emits is one a
reader waits for forever. This wave already paid once for a rule kept in a
document while the code moved --- the reason-token table was "the four it was
missing" twice over.

GENERALISED PAST WINDOWS. `__mcpp_target_<os>__` is spelt from the triple's
own `os` field, so the ENGINE LEARNS NO OPERATING-SYSTEM NAME and a target
added to the triple parser gets its macro with no change here --- the same
discipline `[kernel-abi]` interface names follow. Measured:
`x86_64-linux-gnu` gives `__mcpp_target_linux__`, `x86_64-windows-gnu` gives
`__mcpp_target_windows__`, `riscv64-none-elf` gives `__mcpp_target_none__`.

DEFINED ALWAYS, not only where a realisation suppressed something. Conditional
emission would make its absence ambiguous: "not Windows" and "Windows, but
nothing hid its macros" would read the same, which is the shape of every
defect where a "no" and a "never asked" share a reading.

Lowercase, `__mcpp_`-prefixed. Two conventions exist --- vendor and product
names upper (`__APPLE__`, `_WIN32`), kind-of-system names lower (`__linux__`,
`__unix__`) --- and these name kinds of target, sitting beside the second
family in real guards. The prefix is load-bearing: a name mcpp owns means what
mcpp says it means, which is exactly what `__CYGWIN__` could not offer.
`__openkal__` joins the same contract; `__unix__` is listed as SUPPLIED rather
than owned, so it keeps the standard spelling and mcpp may not redefine it.

THE SEPARATION IS ITSELF A TESTED PROPERTY. The macro was first realised in
`mcpp.toolchain.cenv`, which made it derived from a declaration --- yet
whether a target is Windows does not depend on any `[c-abi]` block existing.
Moving it out turned the cenv test red, correctly; that test now asserts the
realisation does NOT carry it, because a token with two owners is a token that
will disagree with itself.

docs/21 renders the contract in both languages, and docs/24's note that the
`__CYGWIN__` trade-off "may flip" is updated: it has, and the replacement has
a name.

* C4 does not reproduce, and the belief that blocked measuring it was wrong

`aarch64-macos --profile release` over the openkal stack links, and the
artefact references no `memset_pattern` symbol at all. Measured here, on this
Linux host, with mcpp 2026.9.21.1 and [email protected] through openkal-macos
0.12.0 / openkal-musl 0.18.0 / openkal-llvm-runtime 0.13.0:

    zstd 1.5.7   compiles, links; Mach-O 64-bit arm64, NOUNDEFS; 0 refs
    xz 5.8.3     compiles, links

Both are on the chain the report named. The engine has emitted
`-fno-builtin-memset_pattern16` for `builtins = "iso"` on macOS since before
this branch, and that report was taken on an engine that did not. Nothing is
implemented for C4, and nothing should be: adding the symbol to openkal-musl
would have repaired a gap that does not exist.

THE BELIEF THAT KEPT THIS UNMEASURED IS THE MORE USEFUL FINDING. Both this
plan and the previous wave's self-review stated that macOS facts need a macOS
runner, because a probe targeting `aarch64-macos` failed here. That probe
declared no openkal dependency, so it took the PLATFORM path, which does need
an Apple SDK. The openkal path does not --- universal cross-building is the
premise of the whole system, and a Linux host reaches `arm64-apple-macos14.0`
through it with every layer resolved from the graph.

The cost of that belief compounded: it filed C4 as unmeasurable, and it filed
openkal-macos's `provides-interfaces` as needing a runner when 0.12.0 derived
it mechanically. Both corrected, with the rule beside them --- a claim that
some platform cannot be measured locally has to be tested with a probe that
goes THROUGH the stack under test, not one that bypasses it.

---------

Co-authored-by: speak-agent <[email protected]>