From 41dbdd376242249d36189a1895f79e4120f9f473 Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 18:18:27 +0800 Subject: [PATCH 1/3] openkal 0.8.1: the kit reaches the specification the way a consumer does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE SPECIFICATION TEXT IS UNCHANGED. This release exists because openkal-kit 0.1.0 was published in a form that cannot be used. Its manifest read `openkal = { path = ".." }`, which is true inside this repository and true inside the tarball the package is published from. It is not true for a consumer. A project naming openkal-kit alongside any implementation reached the specification two ways and the build refused: error: dependency 'mcpplibs.openkal' is requested as both a path dep (by 'mcpplibs.openkal-kit@0.1.0') and a version dep (by 'mcpplibs.openkal-musl@0.4.0'). Pick one. The refusal is right. Two routes to one package are two copies of it, and two copies of a specification are two sets of its modules. ⚠️ NOTHING IN THIS ECOSYSTEM COULD HAVE FOUND IT BEFORE PUBLICATION. Every workflow substitutes working trees, so both routes become paths and agree. The defect requires an implementation that names the specification by VERSION, which is the published shape and precisely what no job reproduces. It was found by resolving the published packages in a sandbox. `include_dirs = ["../include"]` goes with it. That directory reaches out of the package into whatever sits above it; with the specification named by version it would be the tarball's own copy while the headers actually compiled against come from the resolved dependency --- two header sets for one contract. openkal publishes `include` as its own `include_dirs` and a dependency's public directories reach a consumer. Measured with the specification placed outside the package: the kit compiles and runs without the line. tools/run-kit-tests.sh gains a substitution it did not need before. While the kit named the specification by path it already pointed at this working tree; now that it names a version, these tests would have run against the PUBLISHED specification while claiming to test the one written here. The new substitution is asserted, as the two beside it are. The guard added to the conformance job is structural rather than behavioural, and deliberately so: it cannot reproduce the consumer's graph, but it can require the form that avoids it. Measured before pushing, with the released engine: the kit's tests build the specification, the implementation and the tests together and report `openkal-kit: every observation held`. --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ kit/mcpp.toml | 30 ++++++++++++++++++++++++++---- mcpp.toml | 2 +- tools/run-kit-tests.sh | 12 ++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 034c79b..bb11c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -730,6 +730,39 @@ jobs: # C++ modules in `namespace kal::kit` and exports no name beginning with # `kal_`; a C++ name is mangled to `_ZN3kal3kit…`, so an implementation # that linked the kit still passes `check-surface.sh --complete`. + # ⭐⭐ A PUBLISHED PACKAGE MAY NOT REACH ANOTHER BY PATH. + # + # kit/mcpp.toml is published as openkal-kit, out of this repository's own + # tarball. While it read `openkal = { path = ".." }` that was true here and + # true inside the tarball, and it made the package unusable with any + # implementation: a consumer naming both openkal-kit and an implementation + # reached the specification two ways --- by path from one, by version from + # the other --- and the build refused, correctly, because two routes to one + # package are two copies of it. + # + # ⚠️ NO CONTINUOUS INTEGRATION HERE COULD HAVE CAUGHT IT. Every workflow in + # this ecosystem substitutes working trees, so both routes become paths and + # agree. The defect appears only where an implementation names the + # specification by VERSION, which is the published shape and exactly what + # no job reproduces. It was found in a sandbox against the published + # packages, after release. + # + # This check is structural rather than behavioural for that reason: it + # cannot reproduce the graph, but it can require the form that avoids it. + - name: The kit names the specification by version + run: | + set -euo pipefail + if grep -nE '^openkal = \{[^}]*path' kit/mcpp.toml; then + echo "::error::kit/mcpp.toml reaches the specification by path." + echo " openkal-kit is published from this tarball, and a path" + echo " there is a second route to a package every consumer" + echo " already reaches by version." + exit 1 + fi + grep -qE '^openkal = "[0-9]' kit/mcpp.toml \ + || { echo "::error::kit/mcpp.toml does not name openkal by version at all"; exit 1; } + echo " the kit reaches the specification the way a consumer does: $(grep -E '^openkal = ' kit/mcpp.toml)" + - name: The kit, composed from the interfaces above if: matrix.implementation == 'openkal-linux' run: | diff --git a/kit/mcpp.toml b/kit/mcpp.toml index 1839cfe..2154f93 100644 --- a/kit/mcpp.toml +++ b/kit/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal-kit" -version = "0.1.0" +version = "0.1.1" description = "Facilities composed from openkal's interfaces. Not part of the specification, and structurally incapable of being mistaken for it." license = "Apache-2.0" authors = ["mcpplibs"] @@ -50,10 +50,32 @@ repo = "https://github.com/mcpplibs/openkal" kind = "lib" [dependencies] -openkal = { path = ".." } +# ⚠️⚠️ BY VERSION, NOT BY PATH, AND THE DIFFERENCE IS NOT COSMETIC. +# +# This line read `{ path = ".." }`, which is true inside this repository and +# inside the tarball this package is published from --- and which made the +# package unusable with any implementation. A consumer naming both openkal-kit +# and an implementation reached the specification two ways, and the build +# refused, correctly: +# +# error: dependency 'mcpplibs.openkal' is requested as both a path dep (by +# 'mcpplibs.openkal-kit@0.1.0') and a version dep (by +# 'mcpplibs.openkal-musl@0.4.0'). Pick one. +# +# Two routes to one package are two copies of it, and two copies of a +# specification are two sets of its modules. Measured in a sandbox against the +# published packages, which is the only place this could be observed: no +# continuous integration in this ecosystem resolves a published package, +# because they all substitute working trees by design. +openkal = "0.8.0" -[build] -include_dirs = ["../include"] +# ⚠️ NO `include_dirs` HERE EITHER. It named `../include`, which reaches out of +# this package into whatever happens to sit above it. With the specification +# named by version that directory is the tarball's own copy while the headers +# actually compiled against come from the resolved dependency --- two header +# sets for one contract. openkal publishes `include` as its own `include_dirs`, +# and a dependency's public directories reach a consumer; measured before this +# line was removed, with the specification placed outside this package. # AN IMPLEMENTATION, FOR THE TESTS AND FOR NOTHING ELSE. # diff --git a/mcpp.toml b/mcpp.toml index 8e044c8..c0373b1 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.8.0" +version = "0.8.1" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"] diff --git a/tools/run-kit-tests.sh b/tools/run-kit-tests.sh index a95f133..272a6d5 100755 --- a/tools/run-kit-tests.sh +++ b/tools/run-kit-tests.sh @@ -56,6 +56,16 @@ sed -i.bak -E "s|^${package} = \{ version = \"[^\"]*\"(.*)$|${package} = { path "$kit/mcpp.toml" rm -f "$kit/mcpp.toml.bak" +# ⚠️ AND THE KIT REACHES THE SPECIFICATION BY VERSION TOO, WHICH IT DID NOT USED +# TO. While that line read `path = ".."` it already named this working tree and +# needed no substitution. It now names a published version --- because a path +# there made the package unusable alongside any implementation --- so without +# this line these tests would run against the PUBLISHED specification while +# claiming to test the one written here, and a change to the specification would +# be invisible to them. +sed -i.bak -E "s|^openkal = .*$|openkal = { path = \"$here_native\" }|" "$kit/mcpp.toml" +rm -f "$kit/mcpp.toml.bak" + # ⚠️ ASSERTED RATHER THAN ASSUMED. A substitution that matched nothing leaves the # manifest naming a version, the resolver fetches a published implementation, and # the run reports on that one while appearing to report on this branch. @@ -63,6 +73,8 @@ grep -q "path = \"$impl_native\"" "$kit/mcpp.toml" \ || { echo "the implementation substitution matched nothing in kit/mcpp.toml" >&2; exit 2; } grep -q "path = \"$here_native\"" "$impl/mcpp.toml" \ || { echo "the specification substitution matched nothing in $impl/mcpp.toml" >&2; exit 2; } +grep -q "path = \"$here_native\"" "$kit/mcpp.toml" \ + || { echo "the specification substitution matched nothing in kit/mcpp.toml" >&2; exit 2; } echo "--- the kit's dependencies ---" sed -n '/^\[dependencies\]/,/^$/p;/dev-dependencies/,+2p' "$kit/mcpp.toml" From 87c9ba87262bb536c4b5330c65c44bc2b282b60e Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 18:20:08 +0800 Subject: [PATCH 2/3] The in-step check compares the contract, not the string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishing 0.8.1 turned every conformance row red: this is openkal 0.8.1 and openkal-linux is written against openkal 0.8.0. Nothing is wrong with either; they are not in step. Correct by the letter of the check and wrong by its meaning. Clause 8 forbids the specification from altering a declaration it has published, so within one major.minor series every implementation is written against the same contract; a patch releases packaging, wording, or a repaired example. Requiring the patch component to match would make each such release a migration across five repositories that changes no code in any of them --- and would do it to announce a change that, by clause 8, cannot have occurred. ⚠️ The minor component still counts. 0.8 → 0.9 is where interfaces are added and an implementation written for the earlier one does not provide them. That is the mismatch this check exists to name before the compiler reports it as a hundred missing declarations. Self-checked over four pairs: 0.8.1/0.8.0 and 0.8.0/0.8.0 pass; 0.9.0/0.8.0 and 0.8.1/0.7.0 are refused. --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb11c0e..fc53c6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -566,12 +566,32 @@ jobs: # empty string --- which named neither the cause nor the symptom. here="$(sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' mcpp.toml | head -1)" there="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)" + # ⭐ THE COMPARISON IS OF THE CONTRACT, NOT OF THE STRING. + # + # It compared the whole version and so refused 0.8.1 against an + # implementation written for 0.8.0 --- correctly by the letter and + # wrongly by the meaning. Clause 8 forbids the specification from + # altering a declaration it has published, so within one major.minor + # series the contract every implementation is written against is the + # same contract; a patch releases packaging, wording or a repaired + # example. Requiring the patch component to match would make every + # such release a five-repository migration that changes no code. + # + # ⚠️ THE MINOR COMPONENT STILL COUNTS. 0.8 → 0.9 is where interfaces + # are added, and an implementation written for the earlier one does + # not provide them; that is the mismatch this check exists to name + # before the compiler reports it as a hundred missing declarations. + series() { printf '%s' "$1" | cut -d. -f1,2; } if [ -n "$there" ]; then - if [ "$here" != "$there" ]; then + if [ "$(series "$here")" != "$(series "$there")" ]; then echo "::error::this is openkal $here and ${{ matrix.implementation }} is written against openkal $there. Nothing is wrong with either; they are not in step. Re-run after the implementation's branch has the matching version." >&2 exit 1 fi - echo "both describe openkal $here" + if [ "$here" != "$there" ]; then + echo "both describe openkal $(series "$here"); this is $here and the implementation names $there, which is the same contract" + else + echo "both describe openkal $here" + fi else impl_branch="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*{.*branch[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)" if [ -z "$impl_branch" ]; then From 0b9887eff20305652530d65d35b2532a3d30e00f Mon Sep 17 00:00:00 2001 From: speak-agent Date: Thu, 27 Aug 2026 18:23:09 +0800 Subject: [PATCH 3/3] Do not release the specification to repair a package beside it The kit fix does not change the specification, and this reverts the two commits that behaved as though it did. openkal's version returns to 0.8.0. It was bumped for one reason: openkal-kit is published out of this repository's tarball, so a repaired kit needs a tag that contains the repair, and 0.8.0 is already published and immutable. A bump is one way to obtain a tag. It is not the only one, and this index already establishes the other: a package's version key need not equal the tag it is published from --- grpc-plugin 1.83.0 comes from grpc-m's v1.83.0-4. openkal-kit 0.1.1 will therefore be published from a tag of its own. The in-step check returns to comparing the whole version. Relaxing it to the major.minor series is defensible on its own terms --- clause 8 forbids altering a published declaration, so a patch cannot change the contract --- but it was written here to accommodate a version change that should not have happened, and a fix should not carry a rule invented to excuse it. The check turned all five conformance rows red the moment 0.8.1 appeared, which is the check doing its job on a release that had no reason to exist. What remains is the kit's own repair: it names the specification by version rather than by path, it no longer reaches out of its directory for headers, and tools/run-kit-tests.sh substitutes the line that used to need no substitution. --- .github/workflows/ci.yml | 24 ++---------------------- mcpp.toml | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc53c6f..bb11c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -566,32 +566,12 @@ jobs: # empty string --- which named neither the cause nor the symptom. here="$(sed -n 's/^version[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' mcpp.toml | head -1)" there="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)" - # ⭐ THE COMPARISON IS OF THE CONTRACT, NOT OF THE STRING. - # - # It compared the whole version and so refused 0.8.1 against an - # implementation written for 0.8.0 --- correctly by the letter and - # wrongly by the meaning. Clause 8 forbids the specification from - # altering a declaration it has published, so within one major.minor - # series the contract every implementation is written against is the - # same contract; a patch releases packaging, wording or a repaired - # example. Requiring the patch component to match would make every - # such release a five-repository migration that changes no code. - # - # ⚠️ THE MINOR COMPONENT STILL COUNTS. 0.8 → 0.9 is where interfaces - # are added, and an implementation written for the earlier one does - # not provide them; that is the mismatch this check exists to name - # before the compiler reports it as a hundred missing declarations. - series() { printf '%s' "$1" | cut -d. -f1,2; } if [ -n "$there" ]; then - if [ "$(series "$here")" != "$(series "$there")" ]; then + if [ "$here" != "$there" ]; then echo "::error::this is openkal $here and ${{ matrix.implementation }} is written against openkal $there. Nothing is wrong with either; they are not in step. Re-run after the implementation's branch has the matching version." >&2 exit 1 fi - if [ "$here" != "$there" ]; then - echo "both describe openkal $(series "$here"); this is $here and the implementation names $there, which is the same contract" - else - echo "both describe openkal $here" - fi + echo "both describe openkal $here" else impl_branch="$(sed -n 's/^openkal[[:space:]]*=[[:space:]]*{.*branch[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' .impl/mcpp.toml | head -1)" if [ -z "$impl_branch" ]; then diff --git a/mcpp.toml b/mcpp.toml index c0373b1..8e044c8 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,7 +1,7 @@ [package] namespace = "mcpplibs" name = "openkal" -version = "0.8.1" +version = "0.8.0" description = "openkal: a portable kernel ABI specification. This package carries the normative declarations; implementations are separate packages." license = "Apache-2.0" authors = ["mcpplibs"]