diff --git a/skills/codealive-context-engine/SKILL.md b/skills/codealive-context-engine/SKILL.md index 28548f7..1882f84 100644 --- a/skills/codealive-context-engine/SKILL.md +++ b/skills/codealive-context-engine/SKILL.md @@ -308,6 +308,34 @@ artifact that fits in context. The outgoing calls you need are either in the source you just read or in the preview's 3-cap — reach for `relationships.py` only when you specifically need incoming calls, inheritance, or references. +**Call sites — read the call, not the whole caller.** Call relationships +(`outgoing_calls` / `incoming_calls`) also show *where* each call is written: + +``` + • my-org/backend::src/db.py::query + 📍 src/db.py:42 + ↪ called at src/svc.py:17 + ↪ called at src/svc.py:88 (~60% confident) + ↪ … 3 more call site(s) not shown +``` + +There is no flag for this — positions come back whenever they are known, so +don't go looking for one. Read those exact lines with `Read`/`sed` instead of +fetching the whole calling artifact. + +Three things to keep straight: +- **No `called at` line** means the position is **not indexed yet** — the + repository was indexed before call sites existed, or that one edge could not + be located. It **never** means the call does not happen. The relationship + being listed at all is what says the call exists. +- **A confidence** (`~60% confident`) appears only when the position is + approximate; treat it as a hint and confirm by reading. **No confidence + means the position is exact.** +- **`… N more`** means the list was capped, not that the rest do not exist. + +For `incoming_calls` the file shown is the **caller's** file, which is a +different file from the artifact you asked about. + **Noise caveat:** outgoing calls occasionally include compiler-generated helpers (`MoveNext`, `GetEnumerator`, closure invocations) for methods using `foreach`/LINQ. These are analyzer artifacts — ignore outgoing hits that diff --git a/skills/codealive-context-engine/scripts/lib/api_client.py b/skills/codealive-context-engine/scripts/lib/api_client.py index a0da1c8..8fc23da 100644 --- a/skills/codealive-context-engine/scripts/lib/api_client.py +++ b/skills/codealive-context-engine/scripts/lib/api_client.py @@ -626,6 +626,12 @@ def get_artifact_relationships( ``relationships`` groups. Each group has relationType, totalCount, returnedCount, truncated, and an ``items`` list of related artifacts (identifier, filePath, startLine, shortSummary). + + Call items additionally carry ``callSites`` — where the call is + written (filePath, 1-based line, and a ``confidence`` only when the + position is approximate) — and ``callSiteCount``, the pre-cap total. + Their ABSENCE means the position is not indexed yet, never that the + call does not happen. """ profile_map = { "callsOnly": "calls_only", diff --git a/skills/codealive-context-engine/scripts/relationships.py b/skills/codealive-context-engine/scripts/relationships.py index f615883..6054c55 100755 --- a/skills/codealive-context-engine/scripts/relationships.py +++ b/skills/codealive-context-engine/scripts/relationships.py @@ -137,6 +137,8 @@ def format_relationships(data: dict, data_source: str = None) -> str: output.append(f" • {ident}") if loc: output.append(f" 📍 {loc}") + for call_site_line in _format_call_sites(item): + output.append(call_site_line) if short_summary: output.append(f" 📝 {short_summary}") @@ -144,6 +146,36 @@ def format_relationships(data: dict, data_source: str = None) -> str: return "\n".join(output) +def _format_call_sites(item: dict) -> list: + """Render where a call is written, if we know. + + Nothing is printed when the position is unknown — which means the repository has not been + re-indexed since call sites were introduced, NOT that the call does not happen. Printing an + empty "call sites" heading would suggest the latter. + """ + call_sites = item.get("callSites") + if not call_sites: + return [] + + lines = [] + for site in call_sites: + file_path = (site or {}).get("filePath") + line_number = (site or {}).get("line") + if not file_path or not line_number: + continue + + # Confidence is shown only when the backend sent one; its absence means the position is exact. + confidence = (site or {}).get("confidence") + approximate = f" (~{confidence:.0%} confident)" if confidence is not None else "" + lines.append(f" ↪ called at {file_path}:{line_number}{approximate}") + + total = item.get("callSiteCount") + if isinstance(total, int) and total > len(lines) > 0: + lines.append(f" ↪ … {total - len(lines)} more call site(s) not shown") + + return lines + + def main(): """CLI interface for fetching artifact relationships.""" if len(sys.argv) < 2 or sys.argv[1] == "--help": diff --git a/tests/test_relationship_call_sites.py b/tests/test_relationship_call_sites.py new file mode 100644 index 0000000..0f019e4 --- /dev/null +++ b/tests/test_relationship_call_sites.py @@ -0,0 +1,100 @@ +"""Call-site rendering in the relationships script. + +The failure mode is silent in both directions: a renderer that drops positions produces output that +looks perfectly normal, and one that prints a heading for an unknown position tells the agent the +call happens nowhere. Both are asserted here. +""" + +from __future__ import annotations + +import importlib.util +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SCRIPTS_ROOT = REPO_ROOT / "skills" / "codealive-context-engine" / "scripts" + + +def _load_relationships(): + spec = importlib.util.spec_from_file_location( + "codealive_relationships_call_sites", SCRIPTS_ROOT / "relationships.py" + ) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +relationships = _load_relationships() + + +def _item(**overrides): + item = { + "identifier": "my-org/backend::src/db.py::query", + "filePath": "src/db.py", + "startLine": 42, + } + item.update(overrides) + return item + + +def test_known_positions_are_rendered_as_call_sites() -> None: + # Arrange + item = _item( + callSites=[{"filePath": "src/svc.py", "line": 17}], + callSiteCount=1, + ) + + # Act + lines = relationships._format_call_sites(item) + + # Assert + assert lines == [" ↪ called at src/svc.py:17"] + + +def test_confidence_is_shown_only_when_the_position_is_approximate() -> None: + # Arrange + item = _item( + callSites=[ + {"filePath": "src/svc.py", "line": 17}, + {"filePath": "src/svc.py", "line": 88, "confidence": 0.6}, + ], + callSiteCount=2, + ) + + # Act + lines = relationships._format_call_sites(item) + + # Assert + assert lines[0] == " ↪ called at src/svc.py:17" + assert lines[1] == " ↪ called at src/svc.py:88 (~60% confident)" + + +def test_a_capped_list_says_how_many_were_withheld() -> None: + # Arrange + item = _item( + callSites=[{"filePath": "src/svc.py", "line": 17}], + callSiteCount=4, + ) + + # Act + lines = relationships._format_call_sites(item) + + # Assert + assert lines[-1] == " ↪ … 3 more call site(s) not shown" + + +def test_an_unindexed_position_renders_nothing_at_all() -> None: + # An empty heading here would read as "this call happens nowhere", which is the opposite of + # what a missing position means. + # Arrange + without_key = _item() + explicit_empty = _item(callSites=[], callSiteCount=0) + + # Act + from_missing = relationships._format_call_sites(without_key) + from_empty = relationships._format_call_sites(explicit_empty) + + # Assert + assert from_missing == [] + assert from_empty == []