descry-codegraph 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/CHANGELOG.md +38 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/PKG-INFO +1 -1
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/pyproject.toml +1 -1
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/ast_grep.py +0 -26
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/cross_lang.py +2 -54
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/embeddings.py +0 -41
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/generate.py +0 -1
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/handlers.py +42 -13
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/mcp_server.py +7 -2
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/query.py +0 -1
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/scip/parser.py +0 -22
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/web/server.py +34 -11
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_config.py +44 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_handlers.py +0 -1
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/.github/workflows/ci.yml +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/.github/workflows/release.yml +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/.gitignore +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/CLAUDE.md +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/LICENSE +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/README.md +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/SECURITY.md +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/justfile +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/pi-extension/README.md +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/pi-extension/descry-cli.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/pi-extension/descry-tools.ts +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/__init__.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/_env.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/_graph.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/cli.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/git_history.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/scip/__init__.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/scip/cache.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/scip/pb2.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/scip/support.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/tree_sitter_parser.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/web/__init__.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/web/web/index.html +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/src/descry/web/web/style.css +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/__init__.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/conftest.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/fixtures/small_graph.json +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_cli.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_generate.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_git_history.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_hardening.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_mcp.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_query.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_scip.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_tree_sitter_parser.py +0 -0
- {descry_codegraph-0.1.0 → descry_codegraph-0.1.1}/tests/test_web_server.py +0 -0
|
@@ -4,6 +4,44 @@ All notable changes to Descry will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to pre-1.0 semver (minor-version bumps may include breaking changes; see README versioning stance).
|
|
6
6
|
|
|
7
|
+
## [0.1.1] — 2026-04-17
|
|
8
|
+
|
|
9
|
+
Patch release focused on making cross-language tracing actually configurable,
|
|
10
|
+
plus a round of dead-code cleanup. No breaking changes.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Cross-language tracing is now configurable from `.descry.toml`.** The
|
|
15
|
+
`DescryConfig.openapi_path` field existed in 0.1.0 but had no TOML loader
|
|
16
|
+
and the web `/api/cross-lang` handler hardcoded `public/api/latest.json`,
|
|
17
|
+
so custom spec locations silently did nothing. Added a `[cross_lang]`
|
|
18
|
+
section with `openapi_path`, `backend_handler_patterns`,
|
|
19
|
+
`frontend_api_patterns`, and `api_prefixes` keys. Web and CLI/MCP now
|
|
20
|
+
both honour the config and pass all four through to `CrossLangTracer`.
|
|
21
|
+
- `[cross_lang] openapi_path` is containment-checked against the project
|
|
22
|
+
root; a crafted `.descry.toml` cannot point the indexer at files outside
|
|
23
|
+
the configured project.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
|
|
27
|
+
Dead code with no call sites anywhere in the tree was pruned:
|
|
28
|
+
|
|
29
|
+
- `DescryConfig.project_markers` (auto-detect used a module constant).
|
|
30
|
+
- `DescryConfig.use_tree_sitter_ts` (scaffolding field with no consumer).
|
|
31
|
+
- `descry.query.MAX_INLINE_THRESHOLD` constant.
|
|
32
|
+
- `CrossLangTracer.endpoint_to_node_id()` and module-level
|
|
33
|
+
`_create_cross_lang_edges()` helper.
|
|
34
|
+
- `SemanticSearcher._find_similar()` (distinct from
|
|
35
|
+
`GraphQuerier._find_similar_nodes`, which remains).
|
|
36
|
+
- `descry.ast_grep.extract_imports_typescript_batch()`.
|
|
37
|
+
- `ScipIndex.get_definition_location()` and `get_symbol_info()`.
|
|
38
|
+
- `TypeScriptSymbolTable.file_dir` attribute.
|
|
39
|
+
- `DescryService._clear_dedup_cache()` (superseded by `reset_caches()` in
|
|
40
|
+
the 0.1.0 hardening sweep).
|
|
41
|
+
|
|
42
|
+
A `vulture --min-confidence 80` sweep on `src/descry/` now reports zero
|
|
43
|
+
findings outside generated protobuf code.
|
|
44
|
+
|
|
7
45
|
## [0.1.0] — 2026-04-16
|
|
8
46
|
|
|
9
47
|
Initial public PyPI release. Descry is a polyglot codebase knowledge graph toolkit with three interfaces: CLI (`descry`), MCP server (`descry-mcp`), and local web UI (`descry-web`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: descry-codegraph
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Codebase knowledge graph: polyglot indexer, semantic search, call-graph analysis
|
|
5
5
|
Project-URL: Homepage, https://github.com/06chaynes/descry
|
|
6
6
|
Project-URL: Repository, https://github.com/06chaynes/descry
|
|
@@ -354,29 +354,3 @@ def extract_imports_typescript(file_path: str) -> dict:
|
|
|
354
354
|
continue
|
|
355
355
|
|
|
356
356
|
return result
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
def extract_imports_typescript_batch(file_paths: list) -> dict:
|
|
360
|
-
"""Extract imports from multiple TypeScript files efficiently.
|
|
361
|
-
|
|
362
|
-
Processes files individually but caches results for repeated calls.
|
|
363
|
-
|
|
364
|
-
Args:
|
|
365
|
-
file_paths: List of file paths to process
|
|
366
|
-
|
|
367
|
-
Returns:
|
|
368
|
-
Dict mapping file paths to their import data:
|
|
369
|
-
{
|
|
370
|
-
"path/to/file.ts": {
|
|
371
|
-
"imports": {"name": ("module", "type")},
|
|
372
|
-
"namespaces": {"alias": "module"},
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
"""
|
|
376
|
-
results = {}
|
|
377
|
-
for file_path in file_paths:
|
|
378
|
-
try:
|
|
379
|
-
results[file_path] = extract_imports_typescript(file_path)
|
|
380
|
-
except Exception:
|
|
381
|
-
results[file_path] = {"imports": {}, "namespaces": {}}
|
|
382
|
-
return results
|
|
@@ -185,7 +185,7 @@ class CrossLangTracer:
|
|
|
185
185
|
return self.path_to_operation[key]["operationId"]
|
|
186
186
|
|
|
187
187
|
# Try pattern matching for paths with IDs
|
|
188
|
-
for pattern_method,
|
|
188
|
+
for pattern_method, _, pattern, info in self.endpoint_patterns:
|
|
189
189
|
if pattern_method == method_upper and pattern.match(path):
|
|
190
190
|
return info["operationId"]
|
|
191
191
|
|
|
@@ -197,21 +197,6 @@ class CrossLangTracer:
|
|
|
197
197
|
|
|
198
198
|
return None
|
|
199
199
|
|
|
200
|
-
def endpoint_to_node_id(self, method: str, path: str) -> Optional[str]:
|
|
201
|
-
"""Find the graph node ID for a backend handler.
|
|
202
|
-
|
|
203
|
-
Args:
|
|
204
|
-
method: HTTP method
|
|
205
|
-
path: API path
|
|
206
|
-
|
|
207
|
-
Returns:
|
|
208
|
-
Full node ID (e.g., "FILE:backend/src/routes/actions/handlers.rs::list_actions")
|
|
209
|
-
"""
|
|
210
|
-
op_id = self.endpoint_to_handler(method, path)
|
|
211
|
-
if op_id:
|
|
212
|
-
return self.operation_to_handler.get(op_id)
|
|
213
|
-
return None
|
|
214
|
-
|
|
215
200
|
def get_handler_info(self, method: str, path: str) -> Optional[dict]:
|
|
216
201
|
"""Get full information about an endpoint's handler.
|
|
217
202
|
|
|
@@ -227,7 +212,7 @@ class CrossLangTracer:
|
|
|
227
212
|
return info
|
|
228
213
|
|
|
229
214
|
# Try pattern matching
|
|
230
|
-
for pattern_method,
|
|
215
|
+
for pattern_method, _, pattern, info in self.endpoint_patterns:
|
|
231
216
|
if pattern_method == method_upper and pattern.match(path):
|
|
232
217
|
result = info.copy()
|
|
233
218
|
result["node_id"] = self.operation_to_handler.get(info["operationId"])
|
|
@@ -349,43 +334,6 @@ class CrossLangTracer:
|
|
|
349
334
|
}
|
|
350
335
|
|
|
351
336
|
|
|
352
|
-
def _create_cross_lang_edges(
|
|
353
|
-
graph_data: dict,
|
|
354
|
-
openapi_path: str,
|
|
355
|
-
) -> list[dict]:
|
|
356
|
-
"""Create CALLS_API edges linking frontend to backend handlers.
|
|
357
|
-
|
|
358
|
-
This can be used during graph generation to add cross-language edges.
|
|
359
|
-
|
|
360
|
-
Args:
|
|
361
|
-
graph_data: Current graph data (nodes and edges)
|
|
362
|
-
openapi_path: Path to OpenAPI spec
|
|
363
|
-
|
|
364
|
-
Returns:
|
|
365
|
-
List of new edges to add
|
|
366
|
-
"""
|
|
367
|
-
tracer = CrossLangTracer(openapi_path)
|
|
368
|
-
api_calls = tracer.find_ts_api_calls(graph_data)
|
|
369
|
-
|
|
370
|
-
edges = []
|
|
371
|
-
for call in api_calls:
|
|
372
|
-
if call.get("rust_node_id"):
|
|
373
|
-
edges.append(
|
|
374
|
-
{
|
|
375
|
-
"source": call["ts_node_id"],
|
|
376
|
-
"target": call["rust_node_id"],
|
|
377
|
-
"relation": "CALLS_API",
|
|
378
|
-
"metadata": {
|
|
379
|
-
"endpoint": call["inferred_endpoint"],
|
|
380
|
-
"method": call["inferred_method"],
|
|
381
|
-
"handler": call["rust_handler"],
|
|
382
|
-
},
|
|
383
|
-
}
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
return edges
|
|
387
|
-
|
|
388
|
-
|
|
389
337
|
if __name__ == "__main__":
|
|
390
338
|
# Generic stats-only smoke test. Prints OpenAPI + graph linkage stats.
|
|
391
339
|
import sys
|
|
@@ -441,47 +441,6 @@ class SemanticSearcher:
|
|
|
441
441
|
results.sort(key=lambda x: x[1], reverse=True)
|
|
442
442
|
return results[:limit]
|
|
443
443
|
|
|
444
|
-
def _find_similar(self, node_id: str, limit: int = 5) -> list:
|
|
445
|
-
"""Find nodes semantically similar to a given node.
|
|
446
|
-
|
|
447
|
-
Args:
|
|
448
|
-
node_id: The node ID to find similar nodes for
|
|
449
|
-
limit: Maximum number of results
|
|
450
|
-
|
|
451
|
-
Returns:
|
|
452
|
-
List of (node, score) tuples sorted by similarity
|
|
453
|
-
"""
|
|
454
|
-
# Find the node index
|
|
455
|
-
node_idx = None
|
|
456
|
-
for i, node in enumerate(self.nodes):
|
|
457
|
-
if node["id"] == node_id:
|
|
458
|
-
node_idx = i
|
|
459
|
-
break
|
|
460
|
-
|
|
461
|
-
if node_idx is None:
|
|
462
|
-
return []
|
|
463
|
-
|
|
464
|
-
# Use that node's embedding as query
|
|
465
|
-
node_embedding = self.embeddings[node_idx]
|
|
466
|
-
|
|
467
|
-
# Compute similarities
|
|
468
|
-
similarities = np.dot(self.embeddings, node_embedding) / (
|
|
469
|
-
np.linalg.norm(self.embeddings, axis=1) * np.linalg.norm(node_embedding)
|
|
470
|
-
)
|
|
471
|
-
|
|
472
|
-
# Get top results (excluding the query node itself)
|
|
473
|
-
top_indices = np.argsort(similarities)[::-1]
|
|
474
|
-
results = []
|
|
475
|
-
for idx in top_indices:
|
|
476
|
-
if idx == node_idx:
|
|
477
|
-
continue
|
|
478
|
-
score = float(similarities[idx])
|
|
479
|
-
results.append((self.nodes[idx], score))
|
|
480
|
-
if len(results) >= limit:
|
|
481
|
-
break
|
|
482
|
-
|
|
483
|
-
return results
|
|
484
|
-
|
|
485
444
|
|
|
486
445
|
# Convenience function for one-off searches (with thread safety)
|
|
487
446
|
_cached_searcher = None
|
|
@@ -1399,7 +1399,6 @@ class TypeScriptSymbolTable:
|
|
|
1399
1399
|
project_root: Root directory of the project
|
|
1400
1400
|
"""
|
|
1401
1401
|
self.file_path = file_path
|
|
1402
|
-
self.file_dir = os.path.dirname(file_path)
|
|
1403
1402
|
self.project_root = project_root
|
|
1404
1403
|
|
|
1405
1404
|
# local_name -> (module_path, import_type)
|
|
@@ -174,14 +174,7 @@ class DescryConfig:
|
|
|
174
174
|
max_stale_hours: float = 24
|
|
175
175
|
enable_scip: bool = True
|
|
176
176
|
enable_embeddings: bool = True
|
|
177
|
-
# Opt-in: use tree-sitter for TS/TSX/JS first-pass parsing. Requires
|
|
178
|
-
# descry-codegraph[ast]. Falls back to regex per-file when the grammar
|
|
179
|
-
# hits errors or the extra isn't installed.
|
|
180
|
-
use_tree_sitter_ts: bool = False
|
|
181
177
|
openapi_path: Path | None = None
|
|
182
|
-
project_markers: list[str] = field(
|
|
183
|
-
default_factory=lambda: list(_DEFAULT_PROJECT_MARKERS)
|
|
184
|
-
)
|
|
185
178
|
backend_handler_patterns: list[str] = field(default_factory=list)
|
|
186
179
|
frontend_api_patterns: list[str] = field(default_factory=list)
|
|
187
180
|
api_prefixes: list[str] = field(default_factory=lambda: list(_DEFAULT_API_PREFIXES))
|
|
@@ -316,8 +309,6 @@ class DescryConfig:
|
|
|
316
309
|
self.enable_scip = features["enable_scip"]
|
|
317
310
|
if "enable_embeddings" in features:
|
|
318
311
|
self.enable_embeddings = features["enable_embeddings"]
|
|
319
|
-
if "use_tree_sitter_ts" in features:
|
|
320
|
-
self.use_tree_sitter_ts = features["use_tree_sitter_ts"]
|
|
321
312
|
|
|
322
313
|
# [embeddings]
|
|
323
314
|
embeddings = data.get("embeddings", {})
|
|
@@ -388,6 +379,41 @@ class DescryConfig:
|
|
|
388
379
|
if "skip_crates" in scip:
|
|
389
380
|
self.scip_skip_crates = scip["skip_crates"]
|
|
390
381
|
|
|
382
|
+
# [cross_lang]
|
|
383
|
+
cross_lang = data.get("cross_lang", {})
|
|
384
|
+
if "backend_handler_patterns" in cross_lang:
|
|
385
|
+
patterns = cross_lang["backend_handler_patterns"]
|
|
386
|
+
if isinstance(patterns, list):
|
|
387
|
+
self.backend_handler_patterns = [str(p) for p in patterns]
|
|
388
|
+
if "frontend_api_patterns" in cross_lang:
|
|
389
|
+
patterns = cross_lang["frontend_api_patterns"]
|
|
390
|
+
if isinstance(patterns, list):
|
|
391
|
+
self.frontend_api_patterns = [str(p) for p in patterns]
|
|
392
|
+
if "api_prefixes" in cross_lang:
|
|
393
|
+
prefixes = cross_lang["api_prefixes"]
|
|
394
|
+
if isinstance(prefixes, list):
|
|
395
|
+
self.api_prefixes = [str(p) for p in prefixes]
|
|
396
|
+
if "openapi_path" in cross_lang:
|
|
397
|
+
raw = cross_lang["openapi_path"]
|
|
398
|
+
if isinstance(raw, str) and raw:
|
|
399
|
+
candidate = Path(raw)
|
|
400
|
+
if not candidate.is_absolute():
|
|
401
|
+
candidate = self.project_root / candidate
|
|
402
|
+
# Containment check: keep the spec inside project_root so a
|
|
403
|
+
# crafted .descry.toml can't point descry at arbitrary files.
|
|
404
|
+
try:
|
|
405
|
+
resolved = candidate.resolve(strict=False)
|
|
406
|
+
root = self.project_root.resolve()
|
|
407
|
+
if resolved == root or root in resolved.parents:
|
|
408
|
+
self.openapi_path = resolved
|
|
409
|
+
else:
|
|
410
|
+
logger.warning(
|
|
411
|
+
f"[cross_lang] openapi_path {raw!r} resolves outside "
|
|
412
|
+
f"project root; ignoring"
|
|
413
|
+
)
|
|
414
|
+
except (OSError, RuntimeError) as e:
|
|
415
|
+
logger.warning(f"Invalid [cross_lang] openapi_path: {e}")
|
|
416
|
+
|
|
391
417
|
# [scip.rust]
|
|
392
418
|
scip_rust = scip.get("rust", {})
|
|
393
419
|
if "toolchain" in scip_rust:
|
|
@@ -752,9 +778,6 @@ class DescryService:
|
|
|
752
778
|
del self._dedup_cache[key]
|
|
753
779
|
self._dedup_cache[content_hash] = (graph_mtime, node_id)
|
|
754
780
|
|
|
755
|
-
def _clear_dedup_cache(self):
|
|
756
|
-
self._dedup_cache = {}
|
|
757
|
-
|
|
758
781
|
def reset_caches(self):
|
|
759
782
|
"""Reset all cached instances. Call after reindex."""
|
|
760
783
|
self._graph_cache = {"mtime": 0, "nodes": 0, "edges": 0}
|
|
@@ -1712,7 +1735,13 @@ class DescryService:
|
|
|
1712
1735
|
graph_path = (
|
|
1713
1736
|
str(self.config.graph_path) if self.config.graph_path.exists() else None
|
|
1714
1737
|
)
|
|
1715
|
-
tracer = self._CrossLangTracer(
|
|
1738
|
+
tracer = self._CrossLangTracer(
|
|
1739
|
+
str(openapi_path),
|
|
1740
|
+
graph_path,
|
|
1741
|
+
backend_handler_patterns=self.config.backend_handler_patterns or None,
|
|
1742
|
+
frontend_api_patterns=self.config.frontend_api_patterns or None,
|
|
1743
|
+
api_prefixes=self.config.api_prefixes or None,
|
|
1744
|
+
)
|
|
1716
1745
|
|
|
1717
1746
|
if mode == "stats":
|
|
1718
1747
|
stats = tracer.get_stats()
|
|
@@ -94,8 +94,13 @@ _service: DescryService | None = None
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
@asynccontextmanager
|
|
97
|
-
async def server_lifespan(
|
|
98
|
-
"""Pre-warm graph and embeddings on startup.
|
|
97
|
+
async def server_lifespan(_server: FastMCP):
|
|
98
|
+
"""Pre-warm graph and embeddings on startup.
|
|
99
|
+
|
|
100
|
+
``_server`` is the required lifespan-hook parameter injected by
|
|
101
|
+
FastMCP; the hook only mutates module-level state, so the arg is
|
|
102
|
+
intentionally unused.
|
|
103
|
+
"""
|
|
99
104
|
global _service
|
|
100
105
|
config = DescryConfig.from_env()
|
|
101
106
|
_service = DescryService(config)
|
|
@@ -19,7 +19,6 @@ MAX_CHILDREN_PER_LEVEL = 10 # Branching limit per level
|
|
|
19
19
|
# Token budgets and thresholds
|
|
20
20
|
DEFAULT_TOKEN_BUDGET = 2000 # Default token budget for expanded callees
|
|
21
21
|
CALLEE_INLINE_THRESHOLD = 150 # Inline callees smaller than this (was 100)
|
|
22
|
-
MAX_INLINE_THRESHOLD = 300 # Maximum allowed inline threshold
|
|
23
22
|
MAX_CALLERS_SHOWN = 15 # Maximum callers to show in context
|
|
24
23
|
|
|
25
24
|
# Timeout (milliseconds)
|
|
@@ -481,28 +481,6 @@ class ScipIndex:
|
|
|
481
481
|
|
|
482
482
|
return names
|
|
483
483
|
|
|
484
|
-
def get_definition_location(self, symbol_id: str) -> Optional[Tuple[str, int]]:
|
|
485
|
-
"""Get the definition location for a symbol.
|
|
486
|
-
|
|
487
|
-
Args:
|
|
488
|
-
symbol_id: SCIP symbol identifier
|
|
489
|
-
|
|
490
|
-
Returns:
|
|
491
|
-
Tuple of (file_path, line_number) or None if not found
|
|
492
|
-
"""
|
|
493
|
-
return self.definitions.get(symbol_id)
|
|
494
|
-
|
|
495
|
-
def get_symbol_info(self, symbol_id: str) -> Optional[dict]:
|
|
496
|
-
"""Get metadata for a symbol.
|
|
497
|
-
|
|
498
|
-
Args:
|
|
499
|
-
symbol_id: SCIP symbol identifier
|
|
500
|
-
|
|
501
|
-
Returns:
|
|
502
|
-
Dictionary with kind, display_name, documentation, or None
|
|
503
|
-
"""
|
|
504
|
-
return self.symbols.get(symbol_id)
|
|
505
|
-
|
|
506
484
|
def get_stats(self) -> dict:
|
|
507
485
|
"""Get statistics about the loaded index.
|
|
508
486
|
|
|
@@ -276,13 +276,28 @@ def _caller_to_dict(caller_id: str, q: GraphQuerier) -> dict:
|
|
|
276
276
|
return result
|
|
277
277
|
|
|
278
278
|
|
|
279
|
-
def
|
|
280
|
-
"""
|
|
279
|
+
def _resolve_openapi_path() -> Path | None:
|
|
280
|
+
"""Locate the project's OpenAPI spec, if any.
|
|
281
|
+
|
|
282
|
+
Order of precedence:
|
|
283
|
+
1. `[cross_lang] openapi_path` in .descry.toml (absolute or project-
|
|
284
|
+
relative; resolved by DescryConfig with a containment check).
|
|
285
|
+
2. `public/api/latest.json` under the project root.
|
|
286
|
+
3. `public/api/openapi.json` under the project root.
|
|
287
|
+
"""
|
|
281
288
|
cfg = _get_config()
|
|
289
|
+
if cfg.openapi_path and cfg.openapi_path.exists():
|
|
290
|
+
return cfg.openapi_path
|
|
282
291
|
for name in ("latest.json", "openapi.json"):
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
292
|
+
candidate = cfg.project_root / "public" / "api" / name
|
|
293
|
+
if candidate.exists():
|
|
294
|
+
return candidate
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _openapi_path_exists() -> bool:
|
|
299
|
+
"""Check if an OpenAPI spec file exists in the project."""
|
|
300
|
+
return _resolve_openapi_path() is not None
|
|
286
301
|
|
|
287
302
|
|
|
288
303
|
# --- API Endpoint Handlers ---
|
|
@@ -857,19 +872,27 @@ async def api_cross_lang(request: Request) -> JSONResponse:
|
|
|
857
872
|
tag = request.query_params.get("tag")
|
|
858
873
|
|
|
859
874
|
cfg = _get_config()
|
|
860
|
-
openapi_path =
|
|
861
|
-
if
|
|
862
|
-
openapi_path = cfg.project_root / "public" / "api" / "openapi.json"
|
|
863
|
-
if not openapi_path.exists():
|
|
875
|
+
openapi_path = _resolve_openapi_path()
|
|
876
|
+
if openapi_path is None:
|
|
864
877
|
return JSONResponse(
|
|
865
878
|
{
|
|
866
879
|
"not_configured": True,
|
|
867
|
-
"message":
|
|
880
|
+
"message": (
|
|
881
|
+
"No OpenAPI spec found. Set [cross_lang] openapi_path in "
|
|
882
|
+
".descry.toml, or place a spec at public/api/openapi.json "
|
|
883
|
+
"or public/api/latest.json relative to the project root."
|
|
884
|
+
),
|
|
868
885
|
}
|
|
869
886
|
)
|
|
870
887
|
|
|
871
888
|
graph_path = str(cfg.graph_path) if cfg.graph_path.exists() else None
|
|
872
|
-
tracer = CrossLangTracer(
|
|
889
|
+
tracer = CrossLangTracer(
|
|
890
|
+
str(openapi_path),
|
|
891
|
+
graph_path,
|
|
892
|
+
backend_handler_patterns=cfg.backend_handler_patterns or None,
|
|
893
|
+
frontend_api_patterns=cfg.frontend_api_patterns or None,
|
|
894
|
+
api_prefixes=cfg.api_prefixes or None,
|
|
895
|
+
)
|
|
873
896
|
|
|
874
897
|
if mode == "stats":
|
|
875
898
|
return JSONResponse(tracer.get_stats())
|
|
@@ -268,6 +268,50 @@ extra_args = []
|
|
|
268
268
|
assert config.syntax_lang_map[".rs"] == "rust"
|
|
269
269
|
assert config.syntax_lang_map[".py"] == "python"
|
|
270
270
|
|
|
271
|
+
def test_cross_lang_section(self, tmp_path):
|
|
272
|
+
"""[cross_lang] openapi_path + patterns reach DescryConfig."""
|
|
273
|
+
(tmp_path / "spec.json").write_text('{"openapi": "3.0.0"}')
|
|
274
|
+
(tmp_path / ".descry.toml").write_text(
|
|
275
|
+
'openapi_path = "spec.json"\n'
|
|
276
|
+
'backend_handler_patterns = ["handlers/"]\n'
|
|
277
|
+
'frontend_api_patterns = ["src/api/"]\n'
|
|
278
|
+
'api_prefixes = ["/api/v1", "/api/v2"]\n'.replace(
|
|
279
|
+
"openapi_path", "[cross_lang]\nopenapi_path"
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
config = DescryConfig(project_root=tmp_path)
|
|
283
|
+
config._apply_toml(DescryConfig._load_toml(tmp_path))
|
|
284
|
+
|
|
285
|
+
assert config.openapi_path == (tmp_path / "spec.json").resolve()
|
|
286
|
+
assert config.backend_handler_patterns == ["handlers/"]
|
|
287
|
+
assert config.frontend_api_patterns == ["src/api/"]
|
|
288
|
+
assert config.api_prefixes == ["/api/v1", "/api/v2"]
|
|
289
|
+
|
|
290
|
+
def test_cross_lang_openapi_path_outside_root_rejected(self, tmp_path, caplog):
|
|
291
|
+
"""Paths resolving outside project_root are ignored with a warning."""
|
|
292
|
+
import logging
|
|
293
|
+
|
|
294
|
+
(tmp_path / ".descry.toml").write_text(
|
|
295
|
+
'[cross_lang]\nopenapi_path = "/etc/passwd"\n'
|
|
296
|
+
)
|
|
297
|
+
config = DescryConfig(project_root=tmp_path)
|
|
298
|
+
with caplog.at_level(logging.WARNING):
|
|
299
|
+
config._apply_toml(DescryConfig._load_toml(tmp_path))
|
|
300
|
+
assert config.openapi_path is None
|
|
301
|
+
assert any("outside project root" in r.message for r in caplog.records)
|
|
302
|
+
|
|
303
|
+
def test_cross_lang_openapi_path_absolute_inside_root(self, tmp_path):
|
|
304
|
+
"""Absolute paths that resolve inside project_root are accepted."""
|
|
305
|
+
(tmp_path / "public").mkdir()
|
|
306
|
+
spec = tmp_path / "public" / "openapi.json"
|
|
307
|
+
spec.write_text('{"openapi":"3.0.0"}')
|
|
308
|
+
(tmp_path / ".descry.toml").write_text(
|
|
309
|
+
f'[cross_lang]\nopenapi_path = "{spec}"\n'
|
|
310
|
+
)
|
|
311
|
+
config = DescryConfig(project_root=tmp_path)
|
|
312
|
+
config._apply_toml(DescryConfig._load_toml(tmp_path))
|
|
313
|
+
assert config.openapi_path == spec.resolve()
|
|
314
|
+
|
|
271
315
|
|
|
272
316
|
# --- Precedence ---
|
|
273
317
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|