#2265·graphify

AST extractor emits edges to node IDs it never creates, causing spurious GRAPH HEALTH WARNING on every JS/TS project

Author: drmzperxCreated Jul 28, 2026Updated Sep 17, 2026

AST extractor emits edges to node IDs it never creates, causing a spurious GRAPH HEALTH WARNING on every JS/TS project

Version: graphifyy 0.9.26 Environment: Python 3.12.13, Linux x86_64 (glibc 2.31)

Summary

graphify.extract.extract() emits edges whose endpoints are never added to nodes. Step 4.5's diagnose_extraction then reports them as dangling_endpoint_edges and prints:

GRAPH HEALTH WARNING: 47 dangling-endpoint edges; ... - graph may be incomplete/corrupt.

On a real 69-file Next.js/TypeScript repo this fired with 47 dangling edges, all with _origin: "ast" and none from semantic extraction. Since the trigger is ordinary third-party imports, this warning fires on essentially every JS/TS project, which trains users to ignore a diagnostic that is supposed to signal corruption.

There are two distinct causes.

Cause 1 — ref_<module> targets for external imports are never materialised (45/47)

Every import of a third-party package produces an imports_from edge to a ref_* target with no matching node:

app_api_channels_route --imports_from--> ref_next_server     (no such node)

Observed targets included ref_next_server (9×), ref_react (7×), ref_vitest (6×), ref_next_navigation, ref_mysql2, ref_swr, ref_leaflet, ref_mqtt, ref_auth0_nextjs_auth0_server, ref_influxdata_influxdb_client.

This is inconsistent within graphify itself: the JSON/tsconfig extractor does materialise ref_* nodes. In the same build, the only 10 surviving ref_* nodes (ref_dom, ref_esnext, ref_ts, ref_tsx, ref_node_modules, ref_next_env_d_ts, …) all came from tsconfig.json. So one producer creates these nodes and another emits edges to them without creating them.

Cause 2 — some edge producers build IDs from the absolute path and keep the extension (2/47)

{"source": "app_layout",
 "target": "home_ati_workspace_pushmesh_app_globals_css",
 "relation": "imports_from", "source_file": "app/layout.tsx",
 "source_location": "L3", "_origin": "ast"}

Two defects in one ID: the path was not relativised against cache_root, and the extension was retained (globals.css..._globals_css). Every other node in the build used repo-relative, extension-stripped IDs. The reproducer below hits the same malformed-ID builder from a different producer (indirect_call), so it is not specific to CSS imports.

Reproducer

mkdir -p /tmp/gr && cd /tmp/gr
cat > a.ts <<'EOF'
import { useState } from 'react';
export const A_MAX = Number(process.env.A_MAX || 10);
export function useA() { return useState(A_MAX); }
EOF
cat > b.ts <<'EOF'
import { useA } from './a';
export const B = String(useA);
EOF
python -c "
from pathlib import Path
from graphify.extract import extract
sp = Path('/tmp/gr')
r = extract([sp/'a.ts', sp/'b.ts'], cache_root=sp)
ids = {n['id'] for n in r['nodes']}
print('NODES:', sorted(ids))
for e in r['edges']:
    bad = '' if e['source'] in ids and e['target'] in ids else '   <== DANGLING'
    print(f\"  {e['source']} --{e['relation']}--> {e['target']}{bad}\")
"

Actual output:

NODES: ['a', 'a_a_max', 'a_usea', 'b']
  a --imports_from--> ref_react                                    <== DANGLING
  a --contains--> a_a_max
  a --contains--> a_usea
  b --imports_from--> a
  b --imports--> a_usea
  tmp_gr_b_ts --indirect_call--> a_usea                            <== DANGLING

Note tmp_gr_b_ts on the last edge: absolute path, extension kept — while the same file's node is b.

Expected

Either behaviour would be fine as long as it is consistent:

  1. Materialise ref_* nodes in the TS/JS import extractor, as the JSON extractor already does — external dependencies become first-class nodes; or
  2. Drop the edge when the target is an unresolved external module, so no dangling edge is produced.

Separately, the malformed-ID builder should relativise against cache_root and strip the extension like every other producer.

Impact

  • GRAPH HEALTH WARNING is a false positive on every JS/TS repo, so a genuine integrity problem is indistinguishable from routine noise.
  • Third-party dependency edges are silently lost at build time (595 of 644 edges survived the build in the real repo).