Skip to content

Keep work impact working beside Lean files whose names are not identifiers - #182

Draft
Deicyde wants to merge 3 commits into
mainfrom
fix/impact-module-names
Draft

Deicyde wants to merge 3 commits into
mainfrom
fix/impact-module-names

Conversation

@Deicyde

@Deicyde Deicyde commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #136. Re-reviewing it after it merged, I found one problem a user can hit and two error-path nits. All three still reproduce on main 4c80749.

What a user sees. A Finder duplicate such as Demo/Basic copy.lean under a library's srcDir made autoform work impact exit 2 with invalid Lean declaration name: 'Demo.Basic copy', even though no root or glob selects that file. With this PR the impact report comes out as usual.

  • Non-identifier module names (5a11922, impact.py). project_modules counts every source module under a library as local, and the probe spelled each name with _lean_name, which cannot parse Demo.Basic copy. _source_modules now quotes components that are not plain identifiers the way Lean does (Demo.«Basic copy»), so constants from that module stay local and keep their impact. The module-to-path lookup compares names by component, so Demo.α, which Lean prints unquoted, still finds its file. A dotted stem such as Demo/Foo.Bar.lean is now the single component «Foo.Bar», as in Lean, so a Demo.Foo.* glob no longer selects it as Demo.Foo.Bar, and Demo/..lean is Demo.«.» (with_suffix("") rejects that stem on Python 3.13). A component containing » has no quoted spelling: Lake still builds Demo/«Foo».lean, but no import can name it, so such files are never local, and a glob that selects one is refused as it is on main. The glob branch refuses every quoted module, so the probe imports nothing new.
  • lake not on PATH (9a2d26f, skeleton.py). The message said "a built Lean project is required to extract skeletons" for the impact probe too. It now names the probe's purpose, like the neighboring messages: "lake is not on PATH; running the impact probe requires a built Lean project".
  • An unresolvable --lean-root (4139fee, lean.py, skeleton.py, audit.py). expanduser() raises RuntimeError for an unknown ~user on every Python version, and before 3.13 resolve() raises it for a symbolic link loop; check, audit, and skeleton crashed with a traceback on either. check now prints "Lean sources could not be indexed: Lean root cannot be resolved" (index errors never show host paths), skeleton names the root and the cause, and audit reports its existing invalid-lean-root finding. work list, work context, work impact, and doctor already handled both. This one predates Report Lean revision impact and deprecated targets #136.

Tests, in tests/test_impact.py: test_unselected_source_with_a_quoted_module_name_stays_local (Basic copy.lean, α.lean, ..lean, x»y.lean), test_project_modules_read_a_dotted_file_stem_as_one_component (Foo.Bar.lean and Foo.x»y.lean beside a Demo.Foo.* glob), two test_project_modules_fail_closed cases where a glob selects «Foo».lean or x»y.lean, and the extended test_impact_probe_freshness_messages_never_mention_skeletons. In tests/test_cli.py: test_lean_root_commands_report_an_unresolvable_root_without_traceback, which runs check, audit, and skeleton against both roots on every Python version (on 3.13 check fails later for the loop, when it reads the sources, with the same prefix).

Not changed: render has the same crash for --lean-root, and for its blueprint and output paths. #165, which rewrites render, already guards all three, so this PR leaves render.py alone rather than conflict with it.

Overlap: merges cleanly with every open PR except three. #140 and #141 already conflict with main in 24 files; this adds 3 hunks to their tests/test_impact.py conflict. #75 conflicts with main only in skills/setup/SKILL.md; this adds a one-hunk conflict in tests/test_cli.py.

Validation at 4139fee: locally, the full suite on Python 3.13 and 3.10 (1855 passed, 56 skipped, 1 xfailed each), the real-Lean impact tests (77 passed), ruff check, and make check-example. The real-Lean skeleton and open-statement jobs (173 passed; 95 passed, 1 skipped) last ran before a final change to _source_modules and its tests, which they do not exercise.

A library's srcDir may hold a .lean file whose stem is not a plain
identifier, such as a Finder duplicate "Basic copy.lean", even when the
library's roots and globs never select it. project_modules still lists
every captured source module as local, and render_impact_probe spelled
"Demo.Basic copy" with _lean_name, which cannot parse it, so "autoform
work impact" exited 2 with "invalid Lean declaration name".

