Skip to content

Absolute-path node ids on 0.9.25 for in-root relative import targets (residual of #1899 Variant B; ghost duplicates + dangling endpoints) #2195

Description

@timomitchel

Summary

On 0.9.25, node ids for the targets of relative imports that cross directories are still built from the absolute resolved path, while the same node's source_file is correctly relativized. This is the same class as #1789 / #1899 (Variant B: relativized source_file but an id derived from the absolute path) and #1961, all closed, but with a trigger those did not cover: in-root relative import targets in an Astro plus TypeScript project (../../lib/content, ../../config, ../styles/global.css).

Because the importing file also produces a correctly named node for the same target, the result is a ghost duplicate: src_lib_content and users_<name>_..._<repo>_src_lib_content both exist in the graph.

Environment

  • graphify 0.9.25 (installed via uv tool, Python 3.14, macOS)
  • AST-only extraction (no LLM), undirected build
  • Project: Astro site. Pages under src/pages/** and src/layouts/Base.astro import ../../lib/content, ../../config, and ../styles/global.css.

Observed

Three import-target nodes carry an id derived from the absolute path, with a correct repo-relative source_file:

id:          users_<name>_startups_<...>_<repo>_src_lib_content
source_file: src/lib/content
label:       ../../lib/content

id:          users_<name>_..._<repo>_src_config
source_file: src/config

id:          users_<name>_..._<repo>_src_styles_global_css
source_file: src/styles/global.css

For src_lib_content and src_config, a correctly named node with the same repo-relative id already exists in the graph (the file's own node), so the absolute-id node is a pure duplicate. The absolute-id nodes are also the endpoints of imports_from edges, so on the post-build graph they show up as dangling endpoints (as in #1961).

Reproduction

Minimal shape: a file that imports another in-root file by a relative path that ascends at least one directory.

src/lib/content.ts
src/pages/work/index.astro   ->  import { ... } from '../../lib/content'

Then:

from pathlib import Path
from graphify.extract import extract
res = extract([Path('src/pages/work/index.astro'), Path('src/lib/content.ts')], cache_root=Path('.'))
print([n['id'] for n in res['nodes'] if 'lib_content' in n['id']])
# -> ['src_lib_content', 'users_<name>_..._src_lib_content']   # duplicate, second is absolute

Notes:

  • Deterministic. It reproduces whether the paths passed to extract are absolute or relative to the scan root.
  • It recurs through the post-commit hook. graphify.watch._rebuild_code(root, changed_paths=[that page]) regenerates the absolute-id node on every incremental rebuild of a file that has such an import, so a graph that was sanitized and committed gets re-poisoned the next time one of those code files changes.

Expected

The import-target node id should be relativized against the scan root before slugifying, the same way source_file already is, and should reuse the existing repo-relative id for the target file rather than minting a second node. When relative_to(root) cannot apply, fall back to the module specifier (the import string) rather than the absolute filesystem path.

Consequences

Workaround in use

A pre-commit hook strips any node id that embeds an absolute path back to its repo-relative form and merges the duplicate into its canonical twin before commit, so the tracked graph stays clean. Happy to share the script if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions