Skip to content

2026.8.26.2 — the answer was already resolved, and nothing consulted it - #512

Merged
Sunrisepeak merged 8 commits into
mainfrom
fix/resolved-but-not-consulted
Aug 26, 2026
Merged

2026.8.26.2 — the answer was already resolved, and nothing consulted it#512
Sunrisepeak merged 8 commits into
mainfrom
fix/resolved-but-not-consulted

Conversation

@Sunrisepeak

@Sunrisepeak Sunrisepeak commented Aug 26, 2026

Copy link
Copy Markdown
Member

Analysis: .agents/docs/2026-08-26-resolved-but-not-consulted.md

Two defects, and they are not the family 2026.8.25.x closed. There the predicate asked a narrower question than the one it was given. Here the predicate asked the right question, got the right answer, and the answer never reached a decision.

8.25.x family this one
predicate wrong right
answer wrong right
defect the criterion the answer is not wired to the decision

Both are a field recorded with no decision-side reader — invisible when reading the criterion, visible the moment a user asks "you already knew; why do I have to say it again".


A — requires was checked, never applied

prepare.cppm has a block titled "the toolchain, resolved now that the graph exists". It scans pkg.manifest.provides to decide the compiler. It does not scan requires_ — that is collected a thousand lines later and used only to reject the outcome.

Measured on 2026.8.26.1, llvm@22.1.8 already installed:

$ mcpp build                              # global default gcc@16.1.0
  error: `openkal-llvm-runtime@0.1.3` requires the compiler to be `llvm`.
         Select that compiler …  mcpp toolchain default llvm
$ MCPP_TOOLCHAIN=llvm@22.1.8 mcpp build
  Finished dev [unoptimized + debuginfo] in 1.02s

Nothing was missing. The remedy printed changes the default for every project on the machine because one project's dependency asked.

⭐⭐ Selecting it writes nothing, and that is the position rather than a rule. resolve_target_toolchain has exactly two call sites, both downstream of the graph — so the first-run install-and-persist branch and all three write_default_toolchain calls sit on a branch this selection no longer enters (its condition is !tcSpec.has_value()).

before after
machine with a gcc default refuse, demand a global change install/use llvm, config.toml untouched
machine with nothing installed install gcc → persist gcc → refuse first-run branch not entered; install llvm directly

Refusal now happens in exactly one situation: the project stated its own compiler in [toolchain] or [target.X]. ⚠️ In that situation the global default is not what is being used, so the old mcpp toolchain default llvm advice could not fix the failure it was printed under; it now names the statement that decided. Two packages requiring different families is an error naming both, not a pick by traversal order.

B — the tier gate asked about the identity, not the request

$ mcpp build --target aarch64-linux
  error: target 'aarch64-linux-gnu' is registered but not yet supported (planned)
$ mcpp build --target aarch64-linux-musl
  Finished dev [unoptimized + debuginfo] in 0.99s

parse fills a missing env segment lexically so the identity stays total; envExplicit exists precisely to record that the fill was a fill — and the gate did not read it. The question asked was aarch64, Linux; the question answered was aarch64-linux-gnu, and the message quotes a triple absent from the command. examples/06-openkal-cross teaches the short spelling for three platforms; the fourth was the one that could not be written.

A declined segment is now completed against the vocabulary, lexical-default-first so the rule retires itself when aarch64-linux-gnu graduates. parse() is unchanged — identity must stay lexical, total and host-independent, and its unit test is untouched. Blast radius is two (arch, os) cells; every other lexical default already names a supported row.

C — two diagnostics stopped lying

unknown target 'riscv64-linux' was false: riscv64-linux-musl is registered (planned). The fill had produced a row outside the vocabulary. That path also had no refusal::record, so --format json reported reason: "other" for a branch with a perfectly good name.

D — the query gave two answers for the C library

cLibrary said glibc/payload while layers[].c-abi said musl/graph, in one document. The artifact settles it — static, no interpreter, no DT_NEEDED, 11 openkal symbols. Adds cLibrary.suppliesTarget; a field added, not renamed, and mode not widened, per docs/11 §6.


