Summary
recover_import_edges_from_import_map() in skills/understand/merge-batch-graphs.py builds its candidate node set from type == "file" only, then hardcodes the file: prefix for both edge endpoints. Whole-file nodes created under any other prefix — config:, schema:, service:, document: — are invisible to the recovery pass, so real importMap edges get reported as missing instead of recovered.
Version: 2.9.4
The code
# Build the set of file: node ids actually present in the assembled graph.
file_node_ids: set[str] = set()
for node in assembled["nodes"]:
if node.get("type") == "file":
file_node_ids.add(node.get("id", ""))
...
src_id = f"file:{src_path}"
if src_id not in file_node_ids: ...
tgt_id = f"file:{tgt_path}"
if tgt_id not in file_node_ids:
skipped_no_tgt_node += 1
The file-analyzer agents legitimately model a whole file under a non-file: prefix — that is what the node-type table asks for. A JSON locale catalogue becomes config:messages/de.json, a Prisma schema becomes schema:prisma/schema.prisma, a compose file becomes service:docker-compose.prod.yml.
Symptom
Imports edge recovery:
Recovered 0 `imports` edges from importMap (384 entries scanned)
Skipped 3 importMap target paths with no `file:` node in graph
The message blames the graph for a missing node that exists — under a different prefix. On the project above, the three "missing" targets were all present as config: nodes:
config:messages/de.json
config:messages/en.json
config:src/lib/validation/outcome-config.json
Reproducer
Any project where a code file imports a JSON/YAML/schema file that an analyzer typed as config: or schema:. Concretely: a Next.js app with next-intl where src/app/types/dictionary.ts imports messages/de.json and messages/en.json.
Suggested fix
Build a path -> node_id map across whole-file node types, preferring file: when a path carries more than one, and resolve through it:
WHOLE_FILE_NODE_TYPES: frozenset[str] = frozenset({
"file", "config", "document", "service", "pipeline", "schema", "resource",
})
node_id_by_path: dict[str, str] = {}
for node in assembled["nodes"]:
ntype = node.get("type")
nid = node.get("id", "")
fpath = node.get("filePath")
# The id-shape check keeps whole-file nodes and excludes sub-file nodes
# such as `table:<path>:<name>` or `endpoint:<path>:<method>`.
if ntype in WHOLE_FILE_NODE_TYPES and fpath and nid == f"{ntype}:{fpath}":
if fpath not in node_id_by_path or ntype == "file":
node_id_by_path[fpath] = nid
Then src_id = node_id_by_path.get(src_path) / tgt_id = node_id_by_path.get(tgt_path).
table and endpoint are deliberately excluded — in this schema they are always sub-file constructs and never import endpoints. The two log lines should also change from "with no `file:` node in graph" to "with no whole-file node in graph".
Keep the existing if src_id == tgt_id: continue self-loop guard — it is a useful backstop (see the Python-resolver issue).
Verification
Replaying a real project's 44 batch fragments through the merge:
|
before |
after |
Recovered … imports edges |
0 |
3 |
Skipped … target paths |
3 |
0 |
| dangling edges |
0 |
0 |
| self-loops |
0 |
0 |
Node and edge totals otherwise unchanged; the three recovered edges carry recoveredFromImportMap: true and point at the correct config: nodes.
Found while analysing a production TypeScript + Python repo with /understand. Happy to open a PR.
Summary
recover_import_edges_from_import_map()inskills/understand/merge-batch-graphs.pybuilds its candidate node set fromtype == "file"only, then hardcodes thefile:prefix for both edge endpoints. Whole-file nodes created under any other prefix —config:,schema:,service:,document:— are invisible to the recovery pass, so real importMap edges get reported as missing instead of recovered.Version: 2.9.4
The code
The file-analyzer agents legitimately model a whole file under a non-
file:prefix — that is what the node-type table asks for. A JSON locale catalogue becomesconfig:messages/de.json, a Prisma schema becomesschema:prisma/schema.prisma, a compose file becomesservice:docker-compose.prod.yml.Symptom
The message blames the graph for a missing node that exists — under a different prefix. On the project above, the three "missing" targets were all present as
config:nodes:config:messages/de.jsonconfig:messages/en.jsonconfig:src/lib/validation/outcome-config.jsonReproducer
Any project where a code file imports a JSON/YAML/schema file that an analyzer typed as
config:orschema:. Concretely: a Next.js app withnext-intlwheresrc/app/types/dictionary.tsimportsmessages/de.jsonandmessages/en.json.Suggested fix
Build a
path -> node_idmap across whole-file node types, preferringfile:when a path carries more than one, and resolve through it:Then
src_id = node_id_by_path.get(src_path)/tgt_id = node_id_by_path.get(tgt_path).tableandendpointare deliberately excluded — in this schema they are always sub-file constructs and never import endpoints. The two log lines should also change from "with no `file:` node in graph" to "with no whole-file node in graph".Keep the existing
if src_id == tgt_id: continueself-loop guard — it is a useful backstop (see the Python-resolver issue).Verification
Replaying a real project's 44 batch fragments through the merge:
Recovered … imports edgesSkipped … target pathsNode and edge totals otherwise unchanged; the three recovered edges carry
recoveredFromImportMap: trueand point at the correctconfig:nodes.Found while analysing a production TypeScript + Python repo with
/understand. Happy to open a PR.