You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build plan. Rewritten 2026-09-09 as a handoff against main at d59bcd2. The comments below are history: the two 2026-08-17-and-earlier implementation sketches describe architectures that have since been replaced, and should be read for motivation only. This body is the current statement of what to build.
The problem
A user compat bound on a non-upgradable stdlib steers the julia choice: a julia version is compatible with exactly the stdlib versions it bundles, so a stale [compat] LinearAlgebra = "~1.10" quietly pins the whole toolchain to julia 1.10. The resolution is correct. Nothing says why.
Measured instance in the first comment below: a project with julia = "1.6" and LinearAlgebra = "~1.10" resolves julia to 1.10.11 rather than 1.12.6, and the only signal the user gets is a ⊼ character (bin/resolve.jl:774).
This is the last asymmetry in the diagnostics story. An unsatisfiable resolve now gets a machine-verified proof per conflict, complete coverage of the cheapest fixes, and a per-action answer for every fix it does not offer. A satisfiable but surprising resolve gets one character.
Scope note: the surprise only exists when resolving over julia versions. Pkg pins julia to the running version by default, where the same stale bound produces an ordinary unsatisfiable resolve. But #62 names holdbacks as the mechanism for the version half of pkg> why — "why is X at 1.2 and not 1.9" — which is not julia-specific and is a headline Pkg feature.
Prior art — a spec, not a patch
archive/holdbacks-prototype (commit c9e8b0c, "explain resolutions that succeeded and still surprised") is a complete prior implementation: src/Holdback.jl (361 lines), test/holdback.jl (230 lines), plus bin/resolve.jl wiring and docs. It settles a lot of design and is worth reading in full.
It cannot be ported. It targets src/Diagnose.jl and a Fact/UserCompat/Bound vocabulary that #105 replaced with clauses, and — the trap — its tests assert wording that has since been retired:
prototype asserts
current idiom
your compat restricts Stats to 10
your compat leaves Stats 10
julia 12 works with Stats only at 12
directed verbs read off the consequent: requires / constrains / leaves no version of
The symmetric "works with … only at" phrasing is exactly what #105 replaced, because a bound has no direction and which way it is said has to be chosen when printed (src/Diagnostics.jl:2154). Copy those assertions and you reintroduce two dead vocabularies. Take the prototype's structure and edge cases; re-derive every string.
Two sibling commits on the same branch are prior art for other roadmap items, not for this one: 1539af3 (goals) and 3993297 (the presentation toolkit and --explain). Same caveat applies to both.
What to build
For a package p that resolved at class rank r when a better rank exists:
Probe. Assume p at a better rank on the solved instance. Satisfiable → nothing to explain. Unsatisfiable → continue.
Blame. Take a held-fixed MUS over the selector vocabulary — sat_mus(satx, fixed, selectors), the same call reason_core makes at src/Diagnostics.jl:1649. The result names the responsible statements as clauses.
Render. Those clauses print through the existing clause_phrase / range_phrase path, so a holdback's lines read in the same idiom as a conflict's and no new sentence layer is needed.
Everything in steps 1–4 exists and is tested. This is assembly, not new machinery.
Decisions to make first
None of these is settled, and each shapes the code:
Which packages get probed. julia always? every requirement resolved below its best? everything? behind a flag? A probe is a solve, so this is a cost decision as well as a UX one. The prototype's answer was max_probes = 8 with an explicit truncation line — see (4).
Output shape. Is a holdback Conflict-shaped — heading, Lines, a Fix with Actions — reusing action_phrase and printing inside the diagnosis? Or its own type printed beside it? The first buys the proof checker for free (see below); the second is easier to keep out of the unsatisfiable path.
Entry point. The prototype's was holdbacks(info, prob, sol, [pkgs]; by, max_probes). Does resolve surface them on success, or does a caller ask separately? Pkg integration checklist: conventions, API mapping, rollout #62 needs an answer to write Pkg code against.
Verification — free, if the output is clause-shaped
test/proof_check.jl exports lines_are_true, chain_is_a_proof, proof_problems, claimed_lines and friends, and lines_are_true(sat, prob, clauses) is generic over clauses. If a holdback's lines are clauses over the same instance, they are proof-checked with no new checker: every line true of the universe the query left, and the claimed set contradictory.
That is a strong argument for decision (2) landing on "Conflict-shaped".
Harnesses to extend: test/proof_check.jl (the checker itself needs nothing), test/chain_coverage.jl (add holdback probes to the corpus loop), test/diagnostics.jl (report wording). The prototype's test/holdback.jl is the scenario list to port — STEERING and its siblings — with every occursin string rewritten.
Where things are
src/Diagnostics.jl — the diagnosis layer; reason_core (1729) and the selector plumbing (1793) are the models to follow.
src/UnsatCores.jl — sat_mus, including the held-fixed form (135).
src/Clauses.jl — the clause algebra and the sentence layer.
docs/src/theory/unsat-explanation.md — the contract a report owes. A holdback is subject to it.
bin/resolve.jl — the ⊼ marker (774) is the current signal; the prototype added --explain and automatic julia explanation here.
Docs target moved: the prototype wrote docs/src/diagnostics.md, which no longer exists. Docs are now index.md, api.md and theory/.
Definition of done
Holdbacks are computed, blamed, rendered and fix-verified for the measured steering instance and for the prototype's scenario set.
Every printed line passes lines_are_true against the instance the resolve ran on.
The problem
A user compat bound on a non-upgradable stdlib steers the julia choice: a julia version is compatible with exactly the stdlib versions it bundles, so a stale
[compat] LinearAlgebra = "~1.10"quietly pins the whole toolchain to julia 1.10. The resolution is correct. Nothing says why.Measured instance in the first comment below: a project with
julia = "1.6"andLinearAlgebra = "~1.10"resolves julia to 1.10.11 rather than 1.12.6, and the only signal the user gets is a⊼character (bin/resolve.jl:774).This is the last asymmetry in the diagnostics story. An unsatisfiable resolve now gets a machine-verified proof per conflict, complete coverage of the cheapest fixes, and a per-action answer for every fix it does not offer. A satisfiable but surprising resolve gets one character.
Scope note: the surprise only exists when resolving over julia versions. Pkg pins julia to the running version by default, where the same stale bound produces an ordinary unsatisfiable resolve. But #62 names
holdbacksas the mechanism for the version half ofpkg> why— "why is X at 1.2 and not 1.9" — which is not julia-specific and is a headline Pkg feature.Prior art — a spec, not a patch
archive/holdbacks-prototype(commitc9e8b0c, "explain resolutions that succeeded and still surprised") is a complete prior implementation:src/Holdback.jl(361 lines),test/holdback.jl(230 lines), plusbin/resolve.jlwiring and docs. It settles a lot of design and is worth reading in full.It cannot be ported. It targets
src/Diagnose.jland aFact/UserCompat/Boundvocabulary that #105 replaced with clauses, and — the trap — its tests assert wording that has since been retired:your compat restricts Stats to 10your compat leaves Stats 10julia 12 works with Stats only at 12requires/constrains/leaves no version ofThe symmetric "works with … only at" phrasing is exactly what #105 replaced, because a bound has no direction and which way it is said has to be chosen when printed (
src/Diagnostics.jl:2154). Copy those assertions and you reintroduce two dead vocabularies. Take the prototype's structure and edge cases; re-derive every string.Two sibling commits on the same branch are prior art for other roadmap items, not for this one:
1539af3(goals) and3993297(the presentation toolkit and--explain). Same caveat applies to both.What to build
For a package
pthat resolved at class rankrwhen a better rank exists:pat a better rank on the solved instance. Satisfiable → nothing to explain. Unsatisfiable → continue.sat_mus(satx, fixed, selectors), the same callreason_coremakes atsrc/Diagnostics.jl:1649. The result names the responsible statements as clauses.clause_phrase/range_phrasepath, so a holdback's lines read in the same idiom as a conflict's and no new sentence layer is needed.Everything in steps 1–4 exists and is tested. This is assembly, not new machinery.
Decisions to make first
None of these is settled, and each shapes the code:
max_probes = 8with an explicit truncation line — see (4).Conflict-shaped — heading,Lines, aFixwithActions — reusingaction_phraseand printing inside the diagnosis? Or its own type printed beside it? The first buys the proof checker for free (see below); the second is easier to keep out of the unsatisfiable path.holdbacks(info, prob, sol, [pkgs]; by, max_probes). Doesresolvesurface them on success, or does a caller ask separately? Pkg integration checklist: conventions, API mapping, rollout #62 needs an answer to write Pkg code against.(7 more packages resolved below their best version, not examined). That is the shape of footer drop "There may be more to say about some of these." #108 just removed, on State the whole theory of explaining an unsatisfiable resolve #104's principle that the page owes only what can be acted on. There is now a precedent, and it may cut against the prototype: consider recording the count on the returned value and printing nothing.Verification — free, if the output is clause-shaped
test/proof_check.jlexportslines_are_true,chain_is_a_proof,proof_problems,claimed_linesand friends, andlines_are_true(sat, prob, clauses)is generic over clauses. If a holdback's lines are clauses over the same instance, they are proof-checked with no new checker: every line true of the universe the query left, and the claimed set contradictory.That is a strong argument for decision (2) landing on "
Conflict-shaped".Harnesses to extend:
test/proof_check.jl(the checker itself needs nothing),test/chain_coverage.jl(add holdback probes to the corpus loop),test/diagnostics.jl(report wording). The prototype'stest/holdback.jlis the scenario list to port —STEERINGand its siblings — with everyoccursinstring rewritten.Where things are
src/Diagnostics.jl— the diagnosis layer;reason_core(1729) and the selector plumbing (1793) are the models to follow.src/UnsatCores.jl—sat_mus, including the held-fixed form (135).src/Clauses.jl— the clause algebra and the sentence layer.docs/src/theory/unsat-explanation.md— the contract a report owes. A holdback is subject to it.bin/resolve.jl— the⊼marker (774) is the current signal; the prototype added--explainand automatic julia explanation here.docs/src/diagnostics.md, which no longer exists. Docs are nowindex.md,api.mdandtheory/.Definition of done
lines_are_trueagainst the instance the resolve ran on.bin/resolve.jlexplains a held-back julia without being asked; the rest is behind a flag.docs/src/api.mddocuments whatever is public.🤖 Rewritten as a handoff with Claude Code.