ts-js-local-specifier-reverse-resolution
I've written the complete solution to ~/GAP-069-local-specifier-reverse-resolution.md.
The <project>/hilo checkout at ~/<project> does not exist in this sandbox, and no Rust toolchain (cargo/rustc) is installed. Only /workspace/problem.json was present. I therefore could not compile or run the fix against the real crate. The deliverable is a self-contained, directly-applicable patch reconstructed from the detailed GAP-069 specification; it should be applied to the v0.3.0 tree and validated with the included commands.
Root-cause analysis — classify_js stores local:<spec> verbatim, so one file maps to many node strings (one per importer-dir/specifier pair); the Go/Python file → pkg: inversion cannot work. Plus the two correctness traps and the JIT-vs-warm secondary finding.
Exact fix — a new hilo-graph/src/local_reverse.rs module with:
./.. normalization and ?query/#hash stripping.ts/.tsx/.js/.jsx/.mjs/.cjs/.json), TS-ESM .js/.mjs/.cjs → .ts/.tsx, and <base>/index.*resolve_specifier(importer_dir, spec) resolving against each importer's own directory (filesystem check disambiguates the decoy)LocalReverseIndex keyed target-path → {local node → importer set} so trap (1) is structurally enforcedcanonical_target bridging absolute JIT paths and repo-relative warm paths, with an ambiguity-guarded suffix fallback (trap (2))
Wiring — GraphDB::local_reverse_index() (single SELECT "from","to" ... WHERE "to" LIKE 'local:%' per query), related(Reverse) augmentation with the importer-set guard, and the BFS seed in compute_impact / compute_impact_with_external, covering CLI, MCP, and FFI.
Regression test — an on-disk fixture with three specifier forms plus a decoy that emits the identical local:./pluginContainer node string from a different directory, asserting the decoy never cross-contaminates.
Verification — cargo test -p hilo-graph gap_069, the exact failing CLI command, MCP check, decoy isolation, an absolute-path check, and a negative control documenting that hilo graph warm is required first.
# Evidence - Problem class: ts-js-local-specifier-reverse-resolution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-14T14:31:33.112Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Hilo/WarpFS GAP-069. Symptom: `hilo graph impact <file>.ts --max-depth 1` prints 'No dependents found' (rc 0) and MCP vfs_graph_impact returns {dependents:[],total:0} for a TypeScript file that has real importers, while <file>.go/.py file-form queries work (GAP-057/064 fixed those). Root cause: the JS/TS parser keeps relative import specifiers verbatim as `local:<spec>` edge targets (classify_js), so ONE target file is named by MANY node strings - one per (importer directory, specifier) pair: `local:./pluginContainer` from server/, `local:../pluginContainer` from server/inner/, `local:../server/pluginContainer` from node/plugins/. A per-file resolution walk (the pkg: approach: file -> one pkg node) cannot invert that mapping. FIX (query-time only, no re-warm, no canonical edge-format change): add a reverse index that scans the `local:` edges once per query (SELECT \"from\",\"to\" FROM edges WHERE \"to\" LIKE 'local:%'), resolves each specifier against ITS OWN importer's directory (lexical '.'/'..' normalization; extension probing .ts/.tsx/.js/.jsx/.mjs/.cjs/.json; <base>/index.{ts,tsx,js,jsx}; a .js/.mjs specifier also probes .ts/.tsx per the TS-ESM convention; trailing ?query/#hash stripped), and indexes target-path -> {local node -> importer set}. Two correctness traps: (1) the SAME node string local:./x is emitted from different directories naming DIFFERENT targets, so a consumer must require edge.from to be in the resolved importer set - filtering on the node alone cross-contaminates; (2) the graph has two path spaces: `graph warm` stores importers repo-relative while JIT parses store absolute - bridge absolute queries into repo space via a derived root (canonicalized) with an ambiguity-guarded component-suffix fallback. Wire the family into compute_impact / compute_impact_with_external (BFS seed) and GraphDB::related(Reverse) so the CLI, MCP vfs_graph_impact and FFI all benefit. Test shape that catches the bug: a real on-disk fixture with 3 importers using the 3 specifier forms plus a decoy importer whose node string is identical but resolves elsewhere. NON-TRIVIAL SECONDARY FINDING: reverse/file-form queries against a JIT (never-warmed) graph return empty for genuine importers, because the graph only holds files already parsed - a fresh `hilo graph warm` is required before a reverse query is meaningful. A first verification round that skipped warm reported false negatives (2 of 3 importers missing, and 0 on a fresh corpus).", "environment": "<project>/hilo repo at ~/<project> (Rust workspace, hilo-graph crate, DuckDB edge store), Rust 1.98.0", "language": "rust", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "ts-js-local-specifier-reverse-resolution", "provider": "openrouter", "solved_at": "2026-09-14T14:31:33.113Z", "version": "0.3.0"}I've written the complete solution to ~/GAP-069-local-specifier-reverse-resolution.md.
The <project>/hilo checkout at ~/<project> does not exist in this sandbox, and no Rust toolchain (cargo/rustc) is installed. Only /workspace/problem.json was present. I therefore could not compile or run the fix against the real crate. The deliverable is a self-contained, directly-applicable patch reconstructed from the detailed GAP-069 specification; it should be applied to the v0.3.0 tree and validated with the included commands.
Root-cause analysis — classify_js stores local:<spec> verbatim, so one file maps to many node strings (one per importer-dir/specifier pair); the Go/Python file → pkg: inversion cannot work. Plus the two correctness traps and the JIT-vs-warm secondary finding.
Exact fix — a new hilo-graph/src/local_reverse.rs module with:
./.. normalization and ?query/#hash stripping.ts/.tsx/.js/.jsx/.mjs/.cjs/.json), TS-ESM .js/.mjs/.cjs → .ts/.tsx, and <base>/index.*resolve_specifier(importer_dir, spec) resolving against each importer's own directory (filesystem check disambiguates the decoy)LocalReverseIndex keyed target-path → {local node → importer set} so trap (1) is structurally enforcedcanonical_target bridging absolute JIT paths and repo-relative warm paths, with an ambiguity-guarded suffix fallback (trap (2))
Wiring — GraphDB::local_reverse_index() (single SELECT "from","to" ... WHERE "to" LIKE 'local:%' per query), related(Reverse) augmentation with the importer-set guard, and the BFS seed in compute_impact / compute_impact_with_external, covering CLI, MCP, and FFI.
Regression test — an on-disk fixture with three specifier forms plus a decoy that emits the identical local:./pluginContainer node string from a different directory, asserting the decoy never cross-contaminates.
Verification — cargo test -p hilo-graph gap_069, the exact failing CLI command, MCP check, decoy isolation, an absolute-path check, and a negative control documenting that hilo graph warm is required first.
# Evidence - Problem class: ts-js-local-specifier-reverse-resolution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-14T14:31:33.112Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Hilo/WarpFS GAP-069. Symptom: `hilo graph impact <file>.ts --max-depth 1` prints 'No dependents found' (rc 0) and MCP vfs_graph_impact returns {dependents:[],total:0} for a TypeScript file that has real importers, while <file>.go/.py file-form queries work (GAP-057/064 fixed those). Root cause: the JS/TS parser keeps relative import specifiers verbatim as `local:<spec>` edge targets (classify_js), so ONE target file is named by MANY node strings - one per (importer directory, specifier) pair: `local:./pluginContainer` from server/, `local:../pluginContainer` from server/inner/, `local:../server/pluginContainer` from node/plugins/. A per-file resolution walk (the pkg: approach: file -> one pkg node) cannot invert that mapping. FIX (query-time only, no re-warm, no canonical edge-format change): add a reverse index that scans the `local:` edges once per query (SELECT \"from\",\"to\" FROM edges WHERE \"to\" LIKE 'local:%'), resolves each specifier against ITS OWN importer's directory (lexical '.'/'..' normalization; extension probing .ts/.tsx/.js/.jsx/.mjs/.cjs/.json; <base>/index.{ts,tsx,js,jsx}; a .js/.mjs specifier also probes .ts/.tsx per the TS-ESM convention; trailing ?query/#hash stripped), and indexes target-path -> {local node -> importer set}. Two correctness traps: (1) the SAME node string local:./x is emitted from different directories naming DIFFERENT targets, so a consumer must require edge.from to be in the resolved importer set - filtering on the node alone cross-contaminates; (2) the graph has two path spaces: `graph warm` stores importers repo-relative while JIT parses store absolute - bridge absolute queries into repo space via a derived root (canonicalized) with an ambiguity-guarded component-suffix fallback. Wire the family into compute_impact / compute_impact_with_external (BFS seed) and GraphDB::related(Reverse) so the CLI, MCP vfs_graph_impact and FFI all benefit. Test shape that catches the bug: a real on-disk fixture with 3 importers using the 3 specifier forms plus a decoy importer whose node string is identical but resolves elsewhere. NON-TRIVIAL SECONDARY FINDING: reverse/file-form queries against a JIT (never-warmed) graph return empty for genuine importers, because the graph only holds files already parsed - a fresh `hilo graph warm` is required before a reverse query is meaningful. A first verification round that skipped warm reported false negatives (2 of 3 importers missing, and 0 on a fresh corpus).", "environment": "<project>/hilo repo at ~/<project> (Rust workspace, hilo-graph crate, DuckDB edge store), Rust 1.98.0", "language": "rust", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "ts-js-local-specifier-reverse-resolution", "provider": "openrouter", "solved_at": "2026-09-14T14:31:33.113Z", "version": "0.3.0"}