Criteria

  • e2e 299–303, classifying through --format json rather than substring search, wired into the target matrix invariants layer — all four build hosts.
  • ⭐ 301's criterion is the sha256 of ~/.mcpp/config.toml, not a successful build: a build can succeed and rewrite the user's default, and that is the behaviour being removed.
  • ⭐ 299's second half is the control — x86_64-linux must still be gnu. Testing only aarch64 would make "send every bare -linux to musl" look correct.
  • 7 new TripleRequest.* unit tests, including one sweeping the whole vocabulary: every supported row reachable from its own spelling.
  • ⚠️ 301/302 need two compiler families; granted a declared skip by reason, with linux-x86_64 as the denominator required to actually run them.

Docs: 03, 11, 16 + zh mirrors. Version 2026.8.26.2 (two sites; bootstrap pin untouched).


Added after the first review pass

⚠️ "The right position" turned out not to be the whole rule. write_default_toolchain has three call sites; only one is gated on !tcSpec.has_value(). The Windows first-run diversion and the MSVC repair are both reachable, so a bare Windows box building one llvm-requiring project would have written llvm as the machine's default. tc_origin_may_persist(TcOrigin) gives the rule a name both sites call and a unit test states — the config-hash e2e runs where a toolchain is already configured and never reaches either branch.

⚠️ The capability row's remedy was the convention row's. "Depend on a package that supplies this target's system" cancels a convention pin; a capability pin survives it by construction, and the sentence directly above already said so. Split by row kind, with an assertion in 302.

⚠️ [toolchain] default = "system" is left alone. It means "the PATH compiler, whatever it is" — a deliberate opt-out of the payload model, and its family is not knowable from the spec.

compiler.chosenBy = {origin, requiredBy, replaced} added to why toolchain --format json, so "why llvm" is answerable without parsing the status line. 301 asserts on it rather than on prose.

⚠️ Three defects in my own tests, found by reading: jq's // returns its right side when the left is false, so a boolean field read as absent (windows-x86_64 reported it missing while it was there — use has()); 301 measured its baseline in the runner's start directory instead of the project under test, so every assertion passed while the requirement had changed nothing; 281 asserted the old global remedy.

Verified at the artifact, not the exit code