Spell each source module with Lean's guillemet quoting for components
that are not plain identifiers: Demo.«Basic copy». The probe then builds
the Name Lean reports for that module, so constants Lean can import
from it stay local and keep their impact, and the module-to-path lookup
agrees with the record's module. Skipping unrepresentable names instead
would have dropped such modules from locality and missed their
dependents. The lookup now compares names by component, so a Unicode
component Lean prints unquoted (Demo.α) still finds its file. The
components come from the parent directories and the stem rather than
from with_suffix(""), which on Python 3.13 rejects the stem "." of a
stray "..lean" (Lean's Demo.«.»).

A component containing "»", as in Demo/«Foo».lean or Demo/x»y.lean,
has no quoted spelling, so no import can name that module and it is
never local. Lake can still build such a file: it builds
Demo/«Foo».lean as a module whose last component is "«Foo»". So
_source_modules returns these files separately, spelled with the
same quoting so that Demo/Foo.x»y.lean stays one component outside
Demo.Foo.*, and a glob that selects one is refused as on main rather
than leaving the module's constants silently out of the report.

The quoting also treats a dotted stem such as Demo/Foo.Bar.lean as the
single component «Foo.Bar», matching Lean. A Demo.* glob over that file
is refused with "plain module names only", and an explicit root
Demo.Foo.Bar backed only by it reports "has no source file", where the
probe used to emit a wrong import. The glob branch still refuses to
import any quoted module, so the probe imports nothing new.
run_probe said "a built Lean project is required to extract skeletons"
for every probe label, so "autoform work impact" without lake talked
about skeletons. Phrase it with _probe_purpose(label) like the
neighboring lake-manifest and freshness messages: "lake is not on PATH;
running the impact probe requires a built Lean project".
Commands resolve --lean-root with pathlib, which raises RuntimeError in
two cases: expanduser() for an unknown ~user, such as a mistyped
'~nosuchuser/lean', on every Python version; and resolve() for a
symbolic link loop (la -> lb -> la) before Python 3.13, which returns
the loop path instead. "autoform check", "audit" and "skeleton" crashed
with a traceback on such a root, while "work impact" and "doctor"
already caught RuntimeError and reported the path as unreadable.

build_linker and extract_skeletons now raise their usual error instead.
check prints "Lean sources could not be indexed: Lean root cannot be
resolved", a fixed message because index errors never show host paths,
and skeleton names the root and the cause. The audit's Lean findings
guard the expanduser, resolve and is_dir calls with the same exceptions
doctor's _resolve_lean_root catches and report the existing
invalid-lean-root finding.

"render" has the same crash. It is left alone here because #165, which
rewrites render, already guards that call.
@Deicyde

Deicyde commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Review at 4139fee

Verdict: merge-ready. The quoting, the component-keyed locator and the three error-path fixes are correct on main, and CI is green at this head. It also fixes one silent drop the body doesn't claim (test gap 1). The interaction with #183 that item 3 of the posted #183 review predicts is real. The fix belongs in #183, which needs the same change for its own bug 3 anyway. Details are under Landing.

Test gaps

1. Digit-leading module components are fixed but not pinned (autoform_cli/impact.py:266)

On main, Demo/1.lean is keyed Demo.1, and _lean_name renders it into the probe's projectModules as Name.num (Name.str (Name.anonymous) "Demo") 1. Lean names that module Name.str "1", because an import can only spell it Demo.«1». isLocalModule never matches, so the probe emits no record for any constant in that module and drops every use edge through it. work impact then reports a smaller impact than the real one, and nothing errors. At this head the component fails _MODULE_COMPONENT, gets quoted, and renders as Name.str (...) "1".

  • Repro: a project with Demo.lean and Demo/1.lean. project_modules + render_impact_probe give Name.num ... 1 on main and Name.str ... "1" at this head. On real Lean 4.34.1, import Demo.«1» loads a module whose last component is Name.str "1", and m == Name.num Demo 1` is false.
  • Teeth: a mutant at :266 that quotes only components _lean_name_parts cannot read (re.fullmatch(r"[^.\s«»]+", part)) brings Name.num back. It passes all 38 test_impact.py tests that touch module naming (-k "project_modules or quoted or located or source or probe"). No test in tests/ names a digit-leading module file.
  • Fix: add 1.lean to test_unselected_source_with_a_quoted_module_name_stays_local, and assert that 'Name.str (Name.str (Name.anonymous) "Demo") "1"' is in the probe.

Landing

Posted by PR swarm: Review #182 #185 #187

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant