Repository navigation
Name why a Lean root is unusable instead of indexing nothing or crashing - #231
Conversation
Commands resolved --lean-root with Path.resolve() and then asked Path.is_dir(), each in its own way. is_dir() re-raises EACCES on Python 3.10 to 3.13 and returns False for it on 3.15 (3.14 was not checked), so a root under a folder that cannot be searched gave render --ref the bare "Lean sources could not be indexed", made audit call the root missing, and on newer Pythons made index_project return an empty index, so every lean: name read as missing (F-F5, NEW-AUDIT-TRACEBACK, NEW-PY314-EMPTY). render with an explicit --ref indexed through index_project, whose empty index for a missing or non-directory root published the site with 0 code links and exit 0 (B-F4). #182 typed the RuntimeError that an unknown ~user or (before Python 3.13) a symbolic link loop raises, in build_linker, audit and skeleton only; render still crashed with a traceback on such a root, audit described it as missing, and on Python 3.13 a loop reached the source capture. The root cause is that the root check was a boolean pre-check repeated in every caller, with no reason attached, in front of an index function that answers a missing root with an empty index. lean.resolve_lean_root now resolves the root once, stats it, and raises LeanSourceError with the reason, never a host path: "Lean root does not exist or is not a directory", "permission denied while resolving the Lean root", or #182's "Lean root cannot be resolved" for an unknown ~user or a symbolic link loop on every Python version. build_linker, render's explicit root, audit, the runtime projection (work) and search call it, and then capture with snapshot_project_sources, which fails closed, so no command can reach the empty index if the root vanishes after the check. Audit keeps its invalid-lean-root code and now gives the reason as its text. index_project keeps its empty index for a root that does not exist or is not a directory, which a test pins as API, and raises the typed error for every other reason. The explicit-ref alias swap test hooks the capture the linker now calls. The runtime failure test patches the capture the runtime now calls. The #182 command test expects the audit reason instead of the missing-root text.
The previous commit gave commands one place to resolve a Lean root, but three commands still kept their own check (F-F9, C5). skeleton and work impact read lakefile.toml and lakefile.lean before looking at the root, so a missing root was "no lakefile.toml or lakefile.lean in <host path>" and a root under an unsearchable folder a lakefile stat error naming the host path (F-F9's main trigger); #182 covered only the unknown ~user and pre-3.13 loop cases in skeleton, with their own message. Both now call resolve_lean_root first, and skeleton captures its sources with snapshot_project_sources, like the other commands, instead of index_project's empty index. doctor turned every failure into invalid-lean-root and showed only that code, so an unsearchable parent, an unknown home and a loop all read as "1 finding(s): invalid-lean-root", the same as a missing root. Its lean targets check now shows the reason as its detail. The Lean root matrix gains skeleton, work impact and doctor. It covers check, audit and skeleton for an unknown ~user and a symbolic link loop with exact messages, so #182's weaker command test is removed. The skeleton test that expected the lakefile message for a missing root and the doctor test that expected the bare code now expect the reason; on Windows the doctor loop case asserts only a failing check with no host path, because Windows reports the loop by its own error code.
Main gained #198, which added skeleton.resolve_lean_root for the same job as this branch's lean.resolve_lean_root, and #188, which folded the skeleton input re-checks into check_inputs_unchanged. Keep one helper, as promised on #198: skeleton.resolve_lean_root is dropped, and extract_skeletons and unused_hypotheses call lean.resolve_lean_root and wrap its LeanSourceError as a SkeletonError with the same reason. unused-hypotheses now names an unusable root the way skeleton does, with no host path, instead of the lakefile message or the raw Python error. extract_skeletons takes #188's snapshot_inputs and check_inputs_unchanged, and both those re-checks and unused_hypotheses capture with snapshot_project_sources instead of index_project, so no command reaches the empty index of a root that vanished after the check. tests/test_lean_root.py gains an unused-hypotheses row for all five roots, and #198's test for an unknown home now pins the shared reason.
Takes #165 (transactional render publication), which conflicted in render.py: render no longer builds a linker, it resolves the Lean root and captures it with snapshot_project_sources. The resolution keeps #165's structure and resolves the explicit root with resolve_lean_root inside a guard, so an unknown ~user or a symbolic link loop is an error instead of a traceback and every unusable root names its reason. #165's test_lasting_capture_failures_name_their_cause now expects that reason for a missing root.
index_project answered a missing or non-directory root with an empty index. That contract is what let render --ref publish a site with no code links, and the branch worked around it by moving every caller to snapshot_project_sources and splitting the root check into _inspect_lean_root so the contract could survive with no caller left. index_project now calls resolve_lean_root, so it fails closed like the capture it wraps, and the empty-index contract and its test are gone. The runtime projection, search, skeleton, unused-hypotheses and the explicit-ref linker keep calling index_project as they do on main, and the three monkeypatch retargets that followed the call swap are reverted. resolve_lean_root is one function again: it resolves and stats the root in one guarded step instead of passing a tuple through _inspect_lean_root and _lean_root_failure. The 44-case command by root matrix becomes one test of the helper's five reasons plus one case per command code path, each with a root that command misreported at main (check, search, work list, work impact, unused-hypotheses, audit). render, skeleton and doctor were already pinned by their own changed tests. index_project's unsearchable-root test is covered by the helper test; a missing root is pinned by test_index_project_refuses_a_missing_root.
index_project called directory_binding.lexical_absolute_path(root) before resolve_lean_root(root). lexical_absolute_path expands ~, which raises a raw RuntimeError for an unknown ~user, so index_project leaked that error instead of the typed LeanSourceError its docstring promises. build_linker already called the helper first. Call resolve_lean_root first so the helper decides every root failure, then compute the lexical path for exclusion remapping. Pin it by moving the missing-root test next to the unusable_root fixture in tests/test_lean_root.py and parametrizing it over the missing and unknown-home cases. The unknown-home case fails at 5fe3967 with RuntimeError. render caught OSError around resolve_lean_root, although the helper only raises LeanSourceError and every other call site catches that. Narrow the catch to LeanSourceError. Behavior does not change.
Review at 46a4bb0Verdict: Needs small fixes before merge. The PR adds Fixes before merge
Optional
Landing
This was a static review: no tests were run locally. CI at this head: all 9 checks pass. Posted by PR swarm: PR Swarm Lead |
|
Merge readiness: needs small fixes (reviewed at 46a4bb0)
Rebase needed: #236 has merged, so this now conflicts with main in doctor.py, 3 hunks. Use the recipe in #236's body. #235 then gets a one-hunk render.py import conflict: keep Caveat: |
#236 made the lean targets check the doctor's only Lean capture and dropped the runtime projection's Lean root, so this branch's _resolve_lean_root helper and its audit split no longer apply. The lean targets branch now calls resolve_lean_root itself: an unusable root reports the reason, otherwise the resolved root goes to _lean_findings, as the recipe in #236's body describes.
build_linker must call resolve_lean_root before lexical_absolute_path, and render must resolve the root before anything expands it, because expanduser() raises a raw RuntimeError for an unknown ~user and neither command catches it. Removing #182's test_lean_root_commands_report_an_unresolvable_root_without_traceback left that ordering unpinned: swapping the two lines in build_linker brought the traceback back and every test still passed (merge-readiness fix 1). Add check and render cases with an unknown-home root to the main() matrix in tests/test_lean_root.py, asserting the exit code, the exact reason and no host path.
|
Update: fixes pushed, now ready to merge (fd7b518)
Remaining conflict: #235, one import hunk in render.py (keep |
Brings in main up to b1b0094, including #248 (package srcDir), #231 (Lean root errors) and #165. Two files conflicted, and every resolution keeps both sides: - autoform_cli/impact.py: this branch's claims import and main's LeanSourceError and resolve_lean_root import. Main's switch to resolve_lean_root merged into _load_impact_evidence, where this branch moved revision_impact's body. - autoform_cli/skeleton.py: lean_libraries computes this branch's root artifact directory and main's package srcDir. Library srcDirs are relative to the package srcDir, and a package without library targets uses the package srcDir and the artifact directory.
Brings in main up to f169a4d. One hunk conflicted, at the top of build_linker, because #231 replaced the inline root resolution with resolve_lean_root. The resolution keeps this branch's one-argument signature and takes resolve_lean_root, as this PR's description planned. _remap_resolved_root_exclusion stays: open_project_sources still uses it.
Resolves the conflicts the merged PRs' descriptions point at: - runtime: resolve the Lean root with resolve_lean_root before the name-filtered index (#231). - render: keep #235's no-capture path around the name-filtered capture, and warn when code links are skipped. - graph pages: count each chapter's formalization targets as #250 does, with the explorer's collapsed-node wording. - lean: take #265's section forms and mutual scopes. #269 removed tolerated_links, so the generation revision now filters the omitted entries that editor lock links leave. - tests: take #236's doctor test; keep the targeted-index tests beside #265's scope test.
Summary
lean.resolve_lean_rootexpands, resolves and stats the root once, and raisesLeanSourceErrorwith a reason and no host path. Every command that takes a Lean root calls it first: check, audit, render, skeleton, unused-hypotheses, doctor, work list/context/impact, search.index_projectfails closed through the same helper. Its empty index for a missing root is gone; that contract is what letrender --refpublish a site with no code links before Publish blueprint sites transactionally #165.work impactworking beside Lean files whose names are not identifiers #182's guards inbuild_linkerandaudit._lean_findings, Add autoform unused-hypotheses: report hypotheses a proof never uses #198'sskeleton.resolve_lean_root, and the ad hocexpanduser().resolve()plusis_dir()checks in runtime, search, render, impact and doctor.~namefor an unknown user, or a symbolic link loopBehavior changes
Exit codes are unchanged. Messages:
~userroot gives the reason instead of aRuntimeErrortraceback.invalid-lean-rootcarries the reason instead of "does not exist or is not a directory" for every failure.lean.resolve_lean_rootis new;index_projectraisesLeanSourceErrorfor an unusable root;skeleton.resolve_lean_rootleavesskeleton.__all__.Tests
tests/test_lean_root.py(new, 15 cases): the helper with each of five roots (missing, file, under a mode-000 folder, unknown~user, self-loop) gives exactly its reason;index_projectgives the same reason for a missing and an unknown~userroot; one case per command path throughmain()(check, search, work list, work impact, unused-hypotheses, audit) asserts exit code, exact output and no host path, plus check and render with an unknown~userroot. These replacetest_lean_sources.py::test_missing_project_keeps_the_empty_index_contract.work impactworking beside Lean files whose names are not identifiers #182's prefix-onlytest_cli.py::test_lean_root_commands_report_an_unresolvable_root_without_traceback, now covered exactly.resolve_lean_root; render with an unknown~userroot raisesRuntimeErrorthere). Swappingresolve_lean_rootandlexical_absolute_pathinbuild_linkerfailscheck-unknown-home. Dropping the helper'sRuntimeError,PermissionError, ELOOP orS_ISDIRbranch fails exactly one helper case; expanding the root before the helper inindex_projectfails the unknown~usercase.Overlap with open PRs
Main abae767 is merged in. Its doctor.py conflict with #236 is resolved by the recipe in #236's body: the lean targets check calls
resolve_lean_rootand reports its reason, otherwise passes the resolved root to_lean_findings.MAIN_REF=upstream/main pr_conflicts.shat fd7b518, conflicts this branch adds:from .lean import (...)list. KeepLeanSourceError,IndexedSourceSnapshotandSourceIndex. On a trial merge ruff is clean andtest_windows_source_boundary,test_lean_rootandtest_rendergive 167 passed, 2 skipped.resolve_lean_rootreturns.work impactworking beside Lean files whose names are not identifiers #182 test.Validation
At fd7b518:
ruff check autoform_cli servers tests: clean.test_unused_hypotheses_in_a_built_project,test_the_probe_reads_a_built_projectand the four skeleton lakefile tests pass.