$ mcpp build --target aarch64-linux
$ file target/aarch64-linux-musl/*/bin/test4
  ELF 64-bit LSB executable, ARM aarch64, statically linked

Compatibility

No build that previously succeeded changes behaviour. Where the graph declares a compiler, the previous outcome was a check_requirements refusal — those builds did not succeed. --target aarch64-linux and riscv64-linux were refusals. x86_64-linux / x86_64-windows / riscv64-none / aarch64-macos complete exactly as before, held by a unit test that sweeps the whole vocabulary. The machine interface only gains fields.

Ecosystem

All seven openkal repos built from this branch — 29 legs, each logging under review: mcpp 2026.8.26.2 (from fix/resolved-but-not-consulted).

Two defects the previous release's family does not cover. There the predicate
asked a narrower question than the one it was given; here the predicate asked
the right one, got the right answer, and the answer never reached a decision.
Both are a recorded field with no decision-side reader.

`requires` was checked, never applied. `prepare.cppm` has a block titled "the
toolchain, resolved now that the graph exists"; it scans `provides` to decide
the compiler and does not scan `requires_`, which is collected a thousand lines
later and used only to reject the outcome. Measured with llvm@22.1.8 already
installed: `mcpp build` refused and advised `mcpp toolchain default llvm` — a
global change, for one project's dependency — while `MCPP_TOOLCHAIN=llvm@22.1.8
mcpp build` finished in 1.02s.

Selecting it writes nothing, and that is where the decision sits rather than a
rule to remember: `resolve_target_toolchain` has two call sites, both after the
graph, so the first-run install-and-persist branch and all three
`write_default_toolchain` calls are downstream. On a machine with no toolchain
the branch is not even entered — its condition is `!tcSpec.has_value()`.
Refusal now happens only where the project stated its own compiler, and the
advice points at that statement; two packages requiring different families is
an error naming both.

The tier gate asked about the identity, not the request. `parse` fills a missing
env segment lexically so the identity stays total, and `envExplicit` exists to
record that it was a fill — the gate did not read it. `--target aarch64-linux`
refused as `aarch64-linux-gnu` (planned) while `aarch64-linux-musl` built, and
`riscv64-linux` was reported `unknown` although the family is registered. A
request that declined the segment is now completed against the vocabulary, with
the lexical default tried first so the rule retires itself. `parse()` is
unchanged: identity must stay lexical, total and host-independent.

Two diagnostics stopped lying. `unknown target` no longer fires when the
(arch, os) group is non-empty, refusals quote the spelling the user wrote, and
the unknown path finally records a code instead of reporting `other`.

`why toolchain --format json` gave two answers for the C library — `cLibrary`
said glibc/payload while `layers[].c-abi` said musl/graph, and the artifact
(static, no interpreter, no DT_NEEDED, 11 openkal symbols) settled it. Adds
`cLibrary.suppliesTarget` rather than renaming a field, per docs/11 §6.

Criteria: e2e 299-303 classify through `--format json` and run on all four
build hosts via the target-matrix invariants layer. 301's criterion is the
sha256 of config.toml, not a successful build — those can both be true, which
is the behaviour being removed. 299's second half is the control that
`x86_64-linux` is still gnu.
…the claim

`compiler.chosenBy` carries the selection origin into the machine interface:
a consumer asking why a build resolved llvm had to parse the status line, which
is the substring matching that document exists to remove.

Three test defects found by reading, not by failing:

  - the capability-row refusal offered "depend on a package that supplies this
    target's system" — a remedy a capability pin ignores by construction, and
    the sentence directly above it already said so. Split by row kind.
  - 303 read `suppliesTarget` through jq's `//`, which returns its right side
    when the left is null OR FALSE. `false` is an answer; it read as absent,
    and windows-x86_64 reported the field missing when it was there.
  - 281 asserted the old global remedy, which the only refusal that still
    reaches it cannot be fixed by.

303's third half stacks a musl c-abi over the host's own target and not every
host stacks that; granted by reason, with linux-x86_64 as the denominator.
Two branches persist a default — the Windows first-run diversion, whose
condition is `tcSpec.has_value()`, and the MSVC repair, whose gate is "mcpp
chose this itself". A compiler chosen by `requires = ["mcpp:compiler=…"]`
satisfies both, so a bare Windows box building ONE llvm-requiring project would
have written llvm as the MACHINE's default and handed it to every later project
that asked for nothing.

Found by reading the two call sites against the rule, not by a run: it needs a
Windows machine with no toolchain. `tc_origin_may_persist` gives the rule a
name both sites call and a unit test can state — the e2e that hashes
config.toml runs where a toolchain is already configured and never reaches
either branch.
`[toolchain] default = "system"` means "whatever is on PATH" — a deliberate
opt-out of the payload model. Replacing it with a payload because a dependency
named a family defeats what was asked for, and mcpp cannot even tell whether
the requirement is already met: the family of a PATH compiler is not knowable
from the spec. check_requirements reports the mismatch against what the driver
turned out to be, which is the only place that answer exists.
An unnamed branch reports `other` in the machine interface, and this release
exists partly because one of those had a perfectly good name. Both paths in
`resolve_required_family` — an unrecognised family, and a family with no
version to use — now record `compiler-requirement-conflict`, whose comment is
widened to the four situations it covers: what a consumer does about each is
the same.
301's whole claim is that `config.toml` is byte-identical. macOS has no
`sha256sum` — it is `shasum -a 256` — so on that host the first draft would
have compared "" with "", which are equal, and reported the criterion met
while measuring nothing. A build can succeed AND rewrite the configuration;
that is the pair this half exists to separate.

Picks whichever hasher exists, fails when neither does, and refuses an empty
result on either side.
@Sunrisepeak
Sunrisepeak merged commit 35223c5 into main Aug 26, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants