Tags · mcpp-community/mcpp · GitHub
Skip to content

Tags: mcpp-community/mcpp

Tags

v2026.9.6.6

Toggle v2026.9.6.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.6.6: one package, one version — and a rule brings its own envi…

…ronment (#584)

* 2026.9.6.6 —— 一个包一个版本,规则自带它的环境

版本在一个地方属于包的身份,在另一个地方不属于。条件合并按包去重
(`xim:glibc` 与 `xim:glibc@2.40` 是一个条目),而跨包的划分比对整个地址
串(是两个包)。于是工程钉 `xim:cuda-nvcc@13.3.33`、规则包钉
`@>=12.9.86` 时两份都装 —— 每份数 GB —— 而 `fillXpkgDirs` 里「同名保留第
一个值」让 `xpkg_dir` 只答其中一个。装了两份、用了一份,没有一句话提到它。

身份统一为 `(namespace, name)`,版本永远是这个包上的约束。装哪一版分两
步:裁决 —— 离产物更近的声明赢,不带版本的声明弃权,结果被报出来;校验
—— 赢家必须满足每一条落败的要求,不满足就拒绝并同时点出两侧与出路。裸版
本是选择不是要求,两条不同的精确钉走裁决而不被拒绝,否则每一个「依赖钉了
工具、工程也钉了」的组合会在升级当天变成硬失败。

校验是一次比较不是一次搜索:版本由裁决选定再被检查,所以引擎从不需要问索
引「有哪些版本」,也就不带约束求解器。代价写进文档而不是藏起来。

`mcpp::xpkg_dir` 现在回答范围。版本位一直接受范围表达式且 xlings 真的求解
它,而这个查询把整个位置当目录名比对 —— 于是 `>=8.5.0` 装上了载荷然后回答
「没装」。这正是让规则包无法声明下界的那道缝。

四处新拒绝有了名字:`device-source-unconsumed`、`accel-backend-undeclared`、
`host-module-missing`、`tool-version-conflict`。

xlings pin 从 2026.8.30.2 抬到 2026.9.5.1。

判据:e2e 627(装了什么与答了什么必须同时断言)、628(拒绝 + 抬钉后通过的
反向腿 + `--format json` 的 token)、629(范围被安装并被回答)、630(两重门
的两条腿);单测 test_xlings_address_set 与 xpkg_payload_at 的范围/不可解析
版本两组。三条 e2e 在已发布的 2026.9.6.5 上实测为红。

规范:SPEC-001 §10、SPEC-004 §4.5 与判据 5–7。

* docs: 参考文档不用第二人称,四处改回陈述句

* docs(plan): 实现期的实测回填 —— 七条,其中三条推翻了正文

* e2e: 628 不设能力门,拒绝规则与平台无关;记下 diag 的去重是设计属性

* docs(plan): round 7 的沙箱验证脚本 —— 五节,跳过的那几节在 summary 里重列

* docs(20): lane 表那一列是规则「声明的」载荷,不是使用者要写的

* e2e(629): 补上范围的反向腿 —— 不可满足必须被拒,且拒绝里引用那条范围

* docs(07/17): 依赖的 [xlings] 声明会被采纳,且 xpkg_dir 现在回答范围 —— 两处陈述已过期

* docs(changelog): 2026.9.6.6 单独成节,release.yml 才取得到发布说明

* fix(runtime): DT_RPATH 被整条链继承 —— 闭包检查拒掉了一个加载器能起来的产物

* docs(plan): 回填第八条 —— 生态验证挖出的 DT_RPATH 继承缺陷

* docs(plan): 沙箱脚本的三处判据在预演里被证伪并改掉 —— E 节换成自足构造

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.6.5

Toggle v2026.9.6.5's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
docs(plan): the four general build-infrastructure gaps (#583)

* docs(plan): the four general build-infrastructure gaps

Separates what is general build infrastructure from what belongs to the
heterogeneous domain, by a stated criterion: an item qualifies when at least
two of CMake / Meson / Autotools / Cargo have a counterpart and its reason for
existing names no domain concept. RDC fails that test and is left to docs/20;
the primitive it would reuse already exists.

Four gaps survive, and two of them were smaller than they first looked once
main was measured rather than recalled:

  * config.h generation is NOT an engine gap. All four pieces are present
    (toolchain_dir/sysroot_dir for the right compiler, a real program to write
    the file, include-dir to make the package's TUs see it, rerun-if-changed
    for incrementality). What is missing is a shared probe library, which is a
    package rather than an engine change.

  * package layout does NOT need a new section. `[runtime].artifacts` already
    declares a relative path plus a role; the white list is missing exactly one
    role -- a data file read by a loader outside this package. Adding a section
    would have duplicated an answer another section already gives, which
    docs/05 Appendix A refuses.

The two that remain are an `exports` declaration rendered per platform (one
neutral statement, three renderings, the same shape `[runtime]` already
established) and a generic `link-flag` directive, which is the member the
link-lib / link-search / link-script family is missing and the escape hatch a
generated version script needs.

Every criterion is two-sided, and C6 is the one that cannot be verified on a
developer machine: a probe implementation that wrongly reads the host is green
wherever /usr/bin/cc exists, so it has to run in the hermetic container job.

* docs(plan): attribution, the programming-model three-way split, and a falsification target

Revises the design doc. Two of its own judgements were wrong and are corrected
in place rather than appended to, because a design doc whose corrections live
at the bottom is read top-down and gets the retracted version.

  * RDC is plugin-side, engine change zero. Rechecked against the attribution
    rule this revision adds: it names a vendor (test 1), changes no artifact
    property (test 2) and invents no edge kind (test 3). Every primitive it
    needs exists -- `action` with the `object` destination is "an external
    step's object joins the ordinary link", twice. It therefore does not sit on
    the critical path, which changes the staging.

  * stdpar is islandable, so calling it mutually exclusive with mcpp's model
    was an overclaim. `-stdpar` decomposes into a compile-side island and a
    link-side allocator, and the allocator is a whole-image property imposed by
    a dependency -- the shape `cxx_runtime` already established. What is lost
    is the selling point ("change no source"), not the capability. docs/20's
    two-way split should become three-way.

Three gaps added. The first came out of a manifest that reads badly:

    [target.'cfg(not(any(accelerator = "cuda", accelerator = "vulkan")))'.build]

`accelerator` is an open vocabulary by design, and `not(any(<enumeration>))`
over an open vocabulary silently changes meaning as the ecosystem grows -- a
fifth backend edits the meaning of every fallback predicate already written.
The general rule is that an open vocabulary needs a "none" that does not
enumerate, and the spelling is already in this manifest: `os = "none"` is bare
metal, so `accelerator = "none"` is no device backend. Preferred over `cpu`,
which would make the axis carry two questions and leave the truth value of
`cfg(accelerator = "cpu")` under `accel = "cuda"` undecided.

The other two are the package-wide barrier a generated header creates
(`source`'s documented semantics: every compile edge of the package waits) and
Fortran, which is recorded as identified and deliberately given no design.

Also: an attribution rule (three ordered tests) so the engine/plugin boundary
is decided rather than argued each time; a note that "island" collides with
branch-island in linker vocabulary and cuts across single-source/separate-
source, with an alignment sentence rather than a rename; and CANN as the
experiment that could falsify the central claim, since all four existing lanes
are in one vendor lineage. Its decisive criterion is `git diff src/` empty.

* docs(plan): the three CANN uncertainties, measured

Two confirmed, one not, and the one that failed is the only real obstacle.

All CANN repos clone anonymously from gitcode.com/cann at branch 8.5.0 under
the CANN Open Software License Agreement 2.0.

CONFIRMED -- hardware-free execution exists, and it is the vendor's own test
path. Ascend C has three run modes, not two: npu (default), sim (NPU
simulation) and cpu (CPU debug), selected by -DCMAKE_ASC_RUN_MODE=. The
simulators ship inside the toolkit per SoC and asc-devkit's own unit tests link
them (pvmodel_ascend910/310p/610, pem_davinci_ascend910B1/310B/610Lite).

The distinction that decides whether a criterion is usable: cpu mode links
tikicpulib and the tikcpp headers, so the same kernel source is compiled by the
HOST compiler and there is no island in that graph at all. Passing in cpu mode
proves the kernel's numerics, not the mechanism under test -- the criterion
would be pointed at the wrong object, which is a shape this repository has paid
for repeatedly. sim mode is recorded as keeping the island by inference, not by
measurement, with the check to run at implementation time stated.

CONFIRMED -- operator registration is JSON and is per-SoC. In ops-math (72 MB,
1451 .cpp under math/) each operator carries op_host/config/<soc>/<op>_binary.json
mapping the operator signature to the device binary's filename, with six SoC
directories. That is a textbook role = "manifest" case and it is target-
conditional, so the slice exercises SPEC-004's target axis as well. The source
layout is also already islanded: op_kernel/ beside op_host/ per operator.

NOT CONFIRMED -- BiSheng is not a standalone artifact. It lives at
${ASCEND_DIR}/compiler/ccec_compiler/bin/bisheng inside the CANN toolkit, whose
download requires accepting a separate licence and appears to need login. No
stable anonymous URL was found and redistribution terms are unverified. This is
the experiment's one real obstacle and it is not a technical one.

Consequently the feasibility gate is corrected: "hardware or a simulator" was
wrong as a hard gate, because the decisive criterion (git diff src/ empty) is
entirely build-time. The only true prerequisite is obtaining BiSheng lawfully.

Incidental finding worth recording: CMake registers Ascend C as a LANGUAGE
(FindASC.cmake, "plugin support ASC language") with the real machinery inside
the toolkit. That is third-party corroboration of the axis in section 9 --
CMake puts it in the engine, mcpp puts it in a package. Same axis, different
attribution, which is exactly what section 7 exists to decide.

* docs(plan): the toolkit pulls anonymously, so the experiment has no blocker

Overturns the previous commit's one negative finding, and corrects the framing
error underneath it.

The framing error first: BiSheng and the simulator were treated as two
acquisition questions. They are one -- ccec_compiler/bin/bisheng and
simulator/<SoC>/lib are both inside the CANN toolkit.

The toolkit's official distribution is a Docker image, not a .run installer
(measured in ops-math/QUICKSTART.md):

    swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:8.5.0-910b-ubuntu22.04-py3.10-ops

and it pulls anonymously. Measured:

    GET /v2/                                              401
    GET /swr/auth/v2/registry/auth?...:pull               token issued
    GET /v2/ascendhub/cann/manifests/<tag> with token     200, arm64 + amd64

That 401 is what produced the wrong answer the first time. A bare 401 reads
identically to "credentials required", but it is also the first step of an
anonymous token handshake -- Docker Hub behaves the same way. Stopping at step
one turns "anonymously available" into "unobtainable". The criterion has to
walk the whole handshake, which is this repository's recurring lesson about a
criterion whose "no" and whose "not measured" produce the same reading.

Compliance lands in the tier the invariant already allows: proprietary vendor
userspace is fetched from the vendor's own published URL and never copied into
an xlings-res release. Pulling the official image from Huawei's own registry is
exactly that, so no redistribution right is needed.

Also records that AscendNPU IR is now open with a Triton path
(Triton IR -> Linalg -> AscendNPU IR), which gives a rule package a second
possible entry height. Not selected -- Ascend C remains the direct one, because
it is the actual form of the 1451 .cpp files in ops-math.

The feasibility gate therefore has no blocking row. One technical question
remains before implementation: whether sim mode still invokes bisheng, since
that decides which object the device criterion is pointed at.

* docs(plan): sim mode does invoke bisheng, measured in vendored cmake

Closes the last open question in section 10. It was recorded as inference; it
is now source.

asc-devkit vendors the toolkit's own ASC_CMake under tools/ascc/cmake/, so the
device compile path is readable without pulling the image:

    CMakeDetermineASCCompiler.cmake:47
        find_program(CMAKE_ASC_COMPILER NAMES "bisheng" PATHS ".../ccec_compiler/bin/")
    CMakeASCInformation.cmake:49
        CMAKE_ASC_COMPILE_OBJECT = "<CMAKE_ASC_COMPILER> ... -c -x asc <SOURCE>"
    host_config.cmake:69
        CCEC_LINKER = <toolkit>/ccec_compiler/bin/ld.lld

The decisive one is negative evidence: every RUN_MODE test in the repository is
STREQUAL "cpu". There is no sim branch anywhere. Build-time therefore
distinguishes only cpu from not-cpu, so sim takes the same path as npu and
bisheng is invoked. The two differ at run time -- which runtime and simulator
libraries load -- not in the build graph. The one extra action on the non-cpu
branch is update_host_stub.py, which generates host-side launch stubs, and a
launch stub exists precisely because there is a real device binary to start.

Worth keeping the reason the question was asked: the opposite design is real
and has good reasons behind it, since instruction-level simulation is orders of
magnitude slower than host code and a host compiler gives gdb and ASAN. CANN
split those concerns instead -- cpu mode IS that design, so sim would duplicate
it unless it executed real device instructions. Three modes rather than two is
itself the answer.

Section 10 now has no open items.

* docs(plan): the validation item gets a criterion and a slot, and that criterion was wrong

Review pass over the whole document. Three findings, one of which is a defect
in a criterion rather than an omission.

OMISSION -- the validation experiment had neither a criterion nor a stage. It
was described at length in section 10 and then appeared in neither the criteria
table nor the staging table, so nothing tied it to the rest of the plan. Added
as C9 and as a staging row with 13.1 explaining where it sits.

DEFECT -- C9's decisive criterion, "git diff src/ is empty", cannot tell two
failures apart:

  * the diff carries an Ascend-specific identifier   -> the claim is refuted
  * the diff is a general capability like exports    -> the engine is merely
    missing general infrastructure, which is what sections 2-6 exist to add

Both read the same, so running it before the general gaps land would report the
second as the first. Replaced by two levels: the primary criterion is that
test_core_vendor_probes.cpp stays green after the port, which judges by
property rather than by whether anything changed and therefore needs no
precondition; "git diff src/ empty" is kept as a stricter additional statement,
valid only after stages one to three.

That also unblocks running the experiment early: the primary criterion works at
any time, and an early run may surface a fifth general gap, which is cheaper
than discovering it after three stages of engine work.

ORPHAN -- kind = "device" appeared once, in the attribution table, with no
criterion and no stage. It sits in a cross box: engine-side by test 2, but
domain by the section 0.1 admission line, because it talks about accel. Two
rulers, and they cross. Stated explicitly and scoped to docs/20 rather than
left ambiguous.

Also marked the superseded half of revision two in the change record, so a
reader scanning the log does not take an overturned conclusion as current.

* feat: link-flag, and an open vocabulary that can say "empty"

Stage one of the general build-infrastructure design
(.agents/docs/2026-09-07-general-build-infrastructure-gaps-design.md).

mcpp:link-flag (protocol v8). link-lib, link-search and link-script each name
one kind of thing, so a flag the program COMPUTED had no outlet: a version
script whose content depends on which features are on, -Wl,--wrap=malloc for a
runtime that takes over a C-library symbol, -Wl,--exclude-libs,ALL so a
statically absorbed third party does not become part of this package's ABI.

It reaches the consumer, and that CORRECTS the design doc, which first ruled it
private by analogy with include-dir. The analogy is false and the code says so:
linkUsage.ldflags is a copy of buildConfig.ldflags, so a private link flag is
not a policy this engine can express, and [build] ldflags -- the declarative
twin -- already propagates. Making the computed form behave differently would
be the inconsistency rather than the safeguard. The hazard that follows is
stated in the docs rather than hidden: a dependency emitting --version-script
puts it on the consumer's link too, which a dependency writing the same flag in
[build] ldflags has always done.

cfg(accelerator = "none"). A CPU fallback could only be written by enumerating
the backends it is not, and accelerator's vocabulary is open by design -- so
that predicate changes meaning the day a fifth backend exists, and every
fallback already written starts treating a build that named the new backend as
having no accelerator. The spelling reuses os = "none", which this manifest
already means "bare metal" by. Not cpu: that puts a second question on an axis
whose job is "which device compiler, which architecture", and leaves
cfg(accelerator = "cpu") under accel = "cuda" with no self-consistent answer.

Both criteria measure the property rather than its shadow. e2e 620 asserts the
LINKER'S BEHAVIOUR -- the program computes -Wl,--defsym=mcpp_e2e_620=42 and the
artifact prints that symbol's address -- because grepping build.ninja would
pass for a flag written down and never handed to the linker. The unit test
SIMULATES the fifth backend arriving: the enumeration starts lying on the spot
and none does not, which is the entire reason the row exists and is invisible
under a single backend.

* feat: exports -- one neutral statement of the symbol surface, three renderings

Stage two of the general build-infrastructure design.

Both platforms already publish everything: ELF gives symbols default
visibility, and PE gets an auto-generated .def listing every symbol
(mcpp.build.coff_exports, WINDOWS_EXPORT_ALL_SYMBOLS semantics). What was
missing is the other direction.

A runtime with a stable ABI publishes a reviewed set so that what is outside it
stays free to change; a plugin loaded beside its rivals must not collide -- a
Vulkan ICD that exports its internals collides with the loader and with the
other ICDs in the process. This repository has the symptom on file: mcpp's own
duplicate-symbol check on the SYCL example reports 68 _Unwind_* symbols,
because one image holds two C++ runtimes and both export them.

exports takes a file of symbol patterns or an inline list, and the backend
renders it per platform -- version script, -exported_symbols_list, or the .def
that replaces the all-exports one. One statement, three renderings, which is
the shape [runtime] already established and the reason this is a manifest key
rather than three platform-specific flag lists.

IT DOES NOT IMPLY HIDDEN VISIBILITY, and that CORRECTS the design doc, which
said it should. The narrowing is a link-time property on all three formats, so
implying a compile-time one would give a single key two effects -- and the
second effect also changes how this library's own translation units see each
other, which is a separate decision with a separate reason.
-fvisibility=hidden stays available through [build] cxxflags for the code
generation it buys.

The export list is read at manifest load rather than at plan time, so every
later stage sees one representation; origin's parent is the package root and
that holds for the root, a path dependency and a store dependency alike, which
is the "same decision in N places" this would otherwise become.

e2e 621 builds one source twice and requires the two readings to DIFFER.
Asserting only that the public symbol is present passes for a library that
exports everything, which is the state before this change; asserting only that
the internal one is absent cannot distinguish "correctly hidden" from "never
linked at all".

* docs(plan): stages one and two landed, and three judgements they corrected

Records what implementation established, including where it contradicted the
design.

  * link-flag reaches the consumer. The doc ruled it private by analogy with
    include-dir; the code refutes the analogy. linkUsage.ldflags is a copy of
    buildConfig.ldflags and propagateLinkFlags pushes every dependency ldflag
    to the consumer, so a private link flag is not a policy the engine can
    express. include-dir is private because a compile interface HAS a
    declarative public counterpart; link flags do not, so the computed form
    must behave like its declarative twin. C4 is rewritten accordingly.

  * exports does not imply hidden visibility. The narrowing is link-time on all
    three formats, so implying a compile-time effect gives one key two, and the
    second changes how the library's own TUs see each other -- a separate
    decision. C3 is retired because it asserted exactly that coupling.

  * Section 4.3's open question is answered. manifest_emit builds
    [[runtime.artifacts]] from doc.legs plus one interface entry and does NOT
    carry the author's declared artifacts; role is a free string in the parser
    and the white list is an effect of its readers. So stage three is a new
    path through the packer -- carry a declared file into the artifact -- not
    a new role value. Staging is unchanged; its rationale is.

* docs(examples): a multi-backend artifact, an Ascend one, and the table a rule package belongs in

Two examples, and one correction that applies to every example already here.

multi-backend -- several backends in ONE artifact, chosen at run time. The four
examples beside it are each one seam: a device file and a CPU file define the
same symbol and are never in one link, so exactly one exists and the choice is
made at build time. That is right for a program. A library cannot make that
choice; it is compiled once and consumed by people whose machines differ, so its
backends are additive.

It is also the first real use of cfg(accelerator = "none"). The dispatcher must
be built for cuda, for vulkan, for both, and for a backend that does not exist
yet, and not(any(cuda, vulkan)) needs editing every time the ecosystem grows --
the edit that is forgotten being silent. Its default accel is empty, so a plain
mcpp build needs no payloads at all, which is what lets CI build it and makes
the "none" path genuinely covered rather than described.

cann -- Ascend, behind the same seam. It DOES NOT BUILD, and the README says so
and names the two missing pieces: a rules-ascendc rule package, and an xim
package for the toolkit. It records what was measured instead of guessed: both
BiSheng and the simulator live in that one toolkit; the toolkit image pulls
anonymously; sim mode needs no hardware AND keeps the island, while cpu mode
does not keep it and therefore cannot stand in for a device criterion. The
manifest is written out rather than described so the shape is concrete. CANN's
own operator libraries already split op_kernel/ from op_host/, so the island is
not a shape mcpp imposes on Ascend.

THE CORRECTION. All four existing examples declare mcpp:plugins in
[dependencies], and docs/05 section 2.6.1 names exactly that case as what
[build-dependencies] is for: a package whose library must never reach the target
while its rule is still wanted. The two axes are separate -- host-module = true
says which build-time product is wanted, the section says whether the package
reaches the target, and a rule package answers no on the second. Verified by
moving it and rebuilding: build.mcpp compiles, the rule runs, shaders compile,
and the artifact runs on a real device.

Writing it in [dependencies] still works, which is precisely why the
distinction has to be stated rather than left to a failure to teach.

* Five defects a second backend found, and none of them was findable by reading

The gap list this branch implements came from reading the code and from one
device backend. Writing the second one turned up five more in an afternoon, and
not one of them could have been found by continuing to read: every reading is
either "the build succeeded" or an error message pointing somewhere else.

A DEVICE SOURCE CAN COMPILE NOTHING, AND THE BUILD SUCCEEDS. A device-kind file
is the one source the engine has no rule for: it goes to the package's build
program and comes back as an action, or it is never compiled -- and nothing
checked that it came back. The multi-backend example had no build.mcpp at all,
so both device globs were dropped in silence and the reading was `undefined
reference to opkit_cuda_saxpy` at the link: a message naming a symbol and never
the file that would have defined it. A `kind = "lib"` target does not even get
that, because an archive is not resolved. Now refused, naming the files, and
distinguishing "no build program" from "a program that claimed nothing" because
the fixes differ. The criterion is the action INPUTS, which is also the
condition an action needs anyway: one that compiles a file it does not declare
as an input does not rerun when that file changes.

ONE RULE TOOK EVERY DEVICE SOURCE. `device_sources()` is the package's whole
set and every rule in a build program reads it. Correct for exactly as long as
a build has one rule in it -- a premise never written down. Measured with two:
the CUDA rule compiled `scale.comp` AS CUDA and produced an object, and the
shader rule then failed on the `.cu` with a message about stages. The louder
failure was the harmless one. Fixed in mcpp-plugins 0.2.2 (each rule claims its
extensions); the engine's half is the refusal above, because "no rule claimed
it" is only visible here.

`accelerator` WAS CLASSIFIED AS A RESOLVED LAYER. The five real layers are
answered by dependency resolution, so refusing them in `[xlings]` predicates is
right: payloads are installed before resolution. The accelerator is not one of
them -- it is `--accel`, or `[build] accel`, read before the first package is
looked up. The cost was paid on every build with a device island and paid worst
on the cheapest one: a vendor toolkit could be declared unconditionally or not
at all, so a CPU-only build downloaded gigabytes for a device it was not
compiling for. Split by SCHEDULE rather than by subject; the connected
consequence is that `[target.'cfg(accelerator = "cuda")'.dependencies]` now
applies, there being nothing circular about it.

NAMING A SUBSET OF BACKENDS WAS TREATED AS A MISMATCH. The refusal is right
about architectures -- a file for sm_89 in a build targeting sm_80 is not a
variant -- and was applied across backends, so a package with a CUDA island and
a Vulkan one was refused when built with `--accel vulkan1.2`. A package could
have several device backends only if every build took all of them, which is
exactly what an additive-backend library cannot do. A glob whose backend is not
named is now left out as `--no-accel` leaves it; what keeps that from turning
`accel = "cude12.9"` into a file that is never compiled and never mentioned is
a new check against `[package] accelerators`.

A RULE'S PAYLOAD WAS UNREACHABLE FROM THE CONSUMER. A rule's code runs inside
its consumer's build program, so `xpkg_dir` is asked there while the payload was
declared in the rule's own `[feature-xlings]`. The graph already installed it;
only the answer was missing, because `fillXpkgDirs` read one manifest. The
reading was "the toolkit is not installed" with the toolkit on disk.

Also: an import no dependency provides is refused by name. Left to the compiler
it is `failed to read compiled module` plus a note that imports must be built
first -- true, and naming neither the package nor `host-module = true`. The set
of names that can compile there is closed, so a name outside it is refused with
the candidates that could have provided it.

VERIFICATION. e2e 622 through 626, five files. 622 and 625 were run against the
released 2026.9.6.4 and failed as designed. 626 has a negative leg because two
of its three legs would pass on an engine that refused every device source.
107 unit tests pass; the two cfg tests that stated the old classification now
state the new one.

examples/09-heterogeneous/multi-backend is the example all of this came from,
and it now works: four command lines, three of them on a real RTX 4080.

    mcpp run                                    backend: cpu (only backend in this build)
    mcpp run --accel "vulkan1.2"                backend: vulkan (NVIDIA GeForce RTX 4080)
    mcpp run --accel "cuda12.9+{sm_89}"         backend: cuda
    mcpp run --accel "cuda12.9+{sm_89}, vulkan1.2"   backend: cuda

Its Vulkan half was a stub that declined; a stub would have printed `backend:
cpu` for a Vulkan build, which reads as "Vulkan failed here" rather than "this
was never written", so the real implementation is ported from the sibling
example. Its CUDA leg takes the clang route, measured rather than chosen: on the
12.9 line nvcc's own front end refuses the toolkit's non-`noexcept` `cospi`
against the C library's, an older `xim:gcc` payload does not help because the
declarations come from the C library, and the 13.x line raises the driver floor
to r580 -- a requirement on the machine rather than a decision the project makes.

* `.asc` and `.cce` enter the device-source table, and the CANN toolkit turns out to be one download

The Ascend example named an `xim:cann-toolkit` that did not exist and a device
extension the engine did not know. Both are now answerable, and the answer to
the second was simpler than the design doc said.

THE EXTENSION. `.asc` is Ascend C, compiled by `ccec` (BiSheng) from the CANN
toolkit, and `.cce` is the older spelling of the same thing. They belong in the
table for the reason `.sycl` does: the content is ordinary C++ and the
criterion is the compiler. CANN's own operator libraries already split
`op_kernel/` from `op_host/`, and CMake registers ASC as a language of its own
-- the island is a shape Ascend already has, not one mcpp imposes on it.
Adding to this table cannot change a build that works today: device extensions
are absent from the default source glob, and one named in `sources` was until
now a hard error.

THE TOOLKIT. The design doc recorded that the vendor's container image pulls
anonymously, which is true and is not the simplest route -- it stopped one
layer short. Measured today, with no credentials at all: every toolkit from
8.0.RC1 to 8.5.0 is a plain `.run` on Huawei's own OBS, answering 200 to a HEAD
request. 8.5.0 is 1.12 GB for x86_64 and 1.10 GB for aarch64.

It installs without root and without a driver:

    ./Ascend-cann-toolkit_8.5.0_linux-x86_64.run --install \
        --install-path=<dir> --quiet

and 2.9 GB later the two pieces the lane needs are both there:
`x86_64-linux/ccec_compiler/bin/{ccec,bisheng}` runs and reports clang 15.0.5,
and `x86_64-linux/simulator/` carries 38 SoC directories, each with its own
`libpem_davinci.so`. So the whole lane -- compiler and a hardware-free way to
run what it produces -- is one anonymous download.

The installer writes two things outside its install path, `~/Ascend` (8 KB, an
install record) and `~/var/log/ascend_seclog`. Measured: overriding `HOME` for
the duration contains both, and the payload installs identically, so a package
recipe need not be a bad citizen of the user's home directory.

This settles what the design doc listed as the lane's only remaining
prerequisite. Compliance-wise it sits in the first tier of the standing rule --
closed-source and not redistributable, so it is fetched from the vendor's own
URL with no CN mirror, which costs nothing here because the vendor's URL is
already in-country.

Verified after the mcpp-plugins 0.2.2 release: the multi-backend example now
resolves the PUBLISHED `mcpp:plugins@0.2.2` from the index with no path
override and no lock file, and runs -- `backend: cpu` with no accelerator and
`backend: cuda` on a real device.

* The first thing the new refusal caught was this repository's own fixtures

Three e2e fixtures built a `build.mcpp` that read `mcpp::device_sources()`,
printed it, and compiled nothing. They were asserting on the env contract, which
is a real thing to assert on -- and they were modelling a project whose device
files compile to nothing, which is exactly the defect the new refusal exists to
catch. So the refusal caught them, and it was right to.

606, 609 and 613 now declare a `check` action per device source, which is the
edge a real rule package declares, and go on asserting on the variable exactly
as before. The action's input has to be ABSOLUTE: `device_sources()` is
package-root-relative and an action does not run in the package root, which the
first attempt got wrong and `cp: cannot stat 'shaders/s.tesc'` said plainly.

EXAMPLE 05 PINS ITS TOOLCHAIN, and this is a different failure that surfaced in
the same run. `mcpp pack` names its output directory after the ABI tag, and the
consumer beside it names that directory literally -- so the pair only agrees on
a machine whose DEFAULT toolchain is the one that produced the tag. Measured:
a runner whose sandbox had been left with an LLVM default produced
`mathkit-0.1.0-x86_64-linux-gnu-clang22-libcxx22-c++23` while the consumer
named the `gcc16-libstdcxx16` one, and the example that had passed for months
failed without anything in it changing.

An example that hard-codes an ABI tag has to name the toolchain that produces
it. That is not a workaround for CI: a packed artifact IS specific to the ABI
it was built against, which is the entire subject of that example, so leaving
the toolchain implicit was the inconsistency.

* The Ascend example stops being a sketch: the kernel compiles, and the driver is what is missing

It was written as a shape with two named gaps -- a rule package and an xim
package for the toolkit -- and both now exist. What replaced them is a list of
measurements.

On an x86_64 machine with no Ascend hardware and no Ascend driver:
`xim:cann-toolkit` provisions 2.9 GB without root; `build.mcpp` compiles and
imports `mcpp.rules.ascendc`; the kernel compiles under
`bisheng -x asc --cce-aicore-arch=dav-c220`; the resulting object joins the
ORDINARY link, because the rule compiles in mixed mode and gets an x86-64
object carrying the device binary rather than a Da Vinci one the host linker
cannot place; the host half links against ACL and the six-library closure the
rule names. The artifact then does not start, because `libascend_hal.so` is
missing -- the DRIVER, which is the role `libcuda.so.1` plays for CUDA: in ABI
lockstep with the kernel module, not redistributable, and correctly absent
here. `mcpp run --no-accel` builds and runs: `12 24 36 48`, `device: cpu`.

So the example completes everywhere and RUNS only on an Ascend machine, which
is the same statement `examples/09-heterogeneous/cuda` makes about a machine
with no NVIDIA driver. Its skip reason says that instead of what it used to
say.

The payloads are gated on the accelerator, so the CPU leg installs nothing at
all -- which is the first real use of that gating outside the multi-backend
example.

THE SEAM IS A C FUNCTION, and that is measured rather than stylistic: BiSheng's
own launcher for a `__global__` function is C++-MANGLED even when the kernel is
declared `extern "C"`. Calling it directly would make the program depend on
BiSheng and the project's C++ compiler agreeing about mangling -- clang 15 and
whatever the project chose. The `.asc` file exports an `extern "C"` wrapper
instead, and the `<<<...>>>` launch spelling never leaves the translation unit
the device compiler owns.

The README's "what was established about the toolkit" section is replaced by
what the toolkit turned out to BE, including the 38 SoC simulators that make
this lane verifiable without an NPU -- which is the next thing this example
should use, and a separate piece of work with its own contract.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.6.4

Toggle v2026.9.6.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix: a dependency's [feature-xlings] tools must be installed before i…

…ts build.mcpp runs (#581)

* fix: a dependency's [feature-xlings] tools must be installed before its build.mcpp runs

The root's `[xlings.workspace]` declarations are provisioned early. The GRAPH's
were provisioned about 1700 lines further down, after every build.mcpp had
already run. So a package that declares a tool under the feature that needs it
and then asks for it with `xpkg_dir` worked as the ROOT and was refused as a
DEPENDENCY -- with the very declaration it had already made quoted back at it.

Measured on the published `ggml-org:llamacpp@b10069.2`, which is exactly this
shape:

    [feature-xlings.backend-vulkan]
    "xim:shaderc" = "2026.3"

A consumer that writes the one line its README asks for could not build on a
clean machine:

    error: dependency 'llamacpp': build.mcpp exited with 2 (build aborted):
    ggml-org:llamacpp backend-vulkan needs glslc. Declare it: ...

With an isolated MCPP_HOME the unfixed engine pulled twenty-four xim payloads
for that graph and not shaderc; the fixed one pulls it and the consumer builds,
1921 shaders compiled.

WHY NOTHING CAUGHT IT. Once the tool is in the registry for any reason, and
building the package itself puts it there, `xpkg_dir` finds it and the ordering
stops mattering. llama.cpp-m's own CI builds it as the root; every machine that
had touched the package had the payload. Only an empty registry can see this,
which is what the sandbox run is for.

The split is now computed once, beside `activeFeaturesByPackage`, and read by
both the early provisioning pass and the later record-writing pass, so the two
cannot disagree about what "the graph declared" means. The late provisioning
call is kept: the stamp is keyed by content, so it is normally a hit, and it
remains the site that makes the record true if the early pass did not run.

e2e 618 is the criterion, and its first version was a false green worth
recording: it used `xim:ninja`, which mcpp installs to run its own builds, so
`xpkg_dir` answered on the broken engine too. It now uses the tool the defect
was actually found with.

* feat: the tool plane gets its target axis, and docs/spec becomes docs/specs

A tool entry answers one of two different questions. `xim:dpcpp` is a compiler
that runs on the build machine whatever it is asked to emit; `xim:glibc` is
what the produced code is compiled against. `[xlings]` resolves its
platform-keyed values against the HOST at load, so only the first was
expressible. The SYCL example wrote both in that one table and was right by
accident: on a native build the two axes name one platform.

`[target.<selector>.xlings.workspace]` and
`[target.<selector>.feature-xlings.<f>]` are now accepted and resolved against
the TARGET, in the same shape `[target.<selector>.build]` and
`[target.<selector>.feature-deps.<f>]` already had -- the condition written
once, outside, on the selector. Zero new vocabulary, zero new sections, no
published key renamed, every existing spelling unchanged in meaning. The rule
this is an instance of is written down as SPEC-004
(docs/specs/manifest-semantics.md); the field reference is docs/05 section 2.13.

Four things are refused rather than resolved, because each of them is a
statement that would otherwise disappear in silence:

  * A value under a selector that also carries platform keys. The selector
    already said which targets this applies to; two statements of one fact can
    disagree, and the message names the outer selector because that is the
    half the author is not looking at.

  * A selector that names a target-side layer (`accelerator`, `c-abi`,
    `compiler`, ...). This one is schedule, not style: a layer is answered by
    dependency resolution, so a predicate naming one is held back to the second
    merge pass -- which runs after tools are provisioned and after every
    build.mcpp. An entry admitted there would be declared and never installed,
    and the build that results SUCCEEDS with the tool absent. e2e 619 measures
    both sides: on the previous engine that same project builds.

  * `subos` under a selector. A project has one environment, not one per
    target.

  * Nothing, at publish time -- but it is reported. A descriptor has one block
    per platform and a selector is not a platform, so a target-axis entry
    produces no descriptor edge. Guessing a representative triple per platform
    would drop every entry that triple did not satisfy into the same silence.

Where both axes name one package the conditional entry wins and the override is
reported, and dedup is by PACKAGE rather than by address: `xim:glibc` and
`xim:glibc@2.40` are two addresses for one install, and keeping both is not a
build that fails, it is a build whose environment depends on iteration order.

THE CRITERION IS CROSS-TARGET AND RUNS ON ONE MACHINE.
`tests/unit/test_target_xlings_axis.cpp` parses one manifest and merges it
against two different resolved targets: the target-axis entries appear and
disappear with the target while the host-axis entry is present both times. A
non-cross build carries zero information about this difference, so an e2e that
builds green here would have proved nothing. With the wiring removed, five of
its six assertions fail; the one that passes is the host-axis regression, which
is what it is there to say.

The SYCL example moves its four compiled-against payloads to the target axis
and still builds, with all five provisioned and the ecosystem C library ahead
of the host on the device compiler's own search list.

Also: `docs/spec/` is now `docs/specs/`, with the 45 references rewritten.

* docs(plan): remove the one emoji marker from the design doc

* test(round 6): the sandbox script for the target axis, and two consequences the spec now states

The script is what section 7's criteria are run by, against a PUBLISHED
binary in an empty registry -- which is the only place the provisioning half
of the target axis can be asked at all. On a machine that has built the
package before, the payload is already there and the answer is the same
whether the axis was read or not.

Rehearsed against an isolated MCPP_HOME with the local build: six sections,
zero failures, and section B's tool arrived by a real install into a registry
the run had just observed to be empty.

The spec also now states the two consequences the implementation makes true
and nothing else records: a consumer sees the UNION of the two axes (a rule
package does not learn which axis declared a payload), and the materialised
.mcpp/.xlings.json describes the LAST BUILD'S target rather than the project.

* test: a bare-triple selector reaches the tool dispatcher too

`[target.<triple>.xlings.workspace]` is the other spelling of a selector, and
whether it reaches this dispatcher rather than the `[target.<triple>]` handler
that owns `toolchain` and `runner` is not visible from a manifest. If it went
to the wrong one the entry would simply never appear, which is a failure with
no output at all.

Both legs: the matching triple carries the entry, a different one does not.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.6.3

Toggle v2026.9.6.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat: cxx_stdlib, the curriculum in CI, and two criteria that machine…

… state decided (#579)

`mcpp::cxx_stdlib()` / `MCPP_CXX_STDLIB` (2026.9.6.3). The engine has resolved
this value for a long time -- it is in the cache key, the ABI tag, the toolchain
fingerprint and `resolution.json` -- and never handed it to the layer that has
to decide on it. `compiler()` cannot answer it: clang links libc++ on one
machine and libstdc++ on another and reports "clang" either way, while the two
differ in what they accept. llama.cpp-m has the case today, and its refusal
lands in the same round. e2e 617 compares the answer against `resolution.json`
and against the compiler family; removing the wiring turns it red.

The examples say which device ran them. All four islands in 09-heterogeneous
printed the same four numbers as their CPU fallback, so a silent fallback was
indistinguishable from a device run -- in the one set of examples whose subject
is which device ran the computation. The seam carries `saxpy_device_name()`,
each backend fills in its own device, and it is set only after a successful
call: a device run that did not happen has no device to report. Measured:
`device: llvmpipe (LLVM 22.1.8, 256 bits)` against
`device: cpu (this build names no accelerator)`.

CI builds the curriculum. No job did. `.github/tools/build_examples.sh`
enumerates the example ROOTS from the tree and compares them against a build
list and a skip table; a root in neither fails the job, and every skip names its
reason and where the coverage is. Six of fifteen build, including
05-lib-distribution through its own README's two-step order -- which also checks
that the ABI tag its consumer hardcodes is still the one `mcpp pack` produces.
The Vulkan example is built and RUN on the lavapipe payload.

Two e2e criteria were decided by machine state, and both were green on every
runner because a runner installs exactly one of anything.
`206_runtime_binding_physics` asserted `inconclusive` on the assumption that the
private loader cannot reach a host `libtinfo`; `xim:ncurses` is an ordinary
ecosystem package, and a sub-OS that has it puts `libtinfo.so.6` on the
artifact's RPATH, where the closure genuinely closes. It now reads the
artifact's own search path and asserts in both directions.
`168_build_mcpp_musl_host_static` selected its payload with `ls | head -1` --
lexicographic order, hence the oldest -- and on a machine with three musl
toolchains chose one that predates the `std` module. It takes the newest, which
is what resolution picks when nothing pins a version.

The four device examples move to `mcpp:plugins` 0.2.1; the SYCL one also
declares `xim:glibc` and `xim:linux-headers`, unpinned, because the device
compiler is a second compiler and does not inherit the toolchain the engine
configured.

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.6.2

Toggle v2026.9.6.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Round 5 opens: two engine defects llama.cpp's Vulkan backend exposed,…

… and the examples regrouped (#576)

* refactor(examples): group the four device examples under 09-heterogeneous

The four examples are one lesson in four programming models. They share the
kernel, the seam, the constrained glob, the CPU fallback and the answer; they
differ only in which compiler the rule package drives. Four consecutive numbers
in a curriculum say "four lessons", and a fifth model would have said five.

    examples/09-heterogeneous/
      README.md   the shared structure: island, seam, constrained glob,
                  rule package, and the four layers underneath
      cuda/       was 09-cuda-kernel
      vulkan/     was 10-vulkan-compute
      sycl/       was 11-sycl-kernel
      hip/        was 12-hip-kernel

The move carries no behaviour change. It also aligns the pins that had fallen
behind the released ecosystem, because those edits touch the same manifests:

  * examples cuda and vulkan pinned `mcpp:plugins 0.1.1`; sycl and hip already
    pinned 0.2.0, so a reader comparing four examples saw two answers to one
    question.
  * examples cuda and hip named `compat:cuda-runtime`, an entry that is frozen
    and renamed. Its own recipe states the correction: what it farms is
    libcuda.so.1, the driver's userspace library, and "CUDA Runtime" in NVIDIA's
    vocabulary is libcudart, which `xim:cuda-cudart` already delivers as a
    payload. Both now name `compat:cuda-driver`. The old key still resolves, so
    this is a spelling correction rather than a repair.

Version references in prose are left alone on purpose. A floor marker such as
`*(2026.9.5.2+)*` states when a feature landed and is a historical fact;
bumping it to the current release would make the document lie about its own
subject. Only current pins track the release.

References in CHANGELOG.md and in the dated design documents keep the old
paths: they record what shipped under a released version, and rewriting them
would make the record state something that was not true at the time.

The round 4 verification harness moves with the examples, since it is a tool
rather than a record. Its two example sections now fail loudly when
MCPP_VERIFY_SRC names a checkout without them; previously a stale path would
have read as "not requested" and skipped.

* fix(build): a tool `[feature-xlings]` declared is now answerable through xpkg_dir

`[feature-xlings.<f>]` has provisioned its packages since it existed: naming
one downloads and installs it when `<f>` is active. The build program's
environment, however, was filled from `[xlings.workspace]` alone, so
`mcpp::xpkg_dir` returned "" for a payload sitting in the store.

The shape is an answer that was resolved and never wired to the decision that
reads it: the provisioning side reads "declarations plus the active features",
the lookup side read only "declarations". It surfaced building llama.cpp's
Vulkan backend, where the rule could not find `xim:shaderc` and the only
sensible thing it can print is "declare xim:shaderc" -- naming a declaration
the author had already written. A diagnostic that points at the wrong file is
worse than none.

`fillXpkgDirs` now folds in the `[feature-xlings]` entries of the feature
closure the caller had already computed, so "which features are on" is still
answered once. Installation stays the only filter, which is what keeps a
`when = "dev"` entry answering "" for a consumer.

e2e 614 is the criterion and it carries a control: the same project built
twice, the payload path under `--features gpu` and an empty answer without it.
Asserting only the first would pass on an mcpp that answered for every package
it had ever installed. The package is `xim:ninja` -- one mcpp bootstraps into
its own sandbox, so the test needs no network -- and its version is read from
the store rather than written down, since a hardcoded version would turn an
mcpp that bumped ninja into a failing test about features.

Measured: the test exits 1 on the previous binary and 0 on this one.

* fix(plan): a shared library from `[feature-deps]` now reaches the link line

The second defect of the same family, surfaced by the same build. `[feature-deps]`
is folded into the root's dependency map during resolution, so the dependency
was resolved, fetched and COMPILED -- every log line said it was there. The plan
read the root's edges from `packages[0]`, a snapshot taken BEFORE that fold, so
the dependency's shared library was neither linked nor named as an implicit
input.

It was quiet because of the shape it needs. `mcpp build` on a library package
produces an archive, and an archive resolves no symbols, so the build succeeded.
The failure appeared in whoever linked an executable: llama.cpp's Vulkan backend
built `libllama.a` cleanly and then failed to link its test with
`undefined reference to vkGetInstanceProcAddr`.

The fix follows the idiom already in this file: the root's truth lives in `*m`,
not in `packages[0]` -- `checkVersionFloors` takes the same branch for the same
reason.

e2e 615's SHAPE is the criterion. Written as a binary, the same project passes
on both the defective and the fixed engine -- measured -- so a fixture built
around `mcpp run` would have been a test that could not fail. What surfaces the
defect is a library root whose test binary does the linking, which is the shape
every library package in this ecosystem has.

Measured on llama.cpp-m with the two binaries side by side: `mcpp test
vulkan_decode --features backend-vulkan` fails to link on the previous binary
and answers `LLAMACPP_VULKAN_TEST=PASS` on this one.

* docs(20): the shape a framework takes on top of the four lanes

Round 5's first entry measured five things that generalise beyond llama.cpp,
and docs/20 is where the shape of a device build is stated once rather than per
project. Both languages.

The one worth naming here: a software Vulkan device is not automatically a
substitute for hardware. ggml keeps only devices whose type is not `eCpu`, so
Mesa's lavapipe is excluded for its type alone while advertising every feature
the backend requires. That is the framework's policy rather than a packaging
defect, and the way past it is the framework's own selector.

* chore(release): 2026.9.6.2

Two engine defects, both found building llama.cpp's Vulkan backend and both of
the same shape -- an answer that was resolved and never wired to the decision
that reads it. A tool declared under `[feature-xlings]` was installed and then
invisible to `mcpp::xpkg_dir`; a shared library acquired through
`[feature-deps]` was compiled and then left off the link line. Neither had a
diagnostic: the first printed "declare this package" about a declaration the
author had already written, and the second let a library package build cleanly
and fail in whoever linked an executable.

Both sites are updated together, as `.github/tools/check_version_pins.sh`
requires.

* docs(plan): round 5's task list, its status, and the harness that decides it

The plan asked for a task breakdown that survives a context break; this records
what each task is, which repository owns it, what it depends on, and -- for the
ones already done -- the measurement rather than a checkbox.

Two of the plan's own assumptions were wrong and are corrected in place rather
than quietly dropped. "Correct tokens" is too weak a criterion: a token inside
the vocabulary is produced by a backend that computed nonsense and by a build
that never reached a device, so the criterion is now an equality between the
device decode and the host decode, with the offload asserted first because two
host decodes agree trivially. And "on lavapipe" was not reachable as stated:
ggml keeps only Vulkan devices whose type is not `eCpu`, so a software
implementation is dropped for its type alone.

The round 5 harness follows round 4's rule, which round 4 earned the hard way:
every check that selects its own object prints which object it selected.

* docs(plan.cppm): name the rule the fix follows, and record the enumeration

'A root package's dependency EDGES are read from *m, never from packages[0]'.
The four readers in this build were checked rather than sampled; this site was
the only one taking the snapshot for a question about edges, and publisher.cppm
wants the unmerged edges on purpose.

* test(e2e 614): a payload directory is not required to have bin/

The guard picking a ninja version asked for `bin/ninja`, and this payload puts
its executable at the payload root. On this machine that turned a working
criterion into a skip -- which reads the same as a pass. Non-empty is the
property the test actually needs, and it also handles the case the guard was
added for: a directory an uninstall left behind, which `xpkg_dir` answers ""
for and which would have failed the test about features while really measuring
the store.

Re-measured against the binary that predates both engine fixes: exit 1, with
the intended assertion failing and the control still passing.

* test(verify): the additivity criterion belongs on mcpp.lock, not resolution.json

The first draft grepped resolution.json for a backend package name. That file
records the runtime BINDING of the environment -- it lists whatever the
machine's SubOS provides -- so on this machine it names mesa-lavapipe whether or
not the project asked for it, and the check would have failed about the feature
while measuring the host.

mcpp.lock records what the project resolved, and it is state rather than a log
line. It has to be removed first: a lock left by an earlier build of the same
tree still lists that build's packages.

Measured: with the lock removed, a CPU-only build of llama.cpp-m writes no lock
at all. It resolves nothing.

* docs(verify): the sandbox invocation in both harness headers could not be followed

Two things, both measured on 2026-09-06:

  * `xlings subos <name>` is rejected -- the verb is `xlings subos use <name>`,
    and the environment has to exist (`xlings subos new <name>`) first;
  * the sandbox gets an EMPTY $HOME and a fresh /tmp, so `bash <this file>`
    finds nothing. The script has to be passed in, and base64 through --cmd is
    what works.

mcpp is addressed by its store path for the same reason: the xlings data
directory is shared into the sandbox, and a bare `mcpp` is not on PATH there.

An instruction nobody can follow is worse than none, because it reads as though
the step were done.

* docs(plan): llama.cpp leaves the 'not built' row

F1 is measured: the Vulkan backend builds, and on a software device with no GPU
the decode equals the host's. The gate did what a gate is for -- two engine
defects in its first hours -- so F2 may start.

* test(e2e 616): the examples on disk and the ones the docs list must be the same set

No CI job builds an example, so the curriculum is the part of this repository a
rename can break silently -- the docs keep pointing at a path that is gone and
every job stays green. This round moved four examples and edited 14 files by
hand.

The check is bidirectional, and one direction alone would not be worth running:
'every listed path exists' passes on a document that lists nothing, and 'every
example is listed' passes on a document that lists everything and points half of
it at the wrong place.

It found a pre-existing gap on its first run: examples 04 through 07 exist on
disk and neither language's 01-examples.md listed any of them. They are listed
now.

Structural, and it says so: it builds nothing, so it cannot tell whether an
example still works.

* test(verify): a section that measures the PUBLISHED form, not the checkout

Every other section reads a source tree, which is the development form. A
sandbox is the one place that can tell whether what was published resolves and
works, and section F is the only one that asks: a consumer declaring
`ggml-org:llamacpp` with `features = ["backend-vulkan"]` and nothing else --
which is also the claim the README makes about the diff a user writes.

It is red between the tag and the index PR by construction, because the release
order is the reverse of the dependency order. That is the correct reading, not
a flake.

* fix(manifest): the version warning must not predict a failure it cannot see

`cmdline = "0.0.x"` earns a warning for a good reason: the form does not
parse, and the failure that follows names the PACKAGE, sending the reader to
investigate a package that exists. But the message asserted "The fetch will
fail", unconditionally.

A string that is not a range is used as an EXACT index key, so whether the
fetch fails depends on whether the index carries that key -- which this parser
cannot see. `0.0.x` fails because no such key exists; `b10069` succeeds
because the key is there verbatim, and that is the version scheme of a
published package in this ecosystem (mcpp#363). Every build of that package
printed a warning predicting a failure that did not happen, which teaches a
reader to ignore the channel.

The message now states the mechanism instead.

The three assertions moved with it, from the prose to the PROPERTIES the
message must carry: it quotes the offending string (or the reader cannot find
it), it contains the word PACKAGE (because that is what the subsequent failure
names), and it shows an accepted form. One of the three also only asserted
"some warning exists", which a warning about anything else satisfies.

* docs(01): example 07 shows PATH from the declared environment, not xpkg_dir

The row listed `mcpp::xpkg_dir` as one of the example's key concepts. Its
build program reads `MCPP_XPKG_QEMU_RISCV_DIR` directly and, more centrally,
demonstrates that a project declaring `subos` gets that environment's `bin`
at the front of its build program's PATH. An index that names a concept the
example does not show sends a reader there to find it.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.6.1

Toggle v2026.9.6.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.6.1: `.sycl` is a device source, and the criterion is the comp…

…iler (#574)

* 2026.9.6.1: `.sycl` is a device source, and the criterion is the compiler (#573)

`SourceKind::Device` documents itself as a GRAPH ROLE -- "not scanned, no BMI,
compiled by a device compiler mcpp does not drive" -- explicitly so the table
does not grow a row per vendor. Until now every extension in it was also a
dialect an ordinary C++ compiler would refuse, so the distinction had never
been tested. `.sycl` tests it: its content is ordinary C++ and nothing in the
file would tell a reader otherwise. What makes it a device unit is that it goes
to a second compiler with a device back end -- icpx, or a clang built with the
SYCL front end -- which mcpp does not drive and which does not accept C++20
modules.

One row, and nothing else in the engine changes.

THE ALTERNATIVE, AND WHY IT WAS REJECTED. A constrained glob could have carried
`.cpp`. Then one extension would mean two things depending on which glob
matched first, and the seam a device build is written around -- device code
reaches the program only through an `extern "C"` header -- is legible precisely
because the file name says which side of it a unit is on.

THE ADDITION IS INERT ON EXISTING BUILDS, and the two conditions hold jointly:
device extensions are absent from `default_source_globs`, so no glob widens;
and a file with one of these extensions named in `sources` was a hard error
before the row existed, so nothing silently switches role.

e2e 613 is five sections and two of them are the ones that make the other three
a measurement:

  * section three is the control -- the SAME CONTENT named `.cpp` must still be
    compiled as C++ and must NOT reach the build program. A test naming only
    `.sycl` would pass on a table that had started classifying by content.
  * section five asserts the WHOLE default glob list rather than the absence of
    one name, so a future addition cannot pass this file while changing what a
    default build compiles.

Section four is the variant switch applied to the new row: `--no-accel` takes
the constrained glob out, which is what makes the CPU half of a seam reachable
without editing the manifest. Its first draft asserted that a `.sycl` in an
UNCONSTRAINED glob is refused; that was wrong and the run said so -- an
extension the table names is accepted wherever it is globbed, and the
compatibility guarantee is section five, not a refusal.

Chapter 20 gains the table of lanes -- which rule drives which compiler, which
payloads it needs, and what `[build] accel` says -- in both languages, and its
"Not implemented" section is corrected: the whole-target shape is no longer
among the missing, because a SYCL toolchain is now reached THROUGH the island
rather than beside it.

The rules themselves ship in `mcpp:plugins` 0.2.0: `mcpp.rules.sycl` drives the
`xim:dpcpp` payload, `mcpp.rules.hip` treats HIP on the NVIDIA platform as the
header layer over the CUDA runtime that it is, and `mcpp.rules.spirv` gains the
glslc route now that `xim:shaderc` publishes one.

Unit 105/105 (the new case verified to run by name, not by the suite being
green); e2e 606, 609, 610 and 613 pass against this binary.

* docs(plan): round 4's landed rows, the six corrections, and the verification script

Statuses for what has merged (the two payloads, the dpcpp repair, the SYCL
runtime adapter) and section 7.5 for what the work changed about the plan:
Z3 withdrawn because nothing consumes those two payloads yet; N1's second
criterion replaced because the run showed it was measuring a rule that does not
exist; the dpcpp repair that took two attempts and what CI's closure check was
actually saying; the `-lstdc++` rewrite; `compat.sycl-runtime`, which was not
in the plan and without which the round does not close; and why the round is
five pull requests rather than three.

The round-4 verification script carries the split the sandbox forces: a subos
has 14 entries in /dev and none is /dev/nvidia*, so it asserts that the link is
complete and the device result is asserted on the host.

* docs(plan): the round-4 verification script

Its shape is decided by what a sandbox cannot answer: `xlings subos --sandbox`
has 14 entries in /dev and none of them is /dev/nvidia*, so any program that
reaches for a GPU reports no device whatever the build did. The script asserts
that the LINK is complete in the sandbox -- payload resolved, rule ran, object
produced, runtime closure satisfied -- and asserts the device result on the
host. A criterion naming the device result in a sandbox would be unreachable by
construction rather than a statement about the ecosystem.

Section G names each soname the SYCL farm had to gain and why, so a farm that
silently loses one is caught: `libumf.so.1` (without which the runtime loads
and enumerates nothing), `libstdc++.so.6` (which compat.cudart deliberately
does not farm) and `libz.so.1` (which only a fresh runner found).

* docs(plan): the dpcpp correction took three attempts, and the second broke every consumer

The regression was not visible from the payload: `sycl-ls` reported the GPU in
attempts two and three alike. What differed was whether a CONSUMER of the
payload still worked, and only rebuilding the example showed it. A non-empty
DT_RUNPATH on a payload library switches off the inherited DT_RPATH of whatever
loaded it, so the CUDA adapter stopped seeing the artifact's own farm.

Also records compat.sycl-runtime 2026.09.07, which moves the driver hop from
the user's manifest into the adapter: which back end the SYCL runtime dlopens
is the runtime's business, not the project's.

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>

v2026.9.5.4

Toggle v2026.9.5.4's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.5.4: the fast path compares every declared build-program input (

…#570)

* chore: remove the remaining decorative markers from comments and documentation

The sweep that accompanied 2026.9.5.3 covered docs, README.md, CHANGELOG,
the engine sources, tests, examples and the workflow files. It did not
cover bench/, tools/, scripts/, mcpp.toml or README.zh-CN.md, which still
carried the warning and star markers in comments and prose.

Program output keeps its markers: a line a user reads on a terminal is
neither documentation nor a comment.

README.zh-CN.md also gains the Cortex-M row its English counterpart has,
and its status column now reads the same words that one does.

* fix(build): the fast path compares every declared build-program input

`rerun_if_changed("data/table.csv")` states that the program must run
again when that file's content changes. The project-level fast path skips
prepare_build when no source is newer than build.ninja, and prepare_build
is where the program's cache is read; the check the fast path did run
asked only about glob path sets. A data file is neither under src/ nor
named with a C++ extension, so the mtime sweep cannot see it either.
Editing it therefore left the previous run's generated header in place:
`Finished dev in 0.00s`, and the program compiled the previous bytes.

The fast path now compares the three kinds of input the cache records: a
glob's path set, a declared file's content hash, and a declared
environment variable's value. `glob_inputs_stale` is renamed
`program_inputs_stale` because the name was the reason the other two were
never asked about.

e2e 612 uses the program's output as its criterion -- a stale header and
a fresh one make the binary print different strings -- and runs the other
half of the control: with nothing touched, the next build still takes the
fast path, which "always rebuild" would also pass the first assertion
with. Verified failing on 2026.9.5.3 and passing here.

Found by mcpp.tools.embed, the first non-rule member of mcpp:plugins: it
writes a data file into a header while the build program runs, so it
submits no action and falls entirely on this path.

Documentation for both languages, CHANGELOG, and version 2026.9.5.4.

* fix(prepare): a version conflict on a package with no C++ module says so

Two versions of one package are reconciled by mangling the secondary
copy's module names. The two branches that refuse name both versions and
who asked for them; the third reported only that the package declares no
named C++ module to rewrite, which is a true statement about a package
the reader never asked to be staged and says nothing about the conflict
that brought it there. A C package reaches it: compat.vulkan-runtime
compiles one C file, so a manifest that pins one version of it while a
dependency asks for another got a message about modules.

It now names both versions, both requesters, and what to do.

examples/10 moves its pin to compat.vulkan-runtime 2026.09.06, the
version compat.vulkan asks for since mcpp-index#350. The example is the
consumer that found this.

* fix(examples): 10 pins compat.vulkan-runtime 2026.09.07

The version compat.vulkan asks for since mcpp-index#351, where a soname
carried by two installed payloads is decided by symbol coverage rather
than by which store path sorts last.

* docs(plan): the second round's rows, and the two items the first document had left open

The heterogeneous-build plan gains the rows for this round: the declared
payload set and the candidate walk in the two runtime adapters, the fast
path comparing every declared build-program input, the diagnostic for a
version conflict on a package with no C++ module, and the first member of
the tools half of mcpp:plugins.

The multi-device document's section 12.4 listed pocl/lavapipe and the
rule package in the index as not done. Both are done, and neither took
more hours: conda-forge publishes Linux binaries for the two drivers, so
the work was repacking and closure verification rather than building
mesa, and the rules moved to their own repository and are published as
one package whose members features select.

* docs(plan): the C1 row names the module the round settled on

---------

Co-authored-by: speak-agent <x.d2learn.org@gmail.com>

v2026.9.5.3

Toggle v2026.9.5.3's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
2026.9.5.3: a feature-selected rule collection, mcpp.rules.* naming, …

…and a variant switch the fast path no longer replays (#566)

* feat: 2026.9.5.3 -- a feature-selected rule collection, mcpp.rules.* naming, and a variant switch the fast path no longer replays

A host-module package contributes every module interface unit among its
feature-resolved sources, the lib root first; only listed sources take
part. This is what lets mcpp:plugins carry mcpp.rules.cuda and
mcpp.rules.spirv, selected by features. The rule packages leave examples/;
examples 09 and 10 consume the index package. The prefix is mcpp.rules.*
for rules and mcpp.tools.* for utilities; mcpp.build.* is the engine's own
module family and the specification is corrected accordingly.

The graph header records whether --accel/--no-accel chose the variant, and
the fast paths decline a graph an override wrote: a plain build after
--no-accel used to report Finished in 0.00s and run the CPU variant.

The device-source table is by compiler, not by vendor: 18 extensions,
none in the default globs, all a hard error before.

Chapter 20 is renamed to Heterogeneous Builds; docs 05 and 07 gain the
collection and naming sections in both languages.

* style: remove decorative symbols from documentation, comments and workflow comments

Table cells whose value was carried by a symbol alone now carry a word
(yes, no, partial, planned); three comment lists that distinguished
accepted from refused forms keep the distinction in words.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>

v2026.9.5.2

Toggle v2026.9.5.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(accel): version floors, a probe channel, constrained device glob…

…s, and a CUDA example that takes two routes (#563)

* feat(doctor): read the device toolkit from a payload before the host, and state the driver relation without probing for it

## The report was answering about the wrong toolkit

`mcpp self doctor` located `crt/host_config.h` through `CUDA_PATH`, `CUDA_HOME`,
`/usr/local/cuda` and `/usr/include` — host locations only. A toolkit installed
through xlings is the one a build will use, and it is usually the newer one:
measured here, a 12.9 payload states `gcc <= 14` and a 13.3 payload `gcc <= 15`,
where this machine's distribution CUDA 12.0 states `gcc <= 12`. The report said
`gcc 13 exceeds the bound of 12` about a toolkit the build was not using.

Payload stores are now searched first. Both of them: mcpp keeps its own under
`<mcpp home>/registry/data/xpkgs`, and `xlings install` writes to
`<xlings home>/data/xpkgs` — 191 packages in one and 211 in the other on this
machine, with the CUDA components only in the second. The host locations remain,
last, because a machine with a distribution toolkit and no payload is real.

Reading changes accordingly:

    before   warning: cuda will refuse this host compiler: gcc 13 exceeds
                      the bound of 12 stated in /usr/include/crt/host_config.h
    after    ok       cuda accepts this host compiler (gcc 13 <= 15)

## The driver relation, and why the acquisition is not here

A device runtime must not be newer than the driver it runs against. Measured on
a driver serving CUDA 12.4: the 13.3 payload compiles and links cleanly and then
fails at the first allocation with "CUDA driver version is insufficient for CUDA
runtime version", while the 12.9 payload prints the right answer.

`mcpp::toolchain::driver_accepts_toolkit` states when one version may meet
another, including that minor-version compatibility makes 12.9 fine against a
driver serving 12.4 — the case a naive "toolkit <= driver" check would have
refused. Five unit tests, including that either side unknown makes no claim.

The acquisition is NOT here. Asking a machine which driver it has means running
a vendor's tool, and `tests/unit/test_runtime_contract.cpp` refuses exactly that
in `src/`. It caught the first revision of this change, which launched one. The
rule predates this work and it is right: a core that learns to run one vendor's
probe learns to run four. Those numbers will reach the report as declarations —
a toolkit payload stating the driver it needs, and the package that owns the
host driver stating what the host has — which is the rule-package channel.

## Verified

`tests/e2e/602_device_toolkit_payload_first.sh` fabricates a payload store whose
header states `gcc <= 41`, a bound nothing real would state, and asserts the
report reads it. The control is the half that matters: without the payload store
the same command must not report 41, or the assertion would pass against a
doctor that hardcoded it.

100 test binaries pass, including the contract test that rejected the earlier
revision.

* feat(example): the CUDA example takes its toolkit from a payload, not from the host

The example shipped in 2026.9.5.1 wrote `-L/usr/local/cuda/lib64` in its
manifest and probed `/usr/local/cuda/bin/nvcc` and `/usr/include/crt/host_config.h`
in its rule package. It was the project's own demonstration of how to do this,
and what it demonstrated was reaching for the host.

The project now names the toolkit:

    [xlings.workspace]
    "xim:cuda-nvcc"   = "12.9.86"
    "xim:cuda-cudart" = "12.9.79"

and the rule resolves it with `mcpp::xpkg_dir`, building the whole invocation
from what it finds — compiler, include directories, and library search paths
through `mcpp::link_search`. The manifest names libraries and no locations.
Host paths remain in the rule as a last fallback so a machine with only a
distribution toolkit still builds.

⚠️ THE PAYLOAD'S HEADERS HAVE TO BE NAMED. nvcc adds
`<its own directory>/../include` by itself, and on the 12.x line that holds
`crt/` but not `cuda_runtime.h` — which is in the `cuda-cudart` component. The
first revision of this change left it out: nvcc resolved `cuda_runtime.h` from
/usr/include and then read the HOST's `crt/host_config.h` beside it, and the
build failed with the host toolkit's complaint (`clang version must be less than
15`) while using the payload's compiler. Naming the payload include directories
is what makes "uses the payload" true rather than nearly true.

Verified: `mcpp run` prints `12 24 36 48`, and

    $ mcpp build -v | grep -c '/usr/local/cuda\|/usr/bin/nvcc|-I/usr/include'
    0

nvcc and both include directories resolve under
`registry/data/xpkgs/xim-x-cuda-*`. 100 test binaries pass.

* feat(build): version floors, so a machine's limits are known before anything is compiled

Some facts about a machine bound what may be built for it, and the failure when
they are ignored arrives late. Measured on a driver serving CUDA 12.4: a binary
built with the 13.3 toolkit compiles cleanly, links cleanly, and fails at the
first allocation with "CUDA driver version is insufficient for CUDA runtime
version" — a message naming neither the toolkit nor the driver.

Both numbers are knowable beforehand. What mcpp must not do is go and ask a
vendor's tool for them, and `tests/unit/test_runtime_contract.cpp` refuses
exactly that in `src/`. So the numbers arrive as declarations.

A package states what it needs:

    [[runtime.requirements]]
    kind  = "version-floor"
    value = "cuda.driver >= 12.0"

A package that established a fact about this machine — at install time, which
is where probing belongs — states it:

    [runtime]
    provides = ["cuda.driver=12.4"]

They are compared when capabilities are bound, and a short machine is refused
before anything is compiled, reporting `version-floor-unmet`.

## No vendor vocabulary reaches the engine

`src/build/version_floor.cppm` reads a name, a relation and a version and knows
nothing about what any of them mean. `cuda.driver` is data passing through, no
new manifest keys were needed — `kind` was already a free string — and a
backend mcpp has never heard of compares the same way. That is asserted with a
name no backend uses.

## A floor nobody answered is silent

A machine that never declared what it has is not a machine that fails the floor;
it is one nobody asked. Turning "we do not know" into "no" is the failure mode
this exists to avoid, so it is the third case in the e2e rather than a comment.

Without that control the first case would also pass against an engine that
refused every version-floor requirement.

## Verified

`tests/e2e/603_version_floor.sh`, three halves:

    PASS: refused, naming the requirement, both versions and who stated the fact
    PASS: a met floor builds
    PASS: a floor with no stated fact is silent, for a name no backend uses

Eight unit tests over the parsing and the comparison, including that `>=` is
never read as `=` — both spellings live in string lists, and reading a floor as
a fact would turn a requirement into its own satisfaction. 101 test binaries
pass.

* test(e2e): give 317 the window its bound needs, and run 602 where the report exists

Reaching "five consecutive short failures" costs at least 1.25 s (four
restart delays of 250 ms and five 50 ms polls). A two-second pause left
150 ms per spawn, and a loaded macOS runner exceeded it twice in a row on
main while the same code passed twice on this branch. The pause is now
five seconds, so the property is decided by the code.

602 declared no platform, and on Windows the doctor does not emit the
device-toolkit section at all: the payloads are Linux builds and the bound
a Windows toolkit states is an _MSC_VER range the report does not read.
It also ran the doctor in a fresh home, which provisioned a full toolchain
into the temporary directory: 229 s and 1.4 GB per run. It now requires
unix-shell and runs under MCPP_OFFLINE.

* fix(config): offline mode skips the first-use sandbox bootstrap

--offline promises never to touch the network. In a home that had never
been used, load_or_init cloned the package index through `xlings self
init` and installed ninja and patchelf through `xlings install` before any
command ran: measured, 26 s and 126 MB under MCPP_OFFLINE=1.

The three network-bound steps are now skipped under offline mode, the skip
is announced once per process, and the completion markers are still
written from what is on disk, so a bootstrapped home loses nothing. The
commands that need the tools report their absence through check_base_init.
e2e 604 asserts the fresh-home behaviour and, as its control, that a
bootstrapped home announces nothing.

* feat(build): the probe channel, and the device axis as a build program sees it

A rule package is the thing that knows how to ask a machine what it has,
and the engine is the thing that must not. Two directives close that gap:
`mcpp:fact=<name>=<version>` states what the program established about the
machine, `mcpp:floor=<name> >= <version>` what the package needs of it. Both
fold into the runtime declarations a manifest could have carried itself, so
the existing version-floor check reads one list; it now runs a second time
after the root's build program, which is where a rule package speaks from.
Protocol 7.

MCPP_ACCEL carries the resolved device axis (`--accel` / `--no-accel` over
`[build] accel`) to build programs, so an architecture set is written once,
in the manifest. The same value now feeds the `cfg(accelerator = "...")`
layer key, a field that was declared, documented and never written. The
axis also enters the fingerprint and disqualifies the fast path: measured
before, `mcpp build --no-accel` after a device build finished in 0.00 s and
handed back the device build.

* feat(build): a source glob may carry the accel it is for, and the build narrows to it

`[build] sources` accepts `{ glob = "...", accel = "..." }` entries. The glob
joins the plain list, so every reader keeps working; the constraint is
resolved in prepare after feature application. A constrained glob that
matches nothing is refused, naming it. Under a build that asks for no
accelerator the glob is excluded through the same `!` mechanism feature gates
use, which is how one project yields its CPU-only variant. Under a build
that targets something the constraint is not within, the build is refused
naming the glob and both sides (`accel-mismatch`). Device-kind files the
effective set matches reach the build program as MCPP_DEVICE_SOURCES; the
engine has no compile rule for them and the rule package turns each into an
action. Unit tests cover the table form and its refusals; e2e 606 measures
the four outcomes with a backend nothing knows.

* docs(plan): record the engine batch, the fingerprint drift, and the local 168 reading

* feat(build): what a second compiler needs, and the device axis on run and test

Three gaps a rule package that drives a compiler mcpp did not resolve fell
into, each measured on the CUDA example.

**The flags mcpp passes to its own compiler.** `MCPP_TOOLCHAIN_SYSROOT` and
`MCPP_TOOLCHAIN_BINUTILS_DIR` state the `--sysroot` and the `-B` directory,
empty when mcpp passes none. They are not `MCPP_TARGET_SYSROOT`, which is a
tier fact and empty on a hosted target: under a sub-OS the C library is not at
`/usr/include` and the assembler is not at `/usr/bin`, so a compiler that mcpp
did not place fails at the first `#include`:

    crt/host_config.h:218: fatal error: features.h: No such file or directory

`hipcc`, `-fsycl-host-compiler` and any generator that compiles what it emits
have the same gap, so the answer belongs to the engine. Both read from the
single producer that already decides them for the engine's own command lines;
`gcc::binutils_prefix_dir` now states the `-B` guard once, where three copies
of it stood and one said so in a comment.

**A dynamic build-program helper on Linux gets `DT_RPATH`.** The driver's
default is the new tag, and a runpath is consulted only for the helper's own
needed libraries. A build program that opens a host library at run time then
fails one hop later: measured, `dlopen("<sentinel>/lib/libcuda.so.1")`
answered `libdl.so.2: cannot open shared object file` while the very
directories that hold it sat in the helper's RUNPATH. The artifacts mcpp links
already carry DT_RPATH for this reason. The link policy is part of the helper's
cache identity, so an older helper is rebuilt rather than replayed.

**`--accel` / `--no-accel` on `run` and `test`.** The axis existed on `build`
only, so a project could build its CPU-only variant and not run it. Both verbs
take the same two flags, with `--no-accel` travelling as the same explicit
sentinel, and both bypass their fast path when either is given — a cached
artefact was built for whatever axis the last build used.

e2e 607 covers the chained-action shape a device link needs: an artifact-role
action's output consumed by an object-role action, with the intermediate
absent from the link line.

* refactor(doctor): the device toolkit report belongs to the package that owns the tool

`mcpp self doctor` grew a CUDA section: it located a toolkit payload, read
NVIDIA's `crt/host_config.h` for the host-compiler bound, and parsed
`nvcc --dryrun` to name a back-end stage the tool could not reach. Every one of
those readings was correct, and none of them belonged to the engine.

The repository already states the rule as an invariant. `test_runtime_contract`
refuses a vendor name beside a probe launch in `src/`, and the reason is that
an engine which learns to run one vendor's tool learns to run four: the second
backend arrives as a second section, the third as a third, and the engine
acquires a table of tools it must keep current with releases it does not
control.

The same answers are now produced where the tool is known — the rule package
the project imports — and reach mcpp as declarations through the build
program's channel, which the engine compares without knowing what any of the
names mean. Nothing is lost: the CUDA example reports the driver relation, the
host-compiler bound and the unreachable stage before its first compile, and the
version floor refuses a build the machine cannot run.

`test_core_vendor_probes` states the property the removal establishes, over
comment-stripped sources so that recording a vendor's name in a comment stays
possible. It carries its own denominator: an enumeration that found fewer than
a hundred files is a broken scan rather than a clean result.

e2e 602 is removed with the section it tested. Its subject was the doctor
report, not a build, and the same preference — payload before host — is what
the example's rule package now exercises end to end.

* feat(example): two routes for the CUDA example, and the pairings it refuses

The example took one route — nvcc driving the project's own compiler — and
that route has two constraints neither the engine nor the project controls.
Both now produce a sentence before anything is compiled, and a second route
exists that has neither.

**clang is the primary route.** `[toolchain] default = "llvm@22.1.8"` and the
device unit is compiled by the same compiler as the rest of the project
(`-x cuda --cuda-path=<payload>`). No second host compiler, no host-compiler
bound, no CUDA host header in the way. `MCPP_EXAMPLE_CUDA_ROUTE` selects the
other route, and the rule declares `rerun_if_env_changed` for it.

**nvcc is the alternate, and it refuses two pairings by name.** The host
compiler must satisfy the bound nvcc states in its own `crt/host_config.h`: the
rule uses the project's toolchain when it fits, otherwise a `xim:gcc` payload
the project declared, otherwise a refusal naming the declaration to add.
Measured — GCC 16 under nvcc 12.9 fails inside GCC's own `<type_traits>` even
with `-allow-unsupported-compiler`, which admits a compiler one step past the
bound and not a standard library two majors newer.

The second pairing is a toolkit older than the C library. Toolkit 12.9's
`crt/math_functions.h` redeclares the C23 functions `cospi`, `sinpi` and
`rsqrt` for the host without `noexcept`; glibc 2.41 and later declare them with
it, and since C++17 that is part of the function type. The compile stops with
six `exception specification is incompatible` errors naming a glibc header and
a CUDA header, and no decision. The rule reads the C library's
`bits/mathcalls.h` through `mcpp::toolchain_sysroot()` and states the pair it
cannot have, naming the 13.x toolkit as the way out.

**A CPU implementation behind the same seam.** `src/cpu/saxpy.cpp` is selected
by `cfg(not(accelerator = "cuda"))` while the `.cu` carries the accel its glob
is for, so `mcpp build --no-accel` compiles one and `mcpp build` the other,
with no hand-written condition on either side.

Measured on an RTX 4080, driver 550.144.03 reporting CUDA 12.4: `mcpp run` and
`mcpp run --no-accel` both print `12 24 36 48`, from different artifact
directories, and the second contains no `cudaMalloc`. The nvcc route is not
exercisable there — 12.9 meets the driver and not the C library, 13.3 meets the
C library and not the driver — and both refusals are the ones above.

* fix(build): key a package's device sources by its root, not by its bare name

Two packages in one graph may share a bare name and differ only by namespace —
that is what namespaces are for — and the map that carries device sources to a
build program was keyed by the bare name at both its write site and its two
read sites. A collision would hand one package's device sources to the other's
build program, and nothing would report it: the receiving program would emit
actions for files it does not own, and the owner would emit none.

Keyed by the package root instead, which is unique by construction. Found by
re-reading the diff before merge; no test covered it, and a test that did would
need two same-named packages in one graph, which is worth adding when a second
consumer of this map appears.

* release: 2026.9.5.2

The version annotations in the manual said 2026.9.6 while the release lands
today, so they name the version that actually carries them. `mcpp.toml` and
`MCPP_VERSION` move together, as `01_help_and_version.sh` requires.

The CHANGELOG's Unreleased section described the doctor's `nvcc --dryrun`
check as a shipped feature. That check left the engine in this same batch — the
reading was right and the place was wrong — so the entry is rewritten to say
where it went rather than to announce something the release does not contain.

* fix(build): "no accelerator" is the empty string, not the display spelling

`accel_str` prints `(none)` for an empty set so an ABI tag reads as a
sentence. `resolvedAccel` handed that spelling on as a value, and two readers
were wrong at once:

- A build program saw `MCPP_ACCEL=(none)` while the manual promised an empty
  string, so a rule package asking whether there is an accelerator got a yes
  and a backend named `(none)`. This reached every project that never
  mentioned an accelerator, not only those passing `--no-accel`.
- The fingerprint's own guard, `if (!accel.empty())`, was true for every
  project, appending `#accel=(none)` to builds that had asked for nothing —
  the opposite of what the comment beside it says.

Measured with a build program that wrote the value to a file, which is the only
way to see it: mcpp shows a build program's stdout only when it exits non-zero.

e2e 605's fourth section was titled "--no-accel empties both the variable and
the layer" and measured only the layer, which is how this passed a suite it was
meant to be covered by. It now asserts both, and the fixture prints the value
delimited — an empty warning is indistinguishable from an absent one.

* docs(plan): batch ④ landed, and three readings the engine batch produced

The adapters and the rename are in mcpp-index#347; the plan table says so and
records the three findings that came out of measuring them — the display
spelling that leaked into a value, the two upstream couplings the CUDA recipes
now carry, and the path-index namespace behaviour that made local verification
look like a resolution failure.

The verification script travels with the plan rather than living only in a
scratch directory: it is the thing that found the `(none)` defect, and it is
what the release is checked with inside a sandbox.

* refactor(example): the CUDA rule package moves into the mcpplibs namespace

The rule is content this ecosystem wrote rather than anyone's upstream, so
`mcpplibs` is its namespace by the same rule the rest of the index follows, and
the module follows the namespace: `mcpplibs.rules.cuda`.

It also makes the package publishable. The index entry for it points at this
same directory inside an mcpp release tarball — the shape `grpcgen` already
uses — and a descriptor's identity has to match the manifest it points at, so a
package that stays in an `example` namespace can only ever be copied into the
index rather than referenced there.

Measured: `mcpp run` and `mcpp run --no-accel` both print 12 24 36 48 after a
clean rebuild.

* docs(design): §12 — what the implementation overturned, produced, and left

Five places the plan was wrong and two defects the implementation created and
fixed, each with the reading that settled it. Also what a consumer writes after
this batch, and the list of what remains with the reason each item is not done
rather than a silence that reads as done.

* feat(build): an object action's outputs reach a static library, not only a linked image

A package whose device code is its point declares `kind = "lib"`. Until this,
the actions its build program emitted were dropped — with a warning, and with
the archive coming out containing none of them:

    build.mcpp action 'cuda:wkv' has role = "object" but this build produces
    no executable, shared library or test binary to link its outputs into

Measured on llama.cpp's CUDA backend, which is 305 `.cu` files behind exactly
such a target: every action reported that line and the build succeeded, having
produced a library with no device code in it.

The archive rule already consumes `lu.objects`; a static library was simply
absent from the predicate that decides which link units an object action
attaches to. The objects an action produced belong there for the same reason a
compiled `.cpp`'s do — a target's content is what it was told to contain.

e2e 608 asserts the member list of the archive rather than the exit status: an
`ar` handed nothing still writes a well-formed archive and reports success.
This is the engine half of the multi-device design's C-6.

* docs(plan): T5.1 as a gate paid for itself, and what it left

The framework batch's first item found the engine gap C-6 named and nothing
had measured, and it got as far as compiling before stopping on a payload
version rather than on anything about the design. Both readings are recorded
with what would close them.

* fix(example): the clang route passes NVIDIA's own libc++ escape

A device unit that includes <cuda_runtime.h> stops on

    crt/host_defines.h:67: error: "libc++ is not supported on x86 system"

whenever the toolchain is LLVM, which is the toolchain the clang route exists
for. The guard is `#if defined(__CUDACC__) && … && defined(_LIBCPP_VERSION)`,
and clang defines `__CUDACC__` when it compiles CUDA itself, so the refusal —
which is about nvcc's host pass — lands on a compiler it was not written about.

The escape hatch is upstream's own and is passed only on the clang route: nvcc
really does break against libc++, and nothing here weakens that refusal.

Found on ggml's CUDA backend. The example's own kernel never showed it because
a bare kernel includes no toolkit header at all — which is worth knowing about
the example: it exercises the plumbing, not the headers.

* docs(plan): where T5.1's CUDA lane actually stops, and what it proved on the way

The chain the design is about — axis, narrowed glob, device source list, rule
package, actions, static archive, link — runs end to end and produced 48 device
objects. What stops it is a four-way payload matrix that has no satisfiable
point on this machine, and none of its four sides is mcpp's.

* test(e2e): 188's no-consumer case needs a target that does not exist, not a library

A `kind = "lib"` target used to serve as "nothing that takes an object
action's outputs", because a static library did not take them. It does now, so
that fixture stopped exercising the case and the suite reported it — which is
the whole value of running it.

The one shape that still leaves an object action with nowhere to go is a target
behind an inactive `required_features`: it is absent from the build entirely.
A control runs the same project with the feature on and asserts the diagnostic
is gone, so the assertion cannot pass on a build that stopped emitting it for
everyone.

* docs(design): §12.5 — what the framework gate actually measured

T5.1 did what a gate is for: it found the engine gap C-6 named and nothing had
exercised, and it carried the whole chain to 48 device objects before stopping
on a four-way payload matrix with no satisfiable point on this machine. The
matrix is written out with the reading for each side, and the three general
findings it produced are recorded with where they went.

* docs(example): the clang route does pass one flag, and the README should say which

The README claimed "no CUDA host header in the way", which stopped being true
the moment a device unit includes <cuda_runtime.h>. It says what the flag is,
why the guard fires on a compiler it was not written about, and why this
example's own kernel never showed it.

* test(verify): section E asserted a silence that a fresh sandbox gives for free

Rehearsed inside a sandbox against the previous release — the one that still
had the doctor's toolkit reader — and section E reported ok. It was watching
for a section `self doctor` only prints when a CUDA toolkit is installed, and
a fresh sandbox has none, so the assertion could not fail there.

It now reads the engine binary for the names the removed code carried.
`crt/host_config.h` appears twice in 2026.9.5.1 and not at all in 2026.9.5.2,
so the check discriminates: the control run against the older binary fails with
that exact count. The source-level property stays where it belongs, in
`test_core_vendor_probes`.

Also fixed while writing it: `grep -c` prints 0 and exits 1 when it matches
nothing, so `|| echo 0` appended a second line, `[` said "integer expression
expected", and the section reported ok having skipped every comparison.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
Co-authored-by: speak-agent <x.d2learn.org@gmail.com>

v2026.9.5.1

Toggle v2026.9.5.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat: accelerator support — device translation units, the accelerator…

… dimension of artifact identity, and the host-compiler pairing (#559)

* feat(source-kind): classify device translation units

A device translation unit is compiled by a vendor device compiler that mcpp
does not drive directly, and no such compiler accepts C++20 modules. The new
kind states the graph role — never scanned for imports, never produces a BMI —
and says nothing about the language, which is what lets one kind cover CUDA
C++, HIP, and device dialects that are not C++ at all.

Device headers are classified as headers rather than device units because
their role is the header role: they are not compiled, and editing one can
change what the graph should be. Leaving them in Other is how a project would
observe an edited kernel header producing no rebuild.

The default source glob is deliberately unchanged. Widening it would make a
published package that vendors a device source it builds elsewhere start
compiling it on the next mcpp upgrade, which its author cannot fix because
that version's tarball has already shipped.

* feat(cfg): accelerator as the first multi-valued target-side layer

One build can enable several accelerator backends at once, which is what an
inference framework shipping CUDA and ROCm device code in one artifact
requires, so this layer holds a set rather than the single answer the other
five hold.

The comparison is membership, and it is membership everywhere rather than only
inside any(...). Letting a combinator change the meaning of its operand would
make all(accelerator = "cuda", accelerator = "rocm") unsatisfiable rather
than "both backends are enabled". Membership everywhere keeps any, all and
not pure boolean combinators, and a single-backend build still answers its own
backend true and every other backend false.

The multi-valued case differs in exactly one function, Ctx::layer_matches, so
adding a second such layer later is a row rather than a second code path.

* docs(design): how DPC++ and NVHPC attach to the toolchain model

The open-source intel/llvm DPC++ compiler is clang: one driver, GNU-style
flags, the same BMI format. Its NVIDIA and AMD backends are configured into
the compiler itself through buildbot/configure.py --cuda and --hip, so the
Codeplay plugins the v4 text referred to are a property of Intel's binary
distribution rather than of DPC++. It therefore attaches as a payload variant
of the existing LLVM family plus a provider capability, and requires no new
value in the closed Family enumeration.

nvc++ attaches as a device rule compiler rather than a toolchain family. The
HPC SDK user guide states C++17 with GNU compatibility as its standard, and a
compiler without C++20 modules cannot host projects that import std.

* docs(design): the host-dependency axis for accelerators, and what xim already provides

The governing rule needs no accelerator-specific extension. A toolchain is
mcpp's contract and must come from a payload; a library the program links is
the program's own business. Applied here, nvcc and the CUDA runtime libraries
are payload material because NVIDIA's redistributable manifests permit it,
while libcuda.so.1 cannot be: the driver EULA forbids third-party
redistribution and the userspace library is in ABI lockstep with the kernel
module.

The lower half of that axis already exists in xim as libcuda-host-link, a
sentinel package that installs one symlink to the host library and gives every
GPU consumer a single source of truth instead of a reimplemented ldconfig
probe. The upper half does not: there is no CUDA toolchain package. Compiling
needs only the nvcc and cudart components rather than the whole toolkit, which
is the same per-component selection the hermetic Bazel CUDA rules make.

* feat(pack): the accelerator dimension of a prebuilt artifact's identity

An artifact that carries device code constrains which devices can run it, and
until now nothing recorded that. Two builds differing only in the compute
capability they targeted produced identical tags, linked cleanly, and failed at
the first kernel launch.

The dimension is a parallel structured field rather than a tag segment. The tag
is dash-joined and parsed from the end, and an architecture list is a set;
joining it in would put separators inside a segment of a string whose triple
already carries a variable number of dashes. tag_check compares it all the
same, so there is one comparator and two storage locations rather than a second
comparator.

Comparison is membership widened two ways, each matching a mechanism that
exists in the hardware rather than a convenience. A family target covers its
own major with an equal-or-higher minor, which is what makes publishing one
artifact per generation viable instead of one per chip. An embedded portable
form covers everything at or above its floor, which is how NVIDIA's own
guidance for a distributed binary works. AMD has neither a portable form nor a
reason for one, leaves the floor empty, and obtains the same reach through
family targets on the architecture side; an empty floor therefore widens
nothing.

* feat(toolchain): read a device toolkit's host-compiler bound instead of tabulating it

nvcc refuses host compilers newer than a bound that moves with every CUDA
release, and the bound is not documentation: it is a preprocessor guard in the
toolkit's own crt/host_config.h. A table transcribed into mcpp would be a copy
of one release, correct until the next and silently wrong afterwards. Reading
the guard means a toolkit mcpp has never heard of still answers.

mcpp supplies the host compiler, so it is in a position to know both sides of
this pairing before either compiler runs. The value is the earlier refusal:
forwarding -ccbin and letting nvcc fail produces a diagnostic naming a compiler
the user did not choose and a bound they cannot see.

The parse is narrow on purpose. A header it cannot make sense of yields no
bound, and no bound means no check, because a refusal invented from an
unreadable file is worse than the failure it would have prevented.

Verified against the CUDA 12.0 header on the development machine: the guard
reads __GNUC__ > 12 and the clang bound is stated as less than 15, which the
parse reports as 12 and 14.

* feat(doctor): report whether this host compiler can drive the device toolkit

nvcc refuses host compilers newer than a bound the toolkit states in its own
crt/host_config.h. The failure is late, the message names a compiler the user
did not choose, and the bound is invisible. Every other build system forwards
-ccbin and lets nvcc discover this; mcpp supplies the host compiler and can
answer before anything is compiled.

Reported rather than enforced. A project that compiles no device code is
unaffected by an incompatible pair, and refusing its build would be a false
alarm.

Observed on the development machine, where the toolkit is CUDA 12.0 and the
resolved host compiler is gcc 13:

    Checking device toolkit
    warning: cuda will refuse this host compiler: gcc 13 exceeds the bound of
             12 stated in /usr/include/crt/host_config.h.

* docs: the ecosystem packages that make accelerator support usable

An engine capability with no packages behind it does not exist for a developer.
The design and the plan now name what has to land in xim and mcpp-index, in
dependency order, with what each package validates.

Two entries carry most of the weight. AdaptiveCpp comes first among the
payloads because its OpenMP backend runs SYCL kernels on the CPU, which makes
the whole of form B verifiable end to end on a machine with no GPU. llama.cpp-m
comes first among the libraries because one package exercises three of the
seven user roles at once, it has no Python packaging layer to confuse the
result, and it already exists in the ecosystem, so the step is adding a backend
rather than adopting a large project.

The two header-only libraries, CCCL and CUTLASS, are listed before anything
with a binary artifact: they carry no size or variant-matrix problem and are
therefore the first real libraries reachable by the source-distribution path.

* feat: carry the accelerator dimension from descriptor to refusal

The comparison added earlier had no reader: nothing populated the field, so it
was empty everywhere and accepted everything. This connects both ends.

An artifact declares what device code it carries in a separate descriptor
field, in one wire form that is also the diagnostic form, so what a publisher
writes is what a refusal prints back. A build declares what it targets through
[build] accel, overridden for one build by --accel, which is the relationship
--target already has with [toolchain]. --no-accel is not the absence of
--accel: it is an explicit request for none, which is what a user needs in
order to take the CPU-only variant of a package that also publishes device
builds.

A build that asks for no accelerator is satisfied by every artifact
vacuously. That is correct rather than permissive, and it is why a descriptor
lists its CPU-only variant first: the first accepted artifact wins, so an older
mcpp that does not know this field at all still selects the variant that runs
anywhere.

* feat: refuse a prebuilt artifact whose device code cannot run on the target

This is the failure the whole dimension exists to move. A library built for one
compute capability and consumed by a build targeting another linked cleanly and
failed at the first kernel launch, with a runtime message that names neither
the package nor the architecture either side expected.

The refusal now names both, and the remedy is the one that can work. The
generic advice to pin [toolchain] is actively misleading on this axis, because
no toolchain pin changes which GPU architecture a build targets; the device
case offers --accel and --no-accel instead. The published listing carries the
dimension too, since it travels beside the tag rather than inside it and a
listing showing only tags would print two indistinguishable lines for a package
publishing a CPU and a device build of one triple.

Observed:

    error: mcpplibs.gpuonly@0.1.0: no prebuilt artifact matches this toolchain.
      your toolchain : x86_64-linux-gnu-gcc16-libstdcxx16-c++23  accel=cuda12.8+{sm_86}
      published tags :
                       x86_64-linux-gnu  accel=cuda12.8+{sm_90f}
      closest is x86_64-linux-gnu, and it differs on:
        accel     needs cuda12.8+{sm_90f}, this build has cuda12.8+{sm_86}

The accompanying end-to-end test also covers the ordering rule: a descriptor
listing its CPU-only artifact first lets a build targeting an architecture no
device variant carries fall back to the portable one, which is the same
ordering that keeps an mcpp predating this field selecting a usable artifact.

* feat: a CUDA kernel behind a seam module, and the package-level declaration

The example is the design's form A end to end: a device translation unit that
never enters the module graph, an extern "C" interface free of standard-library
types, a seam module that turns that interface back into a C++ one, and a
build-rule package that owns every fact about nvcc's spelling.

The seam is the part worth reading. Its reason for existing is not that nvcc
rejects modules; it is that this is the one place a backend can be exchanged
without any consumer changing, and the one place a cfg(accelerator = ...)
section has somewhere to apply.

The rule package reads the toolkit's own host-compiler bound and selects a
compiler that satisfies it, because mcpp's payload is routinely newer than what
a given toolkit accepts. On the machine this was verified on the payload is gcc
16.1.0 and the toolkit refuses anything above gcc 12, so the rule selects
clang++-14 and says so.

Verified on an RTX 4080 with CUDA 12.0 and driver 550.144.03: mcpp run prints
12 24 36 48, which is the saxpy the kernel computed on the device.

[package] accelerators is declared alongside, mirroring platforms: a statement
of intent and a CI-matrix hint. It is deliberately a different field from an
artifact's accel, because a declaration is written by hand and can be
aspirational while the artifact field is measured from the build and is what a
consumer is refused against.

The example's README records the one hop that is not closed: [xlings] deps
provisions a payload and puts its bin/ on PATH, which is what a payload
providing a program needs, while a payload providing a library also needs its
lib/ on the artifact's runtime search path. Until an mcpp-index package
declares that directory for the driver sentinel, the run needs it supplied.

* docs: accelerators chapter, and the manifest keys it introduces

Chapter 20 states the two shapes an accelerator toolchain takes, says which one
mcpp implements, and gives the reasoning for the three decisions a reader will
otherwise find arbitrary: why device sources are excluded from the default
glob, why the seam module is an architectural unit rather than a workaround for
nvcc's lack of module support, and why the accelerator dimension travels beside
the compatibility tag instead of inside it.

The manifest reference gains [build] accel and [package] accelerators, kept
apart on purpose: one is measured from a build, the other is written by hand.

* docs(design): what implementation overturned, and what the ecosystem enforced

Five design statements did not survive contact with the code. The cfg semantics
were wrong in a way that made the design more complex than it needed to be:
letting any(...) change the meaning of its operand would have made a
conjunction of two backends unsatisfiable rather than "both are enabled", and
membership everywhere is both correct and one rule fewer for a user to learn.
The compatibility risk was real but on a different path than the one checked.
Form B needs no new toolchain family. nvc++ belongs to form A. And the host
compiler bound should be read from the vendor's header rather than tabulated.

The ecosystem refused three shortcuts on its own: mcpp's runtime closure check
rejected linking the host CUDA runtime, allow_host_libs was not enough to get
past it, and the index already documented that [xlings] deps materialises for
the root project only. Every one of those constraints was already there and
every one was right.

Two mistakes were mine and both are shapes this document warns about. A first
draft of the index package reimplemented a host probe that the sentinel's own
recipe forbids reimplementing, and which xim's hostlib module records as having
been got wrong three times out of four. The same draft carried an extra library
pattern justified by a reason written from memory; measurement showed PTX JIT
works without it.

* chore(release): 2026.9.5.1

* feat(examples): reach the driver through the index rather than an env var

The example previously needed LD_LIBRARY_PATH pointing at the xim sentinel,
because nothing declared a runtime search directory for the driver's userspace
library. compat.cuda-runtime now does, so the dependency is an ordinary one and
mcpp run works unaided.

The two packages divide the question by layer. libcuda-host-link in xim owns
where the host's copy is, so no consumer reimplements an ldconfig probe.
compat.cuda-runtime in mcpp-index owns how a built artifact reaches it, which
needs a directory on the runtime search path and is therefore something only an
mcpp package can declare.

Verified end to end on an RTX 4080: mcpp run prints 12 24 36 48 with no
environment variables set.

* docs(design): the cross-repository ordering, walked once and closed

The design states that the ordering between repositories is hard. This records
what walking it actually cost.

The index package used only keys the current floor supports, so it did not have
to wait for the engine to ship and could land independently. What it did have
to wait for was not the merge but the artifact publish: xlings consumes the
index as artifact:<sha>, so there is a workflow between "merged" and
"resolvable". The example's dependency resolves only after that workflow is
green on the merged commit.

What remains genuinely blocked is the other direction: an index package that
uses one of the new engine keys cannot land until the engine ships. This one
was lucky in needing none of them.

* docs(accelerators): state the two boundaries rather than leave them as gaps

--accel is a build option alongside --static and --toolchain, not repeated on
run, test or pack. Those read the manifest key like any other build input.

mcpp pack does not emit the accel field, and the reason is the point rather
than an omission. The field states what an artifact carries, and mcpp does not
yet compile device code itself, so it has nothing to measure. Writing the
declared value into a field whose meaning is measured would make the identity
lie in precisely the way the dimension exists to prevent.

* docs(design): correct the cfg semantics at the point they are stated

Section 14.1 recorded that the original three-line semantics were overturned
during implementation, while section 3.4 still stated them. A correction that
lives only in a later section is the shape where one layer covers another and
a reader takes the first one they reach.

The implemented rule is one line: membership, everywhere. The combinators do
not change the meaning of their operand, which is what makes a conjunction of
two backends mean "both are enabled" rather than being unsatisfiable.

* docs(design): record which stages shipped, and why decision 4 did not

Two of the decisions taken at design time did not land, and a staged plan that
does not say so lets a reader assume otherwise.

Decision 4 turned out not to be implementable as stated. Refusing whenever one
capability has several providers in the graph would also refuse the OpenBLAS
and MKL case, which the manual documents as normal use; refusing only when
their symbols overlap requires reading object files that do not exist yet at
binding time. What is missing is a declaration that a capability's providers
are mutually exclusive, which only the capability's definer knows. That is a
new manifest key and a design decision rather than an implementation, so it is
recorded as such instead of being quietly skipped.

Decision 3 depends on the device-target primitive, and this round compiles
device code through a build-rule package, which the staged plan already placed
before that primitive.

The completion table states the evidence for every stage that did ship.

---------

Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>