Skip to content

Commit 090203d

Browse files
authored
the judge is a bracketed token, and the brackets are part of the match (#446)
`compat.py` recorded a member as correctly "refused" when its output matched the bare word `interface-not-provided`, under a comment claiming the judge was "the engine's own refusal code and NOT a string in the diagnostic". It was a string in the diagnostic. What the comment was reaching for is true but was not what the code did: mcpp prints `[interface-not-provided]` in brackets, the way it prints `E0006`, and the token is an entry in docs/50's table -- a machine interface this index may read rather than a sentence that may be rewritten. The brackets are what make it one. Read as a bare word it is a hyphenated phrase an ordinary compile error can contain: an upstream message quoting a manifest key, or a member's own diagnostic naming the condition. A member that merely failed would then be published as correctly refused, which moves the figure in the direction that flatters the engine. `classify_failure` is split out of `measure` so the rule has a criterion that runs without a toolchain, a network or a member, and `compat.py selftest` states five: a bracketed token is a refusal, the same token as prose is a failure, an ordinary compile error is a failure, a member that ran and failed its tests still built, and the same output without a runner is not evidence it built. Restoring the previous matcher fails the second and only the second. The step runs in `lint` rather than in openkal-compat.yml: its cases are about the rule, not about any member, and the measurement it guards takes four hours to reach.
1 parent cf36e1e commit 090203d

3 files changed

Lines changed: 89 additions & 12 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,16 @@ jobs:
397397
# release that republished an old tag would leave no other trace.
398398
- name: Lint duplicate versions
399399
run: lua5.4 tests/check_duplicate_versions.lua pkgs/*/*.lua
400+
# The compatibility measurement's classifier decides a PUBLISHED figure
401+
# -- which members "run", which "build", and which were correctly
402+
# "refused" -- and the only thing that exercised it was the four-hour
403+
# openkal-compat matrix, on real members, where a misclassification is
404+
# a number nobody can trace back to a rule. `classify_failure` is split
405+
# out so it can be stated in a second, here, with no toolchain and no
406+
# network. It runs in `lint` rather than in that workflow because its
407+
# cases are about the rule and not about any member.
408+
- name: The compatibility classifier agrees with its own rules
409+
run: python3 tests/openkal/compat.py selftest
400410
# ── Single-source-of-truth grammar check ─────────────────────────
401411
# `mcpp xpkg parse` uses EXACTLY the resolver's parser, so what
402412
# passes here is what builds for users of the pinned MCPP_VERSION.

‎docs/openkal-compat.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ judged by, and a score of that shape asks the engine to supply what the
4545
environment does not have -- which is how a build tool ends up simulating an
4646
operating system it is not running on. The summary counts the two apart.
4747

48-
The judge is mcpp's own refusal code (`interface-not-provided`) and not a
49-
string in the diagnostic: the member DECLARED the requirement and the graph
50-
answered. Matching prose would let a member fall into this status for saying
51-
the right words in an ordinary compile error. No member carries it today --
48+
The judge is mcpp's reason token `[interface-not-provided]`, matched WITH its
49+
brackets. mcpp prints it in the refusal's own message the way it prints
50+
`E0006`, and it is an entry in `docs/50`'s token table -- a machine interface
51+
this measurement may read, rather than a sentence that may be rewritten. The
52+
brackets are part of the match: read as a bare word, the token is a hyphenated
53+
phrase an ordinary compile error could contain, and a member that merely failed
54+
while quoting it would be recorded as correctly refused. No member carries it today --
5255
`requires-interfaces` reaches the index with mcpp 2026.9.20.1 and no
5356
third-party descriptor states it yet.
5457

‎tests/openkal/compat.py‎

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,17 @@ def measure(member: str, target: str, pins: dict) -> dict:
208208
if proc.returncode == 0:
209209
status = "runs" if can_run else "builds"
210210
return {"status": status}
211+
return classify_failure(out, can_run)
212+
213+
214+
def classify_failure(out: str, can_run: bool) -> dict:
215+
"""Classify a member whose build or test command exited non-zero.
216+
217+
Separated from `measure` so the rule below has a criterion that runs
218+
without a toolchain, a network or a member: `compat.py selftest`. The
219+
distinction it draws decides a published figure, and until this split
220+
the only way to exercise it was a four-hour matrix.
221+
"""
211222
built = can_run and re.search(r"^\s*Running bin/", out, re.M) is not None
212223
if built:
213224
return {"status": "builds", "diagnostic": first_diagnostic(out)}
@@ -221,23 +232,32 @@ def measure(member: str, target: str, pins: dict) -> dict:
221232
# not have --- which is how a build tool ends up simulating an operating
222233
# system it is not running on.
223234
#
224-
# THE JUDGE IS THE ENGINE'S OWN REFUSAL CODE AND NOT A STRING IN THE
225-
# DIAGNOSTIC. mcpp emits `interface-not-provided` when a package's
226-
# `[kernel-abi] requires-interfaces` names something the resolved
227-
# implementation does not provide; the member DECLARED the requirement and
228-
# the graph answered. Matching prose instead would let a member fall into
229-
# this status for saying the right words in an ordinary compile error.
235+
# THE JUDGE IS A REASON TOKEN, AND THE BRACKETS ARE WHAT MAKE IT ONE.
236+
# mcpp prints `[interface-not-provided]` in the refusal's own message, in
237+
# brackets, the way `E0006` does (docs/50 §"One token is also printed by
238+
# `mcpp build` itself"); the token is an entry in that page's table, which
239+
# is to say a machine interface this index may read, rather than a
240+
# sentence that may be rewritten.
241+
#
242+
# THE BRACKETS ARE PART OF THE MATCH AND NOT DECORATION. Read as a bare
243+
# word, `interface-not-provided` is a hyphenated phrase an ordinary
244+
# compile error could contain --- a member whose own diagnostic quoted a
245+
# manifest key, or an upstream error message using the same words, would
246+
# be recorded as correctly refused when it had simply failed. Requiring
247+
# the brackets is what distinguishes "the graph answered this member's
248+
# declared requirement" from "the output happened to say so".
230249
#
231250
# No member carries this status today: `requires-interfaces` reaches the
232251
# index with mcpp 2026.9.20.1 and no third-party descriptor states it yet.
233252
# The path is here rather than added later because the figure it changes is
234253
# the one this file publishes, and a member that starts declaring its
235254
# requirements should not have to wait for this file to catch up.
236-
if re.search(r"\binterface-not-provided\b", out):
255+
if "[interface-not-provided]" in out:
237256
return {"status": "refused", "diagnostic": first_diagnostic(out)}
238257
return {"status": "fails", "diagnostic": first_diagnostic(out)}
239258

240259

260+
241261
def cmd_run(args: argparse.Namespace) -> int:
242262
pins = load_toml(os.path.join(HERE, "pins.toml"))
243263
members_file = load_toml(os.path.join(HERE, "members.toml"))
@@ -330,6 +350,48 @@ def cmd_select(args: argparse.Namespace) -> int:
330350
return 0
331351

332352

353+
def cmd_selftest(_args: argparse.Namespace) -> int:
354+
"""Exercise `classify_failure`, whose distinctions decide a published
355+
figure and which no other check reaches.
356+
357+
Each case is one sentence about the rule, and each would have passed
358+
before the rule it pins was written the way it is now.
359+
"""
360+
refusal = (
361+
"error: package 'x' requires the kernel-abi interface 'openkal.space',\n"
362+
" which openkal-windows (14 interfaces) does not provide. "
363+
"[interface-not-provided]\n")
364+
# The same words, as prose, with no brackets: an upstream error quoting a
365+
# manifest key, or a member's own diagnostic naming the condition. Before
366+
# the brackets were part of the match this was recorded as a correct
367+
# refusal, which is to say a member that merely failed improved the figure.
368+
prose = ("error: no member named 'interface_not_provided'\n"
369+
"note: the interface-not-provided condition is described in "
370+
"the README\n")
371+
ran = " Compiling x v0.1.0\n Running bin/x\n test failed\n"
372+
373+
cases = [
374+
("a bracketed token is a refusal",
375+
classify_failure(refusal, False)["status"], "refused"),
376+
("the same token as prose is a failure",
377+
classify_failure(prose, False)["status"], "fails"),
378+
("an ordinary compile error is a failure",
379+
classify_failure("error: no such file\n", False)["status"], "fails"),
380+
("a member that ran and failed its tests still built",
381+
classify_failure(ran, True)["status"], "builds"),
382+
("the same output without a runner is not evidence it built",
383+
classify_failure(ran, False)["status"], "fails"),
384+
]
385+
bad = 0
386+
for name, got, want in cases:
387+
ok = got == want
388+
bad += not ok
389+
print(f"{'ok ' if ok else 'FAIL'} {name}: {got}"
390+
+ ("" if ok else f" (expected {want})"))
391+
print(f"\n{len(cases) - bad} passed, {bad} failed")
392+
return 1 if bad else 0
393+
394+
333395
def main() -> int:
334396
parser = argparse.ArgumentParser(description=__doc__,
335397
formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -344,8 +406,10 @@ def main() -> int:
344406
check.add_argument("--members", nargs="*")
345407
select = sub.add_parser("select")
346408
select.add_argument("files", nargs="*")
409+
sub.add_parser("selftest")
347410
args = parser.parse_args()
348-
return {"run": cmd_run, "check": cmd_check, "select": cmd_select}[args.command](args)
411+
return {"run": cmd_run, "check": cmd_check, "select": cmd_select,
412+
"selftest": cmd_selftest}[args.command](args)
349413

350414

351415
if __name__ == "__main__":

0 commit comments

Comments
 (0)