Implement ADR-0078: single-candidate import resolution - #2531
Merged
steveklabnik merged 10 commits intoAug 18, 2026
Conversation
Two post-acceptance rulings, folded into Decision 4:
The --std-path flag is deferred to RUE-1586 rather than shipped. No present
workflow needs a deliberate override channel - every harness in this
repository overrides through the environment against non-vendoring
programs - and CLI surface shipped now is surface the distribution design
must later honor or break. Adding a flag later is additive. The std chain
is therefore vendored {root}/std/_std.rue, then $RUE_STD_PATH, then E0705.
Under --source-manifest, an existing but undeclared vendored std is a
hermetic denial error, never treated as absence. The alternative (manifest
as world, falling through to the declared env std) would let dev and
manifest builds of one tree silently resolve std to different files, which
is the positive-to-different-positive divergence this ADR exists to kill,
and would collapse the denied/absent distinction ADR-0063 invalidation
depends on. Phase 2 pins this with a test; no behavior change is expected.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
Policy version 2 (ADR-0078). Relative specifiers search one base, the
importing file's directory: the root-directory fallback is gone, so
adding a nearer file can no longer retarget an unchanged import.
Extensionless specifiers name the directory facade alone; a file module
is spelled with its extension, which deletes the file/facade ambiguity
group instead of diagnosing it. std anchors to the program: the vendored
{root}/std/_std.rue precedes the captured toolchain root, so ambient
environment never replaces a standard library the program ships.
A relative import whose single normalized candidate leaves the project
root derives no filesystem request and is rejected as E0713, making
project-root-relative module identity a total function. The diagnostic
is recomputed from the parsed program and captured context by the
canonical diagnostic projection; on the demand path the binding is a
first-class Missing terminal. Trusted standard-library importers resolve
within their own root and are exempt.
Unit tests that encoded the old group shapes move to the new policy;
the ambiguity tests become deterministic-facade tests, and the
two-leaf provenance test becomes a single-leaf test that pins the
sibling file-module spelling as inert.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
The wave-hop carry test exercised its answered-but-open contract through the root-relative fallback, which policy v2 removes. The std chain is the remaining multi-group occurrence: the vendored candidate probed absent leaves the occurrence owing the toolchain facade an operation, pinning the same carry contract. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
With one candidate per group, a group can never accept two sources, so the ambiguity surface is structurally unreachable. Delete it end to end rather than leaving a dead path: CanonicalImportResolution::Ambiguous, CanonicalImportGraphProblem::AmbiguousResolution, the winner and diagnostic arms, ImportBindingFailure::Ambiguous and its AIR ImportResolution::Ambiguous projection, the envelope's ambiguous outcome, and ErrorKind::AmbiguousModule with its boxed payload. E0708 stays a retired number per the registry convention. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
ADR-0078 Phase 4. When an extensionless import reports E0704 and the
sibling {P}.rue exists on disk, the CLI appends a help naming the
extensioned spelling. The probe is presentation-only: it runs after
discovery has closed, feeds no observation ledger, and creates no
dependency edge, so it can never affect resolution, closure, or reuse.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
ADR-0078 Phase 1, under the permanent-ID law: every touched rule is
reworded under its stable ID, never renumbered.
10.2:1 names exactly one candidate per path: extensionless resolves to
the directory facade alone, and a file module requires its extensioned
spelling, so both forms coexist without ambiguity. 10.2:2 searches one
base, the importing file's directory - no root-file fallback. 10.2:4
states canonical identity as the normalized project-root-relative path.
10.2:6 replaces the env-first std search with the program-anchored
chain: vendored {root}/std/_std.rue, then $RUE_STD_PATH; std is a
reserved specifier, never searched importer-relative. New 10.2:7 makes
project-root identity total. 4.13:89 becomes the escape legality rule
(E0713). 10.1:5 states the coexistence rule and retires E0708.
Every extensionless file-module example across the spec gains its
extension; directory-facade examples stay extensionless. ADR-0026 is
marked amended-by 0078 where it stated the E0708 ambiguity.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
Mechanical migration: every extensionless import of a file module across the spec, CLI, and ui fixtures gains its extension; directory-facade imports stay extensionless. Intra-facade imports that spelled paths root-relatively (outer/inner.rue from inside outer/) become importer-relative (inner.rue), which the removed fallback had masked. Policy fixtures are rewritten rather than deleted: - the root-relative-fallback case becomes no_root_relative_fallback (E0704) plus an explicit ../-spelling case that stays inside the root; - the E0708 ambiguity cases become coexistence pins: extensionless resolves the facade, extensioned resolves the file, deterministically; - the RUE-317 dotdot reconciliation cases collapse into one E0713 pin - an escaping import is rejected even when its target exists; - emit-deps expectations track normalized specifiers with extensions and the complete facade resolution where ambiguity used to fail. New coverage for the ADR-0078 acceptance: importer-relative-only locality (nearer file is the ONLY candidate), negative-to-positive at the single candidate path, vendored std serving a nested importer while an adjacent sub/std/_std.rue stays inert (10.2:6), escape rejection citing 10.2:7/4.13:89, the migration help naming the extensioned spelling, and the undeclared-vendored-std hermetic fail-closed denial. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
steveklabnik
marked this pull request as ready for review
August 18, 2026 19:54
steveklabnik
enabled auto-merge
August 18, 2026 19:54
A candidate that could not be read is not a resolution: the precedence walk continues to the next candidate, and a failed observation is reported only when no candidate in the occurrence's chain resolved. This fixes a regression this branch introduced. Under --source-manifest, hermetic denial is lexical and takes no probe - by design, so a hermetic build never touches an undeclared path - so the compiler cannot tell "absent" from "present but undeclared". With the vendored std candidate probed first, treating its denial as conclusive failed EVERY hermetic build whose program does not vendor std at all: the candidate is denied even when nothing is there. The repository's own reproducible-programs fixture is exactly that shape, and CI caught it. The manifest is therefore the authority on which standard library is in the build: declare the vendored copy and it wins, omit it and the declared toolchain facade resolves. Preserving the stricter rule would require statting undeclared candidates, weakening the no-probe guarantee to sharpen a case a correct manifest never reaches. Under policy v2 only std has more than one candidate, so the rule is confined to the std chain; every relative specifier still fails on its single candidate's failure. Denied and absent remain distinguishable typed observations per ADR-0063 - they are merely both non-resolutions for precedence purposes. The semantic-order reproducibility fixture leaned on the removed root-directory fallback to reach its shared module; it now names the path explicitly, keeping the FileId-ordering contract it exists to pin. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
The rue_scan fixture reached its shared module through the root-directory fallback ADR-0078 removes, and its comment documented that dependency. Name the path explicitly; the scan contract it pins is unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JoHS41MNhkoMdqfeNqPrja
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements ADR-0078 (RUE-1127), all five phases. The ruling was recorded in #2522; this is the language change itself.
The change
Policy version 2. Every
@importspecifier owns exactly one candidate path:@import("foo")namesfoo/_foo.ruealone; a file module is spelled@import("foo.rue"). Both forms may coexist — the ambiguity is deleted, not diagnosed.stdanchors to the program. Vendored{root}/std/_std.rue, then the toolchain root. Ambient environment can no longer replace a standard library the program ships.The core is ~40 lines in
discovery_candidate_groups/discovery_groups_for_occurrence; the rest is consequence.Two rulings settled after the ADR merged
--std-pathis deferred, not shipped. No present workflow needs a deliberate-override channel — every harness overrides through the environment against non-vendoring programs — and CLI surface shipped now is surface RUE-1586's distribution design must later honor or break. Adding a flag later is additive. The chain is vendored → env → E0705.Under a manifest, the manifest decides which std is in the build. A candidate the manifest does not declare is skipped and the walk continues: declare the vendored copy and it wins, omit it and the declared toolchain facade resolves.
This narrows an earlier form of the ruling that required an undeclared-but-present vendored std to fail closed. That rule is not implementable as stated, and CI proved it: hermetic denial is lexical and takes no probe — by design, so a hermetic build never touches an undeclared path — so the compiler cannot distinguish "absent" from "present but undeclared". Treating the denial as conclusive failed every hermetic build whose program does not vendor std at all, because the vendored candidate is probed first and denied even when nothing is there. The repository's own
reproducible-programsfixture is exactly that shape. Preserving the stricter rule would require statting undeclared candidates, weakening the no-probe guarantee to sharpen a case a correct manifest never reaches.Consequently a failed observation is conclusive only when no candidate in the occurrence's chain resolved. Under policy v2 only
stdhas more than one candidate, so this is confined to the std chain; every relative specifier still fails on its single candidate's failure. Denied and absent remain distinguishable typed observations per ADR-0063 — they are merely both non-resolutions for precedence purposes.E0708 retired
With one candidate per group a group can never accept two sources, so the ambiguity surface is structurally unreachable. Removed end to end rather than left dead:
CanonicalImportResolution::Ambiguous,AmbiguousResolution, the winner and diagnostic arms,ImportBindingFailure::Ambiguousand its AIR projection, the envelope outcome, andErrorKind::AmbiguousModule. E0708 stays a retired number per the registry convention.Verified against a built compiler
Every hazard the ADR cites, re-run after the change:
sub/std/_std.ruebeside importer, env unset@import("../outside.rue")"../outside.rue"Plus the three manifest arms: no vendored std compiles; undeclared vendored std falls through to the declared toolchain std; declared vendored std wins.
Suites: compiler units 901, AIR 653, driver 45, rue 201, spec 2350, CLI integration 1891, and the
premergetier 194/194.Migration
Zero real-code changes.
dijkstra,calculatorandtinydbwere facade imports all along, so they compile untouched — the ADR's estimate of three affected files was wrong in the safe direction. The churn is ~280 fixture sites that exercised the policy deliberately.Three fixture classes needed judgment rather than mechanical rewriting, all flagged in commit messages:
../reconciliation cases are superseded. They pinned that an escaping import reconciles with a CLI-listed file; total root-relative identity forbids exactly that. Replaced with an E0713 pin.outer/_outer.rueimported"outer/inner.rue"only resolved through the fallback; they are importer-relative now.reproducibility/semantic-order,fixtures/rue-program/hello) and said so in their comments. They now name their paths explicitly, keeping the contracts they exist to pin. A static audit over every.ruefile outsidecrates/confirms no fallback-dependent import remains.Spec
Amended under the permanent-ID law — every touched rule reworded under its stable ID, never renumbered. 10.2:1 (one candidate), 10.2:2 (one base), 10.2:4 (canonical identity), 10.2:6 (program-anchored std), new 10.2:7 (total identity), 4.13:89 (escape legality), 10.1:5 (coexistence). ADR-0026 marked
amended-by: [0078]where it stated the E0708 ambiguity.New coverage
Importer-relative-only locality, negative→positive at the single candidate, vendored std serving a nested importer while an adjacent
sub/std/_std.ruestays inert, escape rejection, the migration help, and the undeclared-vendored-std skip.Closes RUE-1127.