fix(extractors): share the in-flight scan root via base, not graphify.extract (#3666)

`extractors/markdown.py` read the scan root of the extraction in flight with a
function-local `import graphify.extract` + `getattr(_extract,
"_XAML_ACTIVE_EXTRACT_ROOT", None)`. That violates MIGRATION.md invariant #4
("never import from graphify.extract inside this package; direction is
extract.py -> extractors/"), and the `getattr(..., None)` meant a rename of the
module global would silently disable Markdown wikilink (Obsidian vault)
resolution rather than fail loudly.

Move the ambient scan root onto `extractors/base.py` — where the extractors
legitimately live — as `set_active_scan_root()` / `active_scan_root()`.
`_safe_extract_with_xaml_root` now publishes the root there (still saving and
restoring for nested extraction), the two XAML readers and the Markdown vault
lookup read it through the accessor, and `markdown.py` imports it in the correct
direction. The `_XAML_ACTIVE_EXTRACT_ROOT` module global in extract.py is gone;
nothing else referenced it.

Behavior is unchanged — the same process-global root is set around each
extraction and read by the same sites. Tests assert the extractor no longer
imports graphify.extract (fails on the old code), that the set/get/restore
contract nests correctly, and that markdown's accessor IS the base accessor.
The markdown, XAML/dotnet, and language suites stay green; the only failures on
this machine are the pre-existing v8 baseline set (c-include determinism,
wikilink-unicode, two watch cwd-deleted cases), which fail identically on clean
v8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJbLfztSxm2tH5cJwBbe9q
(cherry picked from commit 86721f765d)
This commit is contained in:
abhay-codes07 authored and safishamsi committed 2026-10-16 18:21:28 +01:00
1 parent 5fec9d32e8
commit 97922b9c66
4 files changed
+118 -17

No files matched your search

+12 -8
View File
@@ -44,6 +44,8 @@ from graphify.extractors.base import ( # noqa: F401
_read_source_bytes,
_read_source_text,
_read_text,
active_scan_root,
set_active_scan_root,
)
from graphify.extractors.apex import extract_apex # noqa: F401
from graphify.extractors.bash import extract_bash # noqa: F401
@@ -8134,9 +8136,10 @@ def _xaml_project_root(path: Path) -> Path:
break
except OSError:
continue
if _XAML_ACTIVE_EXTRACT_ROOT is None:
active_root = active_scan_root()
if active_root is None:
return root
boundary = _XAML_ACTIVE_EXTRACT_ROOT.resolve()
boundary = active_root.resolve()
try:
root.resolve().relative_to(boundary)
return root
@@ -8147,7 +8150,7 @@ def _xaml_project_root(path: Path) -> Path:
def _xaml_csharp_class_nodes(path: Path) -> dict[str, list[dict]]:
from graphify.detect import _is_ignored, _is_noise_dir, _load_graphifyignore
root = _xaml_project_root(path)
cache_key = str(root.resolve()) if _XAML_ACTIVE_EXTRACT_ROOT is not None else None
cache_key = str(root.resolve()) if active_scan_root() is not None else None
if cache_key and cache_key in _XAML_CSHARP_CLASS_CACHE:
return _XAML_CSHARP_CLASS_CACHE[cache_key]
classes: dict[str, list[dict]] = {}
@@ -8205,7 +8208,6 @@ def _xaml_pascal_name(name: str) -> str | None:
_XAML_TOOLKIT_FIELD_RE = re.compile(r"\b(?P<name>_?m?_?[A-Za-z_]\w*)\s*(?:=.*)?;")
_XAML_TOOLKIT_METHOD_RE = re.compile(r"\b(?P<name>[A-Za-z_]\w*)\s*\(")
_XAML_ACTIVE_EXTRACT_ROOT: Path | None = None
_XAML_CSHARP_CLASS_CACHE: dict[str, dict[str, list[dict]]] = {}
@@ -8839,13 +8841,15 @@ def _get_extractor(path: Path) -> Any | None:
def _safe_extract_with_xaml_root(extractor, path: Path, root: Path) -> dict:
global _XAML_ACTIVE_EXTRACT_ROOT
previous_root = _XAML_ACTIVE_EXTRACT_ROOT
_XAML_ACTIVE_EXTRACT_ROOT = root.resolve()
# Publish the in-flight scan root on extractors.base (the correct import
# direction) so root-dependent extractors — Markdown link resolution, the
# XAML C# project clamp — can read it without reaching back into this module
# (#3666). Despite the XAML-era name this wraps every extractor.
previous_root = set_active_scan_root(root.resolve())
try:
return _safe_extract(extractor, path, scan_root=root)
finally:
_XAML_ACTIVE_EXTRACT_ROOT = previous_root
set_active_scan_root(previous_root)
def _worker_init(language_overrides: dict[str, str]) -> None:
+28
View File
@@ -6,6 +6,34 @@ from pathlib import Path
from graphify.ids import make_id
# Ambient scan root of the extraction in flight. extract.py's
# _safe_extract_with_xaml_root sets it around every per-file extraction; the
# extractors that need the corpus boundary read it (Markdown link resolution and
# the XAML C# project-root clamp). It lives here rather than in graphify.extract
# so those extractors read it in the correct import direction (extract.py ->
# extractors/, never back), per MIGRATION.md invariant #4 — the old
# function-local `import graphify.extract` + getattr(..., None) was a package ->
# parent dependency that also failed silently if the module global were renamed
# (#3666). None outside extract().
_ACTIVE_SCAN_ROOT: "Path | None" = None
def set_active_scan_root(root: "Path | None") -> "Path | None":
"""Set the in-flight scan root, returning the previous value so the caller
can restore it — extraction nests (a XAML shard re-enters extract())."""
global _ACTIVE_SCAN_ROOT
previous = _ACTIVE_SCAN_ROOT
_ACTIVE_SCAN_ROOT = root
return previous
def active_scan_root() -> "Path | None":
"""The scan root of the extraction in flight, or None outside extract().
A direct ``extract_markdown()`` / ``extract_xaml()`` call (no surrounding
``extract()``) sees None and the root-dependent behavior stays off."""
return _ACTIVE_SCAN_ROOT
# Language built-in globals that AST may classify as call targets when used as
# constructors or coercion functions (e.g. String(x), Number(x), Boolean(x)).
# Without this filter they become god-nodes accumulating spurious edges from
+5 -9
View File
@@ -139,15 +139,11 @@ def _parse_frontmatter_fallback(fm_lines: list[str]) -> dict:
_MD_LINK_INDEX_CACHE: "dict[str, dict[str, list[tuple[int, str, Path]]]]" = {}
def _active_scan_root() -> "Path | None":
"""The scan root of the extraction in flight, or None outside extract().
_safe_extract_with_xaml_root sets this for every extraction despite its
XAML-era name; a direct extract_markdown() call has no root and the
fallback stays off.
"""
import graphify.extract as _extract
return getattr(_extract, "_XAML_ACTIVE_EXTRACT_ROOT", None)
# The in-flight scan root now lives on extractors.base (correct import
# direction: extract.py -> extractors/, never back), so a rename can no longer
# silently disable link resolution the way the old getattr(graphify.extract, …,
# None) could (#3666).
from graphify.extractors.base import active_scan_root as _active_scan_root
def _nfc(s: str) -> str:
+73
View File
@@ -0,0 +1,73 @@
"""The in-flight scan root is shared through extractors.base, not by reaching
back into graphify.extract — MIGRATION.md invariant #4 (#3666)."""
from __future__ import annotations
import ast
from pathlib import Path
from graphify.extractors import base as base_mod
from graphify.extractors import markdown as markdown_mod
def _imports_graphify_extract(module) -> bool:
"""True if the module's source imports graphify.extract anywhere — a
top-level or a function-local `import graphify.extract` / `from
graphify.extract import ...`."""
source = Path(module.__file__).read_text(encoding="utf-8")
for node in ast.walk(ast.parse(source)):
if isinstance(node, ast.Import):
if any(a.name == "graphify.extract" or a.name.startswith("graphify.extract.")
for a in node.names):
return True
elif isinstance(node, ast.ImportFrom):
if node.module == "graphify.extract" or (node.module or "").startswith("graphify.extract."):
return True
return False
def test_markdown_extractor_does_not_import_graphify_extract():
"""The Markdown extractor read the in-flight scan root via a function-local
`import graphify.extract` + getattr, a package->parent dependency that
violated invariant #4 and degraded silently on a rename (#3666). It must now
reach the shared context through extractors.base instead."""
assert not _imports_graphify_extract(markdown_mod), (
"extractors/markdown.py must not import graphify.extract — read the scan "
"root via extractors.base.active_scan_root() (MIGRATION.md invariant #4)"
)
def test_active_scan_root_defaults_to_none():
# A fresh process (no extraction in flight) has no active root.
assert base_mod.active_scan_root() is None
def test_active_scan_root_set_get_restore_nested():
"""set_active_scan_root returns the previous value so nested extraction can
restore it; active_scan_root reflects the current top of that stack."""
assert base_mod.active_scan_root() is None
outer = Path("/repo").resolve()
inner = Path("/repo/vendored").resolve()
prev_outer = base_mod.set_active_scan_root(outer)
try:
assert prev_outer is None
assert base_mod.active_scan_root() == outer
prev_inner = base_mod.set_active_scan_root(inner)
try:
assert prev_inner == outer
assert base_mod.active_scan_root() == inner
finally:
base_mod.set_active_scan_root(prev_inner)
assert base_mod.active_scan_root() == outer
finally:
base_mod.set_active_scan_root(prev_outer)
assert base_mod.active_scan_root() is None
def test_markdown_active_scan_root_alias_is_base_accessor():
"""The Markdown extractor's _active_scan_root is the base accessor, so the
scan root extract() publishes is exactly what link resolution reads."""
assert markdown_mod._active_scan_root is base_mod.active_scan_root