descry-codegraph 0.3.1__tar.gz → 0.3.2__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.3.1 → descry_codegraph-0.3.2}/CHANGELOG.md +93 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/CLAUDE.md +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/PKG-INFO +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/pyproject.toml +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/__init__.py +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/_graph.py +14 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/cli.py +6 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/embeddings.py +10 -5
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/generate.py +2 -11
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/git_history.py +40 -33
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/handlers.py +81 -34
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/mcp_server.py +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/query.py +562 -718
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/web/server.py +26 -9
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/web/web/index.html +32 -8
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_cli.py +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_embeddings.py +17 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_git_history.py +16 -2
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_handlers.py +116 -5
- descry_codegraph-0.3.2/tests/test_interfaces.py +296 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_mcp.py +1 -1
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_query.py +638 -63
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_web_server.py +59 -6
- descry_codegraph-0.3.1/tests/test_interfaces.py +0 -178
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/.github/assets/01-landing.png +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/.github/workflows/ci.yml +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/.github/workflows/release.yml +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/.gitignore +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/LICENSE +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/README.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/SECURITY.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/THIRD-PARTY-NOTICES.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/claude-skill/README.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/claude-skill/descry/SKILL.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/claude-skill/descry/references/cli-reference.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/claude-skill/descry/references/mcp-tools.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/claude-skill/descry/references/recipes.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/justfile +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/pi-extension/README.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/pi-extension/descry-cli.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/pi-extension/descry-tools.ts +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/_comments.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/_env.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/_files.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/_stdlib.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/clang_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/cross_lang.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/dart_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/dotnet_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/go_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/java_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/php_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/ruby_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/__init__.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapter.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/__init__.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/_empty_environment.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/_java_compat.init.gradle +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/clang.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/dart.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/dotnet.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/go.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/java.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/php.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/python.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/ruby.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/rust.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/adapters/typescript.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/cache.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/pb2.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/scip/support.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/c.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/cpp.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/dart.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/dotnet.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/go.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/java.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/node.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/php.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/python.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/ruby.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/rust.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/stdlib/typescript.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/web/__init__.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/src/descry/web/web/style.css +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/__init__.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/conftest.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/README.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/__init__.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/compare.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/corpus.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/metrics.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/registry.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/results/results-descry.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/eval/results/results-ripgrep.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/fixtures/small_graph.json +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_backtracking.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_clang_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_config.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_cross_lang.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_dart_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_dotnet_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_generate.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_go_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_hardening.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_java_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_non_regular_files.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_php_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_ruby_parser.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_cache.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_clang.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_dart.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_dotnet.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_go.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_java.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_php.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_scip_ruby.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_stdlib.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tests/test_written_through.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/README.md +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/common.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_c.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_cpp.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_dart.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_dotnet.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_go.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_java.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_node.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_php.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_python.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_ruby.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_rust.py +0 -0
- {descry_codegraph-0.3.1 → descry_codegraph-0.3.2}/tools/stdlib/gen_typescript.py +0 -0
|
@@ -6,6 +6,99 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] — 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A caller that reaches a symbol only as one of an unresolved call's
|
|
14
|
+
candidates is counted apart from its callers. `in_degree` ("N callers")
|
|
15
|
+
counts the callers whose calls are resolved to the symbol, and is
|
|
16
|
+
counted again as a graph loads, so a graph from 0.3.1 needs no reindex.
|
|
17
|
+
Every other count gives the others as "(+N possible)": brief `context`
|
|
18
|
+
(callers and callees), `callers`, `callees`, and the web page's headers
|
|
19
|
+
(`possible` in the web API). Lists mark each one; `changes` does so in
|
|
20
|
+
text, in its structured form (`candidates`, `callers_possible`) and on
|
|
21
|
+
the page.
|
|
22
|
+
- A function no longer counts as a certain caller of a symbol it defines
|
|
23
|
+
when its call to it is unresolved.
|
|
24
|
+
- A node ID names that node alone: `callers`, `callees`, `quick`, `flow`
|
|
25
|
+
and `path` given one no longer also match other definitions filed under
|
|
26
|
+
the same owner and name (a Ruby `self.x` beside `x`).
|
|
27
|
+
- `context`'s usage examples are calls of that symbol, those resolved to
|
|
28
|
+
it first, then those it is one of the candidates of, marked; they were
|
|
29
|
+
any unresolved call ending in its name.
|
|
30
|
+
- `callers` of a name lists the calls the resolver left unresolved with no
|
|
31
|
+
candidates apart from its callers, marked "(unresolved; same name)" and
|
|
32
|
+
counted as "+N unresolved" (`unresolved` in the web API). Some are calls
|
|
33
|
+
of the project's definition the resolver cannot follow (a Rust re-export,
|
|
34
|
+
a TypeScript static member), some are calls of something else with that
|
|
35
|
+
name (`loader.list_templates()` on a library's loader). Neither is
|
|
36
|
+
counted as a caller. An unresolved call with candidates reaches only
|
|
37
|
+
those. Callers asked for by node ID are those resolved to it or narrowed
|
|
38
|
+
to it.
|
|
39
|
+
- A list cut short (`callers`, `callees`, `context`'s callers, `changes`,
|
|
40
|
+
backward `flow` in text and as a tree) keeps the callers resolved to the
|
|
41
|
+
symbol first, and says how many it leaves out and how many of those only
|
|
42
|
+
may be; a `flow` tree node gives them as `more` and `more_possible`, and
|
|
43
|
+
the web API's `callers` and `callees` as `more`, shown on the page.
|
|
44
|
+
- `flow`, in text and as a tree, places each symbol at the first level
|
|
45
|
+
that reaches it and marks it "(at level N)" where it is met again
|
|
46
|
+
(`seen`, that level, in the tree); a direct callee or caller reached
|
|
47
|
+
first through another one was dropped from its level, with everything
|
|
48
|
+
below it. `context`, at every depth, lists all of the symbol's own
|
|
49
|
+
callees and cuts each level below to the per-level limit with a note; it
|
|
50
|
+
opens a callee (inlines it and lists its own callees) at the first level
|
|
51
|
+
that reaches it while the token budget covers it, lists every other
|
|
52
|
+
callee by its signature or "(at level N)", and says when the budget, the
|
|
53
|
+
node limit or the timeout cut it short. Depth 1 ignored `max_tokens`. A
|
|
54
|
+
`flow` that stops early says so: "(timeout)" or "(node limit)" in text,
|
|
55
|
+
`cut` in the tree, a note on the page.
|
|
56
|
+
- A `flow` of `depth` N shows N levels of calls below the start; `depth=1`
|
|
57
|
+
showed the start alone. `[query] max_depth` is that many levels too, for
|
|
58
|
+
`flow` and for `context`'s callee expansion. A depth past it is traced to
|
|
59
|
+
it and says so; the web API returns the depth traced, and `/api/health`
|
|
60
|
+
gives the ceiling the page's depth field is held to.
|
|
61
|
+
- A callee the resolver left unresolved is no longer shown or counted as
|
|
62
|
+
certain or as external. `context`, at every depth, names it as one of
|
|
63
|
+
its candidates, or "(unresolved)" where it found none. `callees` marks
|
|
64
|
+
it the same way and counts it apart ("+N unresolved"). A callee is what
|
|
65
|
+
a call is written as: calls written apart (`a.add`, `b.add`) are two
|
|
66
|
+
callees, and calls written alike may reach any definition one of them
|
|
67
|
+
is narrowed to. Forward `flow` follows a possible callee to each
|
|
68
|
+
definition it may reach, marked, and `context` at a depth past 1 says
|
|
69
|
+
how many callees each level leaves out.
|
|
70
|
+
- `callees` says how many callees its `limit` leaves out, and of a name
|
|
71
|
+
several definitions share, which one it answers for (`others` in the web
|
|
72
|
+
API).
|
|
73
|
+
- A `limit`, a `flow` or `context` `depth`, a `context` `max_tokens` or
|
|
74
|
+
`callee_budget`, or a `path` `max_depth` below 1 is refused by every
|
|
75
|
+
tool; `limit=0` answered as if there were no
|
|
76
|
+
callers, matches, changes or history.
|
|
77
|
+
- The web page's changes view no longer throws for a changed symbol with
|
|
78
|
+
no callers.
|
|
79
|
+
- `context --max-output-tokens` cuts the parts the answer is made of, not
|
|
80
|
+
its text: expanded callees, then usage examples past 2, related tests
|
|
81
|
+
and configuration, callers past 5 (still counting possible callers
|
|
82
|
+
apart), the symbol's callees past 5 (planned again for those five, so
|
|
83
|
+
the note counts by kind only callees drawn nowhere in the answer and no
|
|
84
|
+
"(at level N)" mark points at a callee cut away), and the source past 30
|
|
85
|
+
of the symbol's lines, saying what each step left out; a section dropped
|
|
86
|
+
whole keeps its heading with "(omitted, max_output_tokens limit)".
|
|
87
|
+
It read the rendered text back, so code with a heading-like or a fence
|
|
88
|
+
line in it was cut wrongly or not at all, opened callees kept their
|
|
89
|
+
headings without their code, and counts were wrong.
|
|
90
|
+
- `flow` text indents the code it inlines as deep as its fence. The code
|
|
91
|
+
was printed from the first column inside an indented fence, so it broke
|
|
92
|
+
the tree, and below the first level the fence read as an indented code
|
|
93
|
+
block.
|
|
94
|
+
- A command that loads no embedding model no longer imports
|
|
95
|
+
sentence-transformers, and with it torch, as the service starts: where
|
|
96
|
+
they are installed every CLI command spent seconds on it.
|
|
97
|
+
- `churn` ranks symbols, and co-change pairs of symbols, with the same
|
|
98
|
+
number of commits by node ID, so the same history gives the same list.
|
|
99
|
+
Their order among ties changed from run to run, and so, at the limit,
|
|
100
|
+
did which of them were listed.
|
|
101
|
+
|
|
9
102
|
## [0.3.1] — 2026-10-22
|
|
10
103
|
|
|
11
104
|
### Fixed
|
|
@@ -23,7 +23,7 @@ just test tests/test_handlers.py::test_index # one test
|
|
|
23
23
|
just test -k "cross_lang and not slow" # by expression
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Python 3.11+ required (enforced by `pyproject.toml`). Pre-commit gate: `just lint && just fmt && just test` all pass (
|
|
26
|
+
Python 3.11+ required (enforced by `pyproject.toml`). Pre-commit gate: `just lint && just fmt && just test` all pass (2810 tests currently — a drop is a regression signal).
|
|
27
27
|
|
|
28
28
|
Invoke the tool while developing:
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: descry-codegraph
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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
|
|
@@ -112,4 +112,18 @@ def load_graph_with_schema(path: Path | str) -> dict:
|
|
|
112
112
|
f"run 'descry index' to rebuild."
|
|
113
113
|
)
|
|
114
114
|
_check_shape(data)
|
|
115
|
+
count_callers(data["nodes"], data["edges"])
|
|
115
116
|
return data
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def count_callers(nodes: list, edges: list) -> None:
|
|
120
|
+
"""Set each node's `in_degree` to the number of its callers: the
|
|
121
|
+
symbols whose calls are resolved to it, not those an unresolved call
|
|
122
|
+
only may be. A graph is counted again as it loads, so one written by an
|
|
123
|
+
older descry counts them the same way."""
|
|
124
|
+
callers: dict[str, set] = {}
|
|
125
|
+
for edge in edges:
|
|
126
|
+
if edge["relation"] in CALL_RELATIONS:
|
|
127
|
+
callers.setdefault(edge["target"], set()).add(edge["source"])
|
|
128
|
+
for node in nodes:
|
|
129
|
+
node["metadata"]["in_degree"] = len(callers.get(node["id"], ()))
|
|
@@ -302,7 +302,12 @@ def main():
|
|
|
302
302
|
choices=["forward", "backward"],
|
|
303
303
|
help="Trace direction (default: forward)",
|
|
304
304
|
)
|
|
305
|
-
sub.add_argument(
|
|
305
|
+
sub.add_argument(
|
|
306
|
+
"--depth",
|
|
307
|
+
type=int,
|
|
308
|
+
default=3,
|
|
309
|
+
help="Levels of calls below the start (default: 3)",
|
|
310
|
+
)
|
|
306
311
|
sub.add_argument("--target", default=None, help="Target symbol to reach")
|
|
307
312
|
sub.add_argument(
|
|
308
313
|
"--inline-threshold",
|
|
@@ -12,6 +12,7 @@ Usage:
|
|
|
12
12
|
|
|
13
13
|
import contextlib
|
|
14
14
|
import hashlib
|
|
15
|
+
import importlib.util
|
|
15
16
|
import json
|
|
16
17
|
import logging
|
|
17
18
|
import os
|
|
@@ -347,13 +348,14 @@ def _resolve_cache_dir(graph_path: Path, cache_dir: str | None = None) -> Path:
|
|
|
347
348
|
# Try to import embedding dependencies
|
|
348
349
|
try:
|
|
349
350
|
import numpy as np
|
|
350
|
-
from sentence_transformers import SentenceTransformer
|
|
351
|
-
|
|
352
|
-
EMBEDDINGS_AVAILABLE = True
|
|
353
351
|
except ImportError:
|
|
354
|
-
EMBEDDINGS_AVAILABLE = False
|
|
355
352
|
np = None
|
|
356
|
-
|
|
353
|
+
# Imported by _load_sentence_transformer: it brings in torch, which takes
|
|
354
|
+
# seconds a command that loads no model should not spend.
|
|
355
|
+
SentenceTransformer = None
|
|
356
|
+
EMBEDDINGS_AVAILABLE = (
|
|
357
|
+
np is not None and importlib.util.find_spec("sentence_transformers") is not None
|
|
358
|
+
)
|
|
357
359
|
|
|
358
360
|
|
|
359
361
|
def embeddings_available() -> bool:
|
|
@@ -376,6 +378,9 @@ def _load_sentence_transformer(model_name: str, revision: str | None = None):
|
|
|
376
378
|
model_name: Registry alias, HuggingFace repo id, or local path.
|
|
377
379
|
revision: Overrides the registry's pinned revision.
|
|
378
380
|
"""
|
|
381
|
+
global SentenceTransformer
|
|
382
|
+
if SentenceTransformer is None:
|
|
383
|
+
from sentence_transformers import SentenceTransformer
|
|
379
384
|
spec = resolve_model_spec(model_name)
|
|
380
385
|
target = spec.repo_id if spec else model_name
|
|
381
386
|
if revision is None and spec:
|
|
@@ -25433,19 +25433,10 @@ class CodeGraphBuilder:
|
|
|
25433
25433
|
output_path: Path to write the graph JSON
|
|
25434
25434
|
scip_index: Optional ScipIndex for type-aware resolution
|
|
25435
25435
|
"""
|
|
25436
|
-
from descry._graph import
|
|
25436
|
+
from descry._graph import CURRENT_SCHEMA, count_callers
|
|
25437
25437
|
|
|
25438
25438
|
self.resolve_references(scip_index=scip_index)
|
|
25439
|
-
|
|
25440
|
-
# the candidates of
|
|
25441
|
-
callers = {}
|
|
25442
|
-
for edge in self.edges:
|
|
25443
|
-
if edge["relation"] in CALL_RELATIONS:
|
|
25444
|
-
candidates = edge.get("metadata", {}).get("candidates", ())
|
|
25445
|
-
for target in (edge["target"], *candidates):
|
|
25446
|
-
callers.setdefault(target, set()).add(edge["source"])
|
|
25447
|
-
for node in self.nodes:
|
|
25448
|
-
node["metadata"]["in_degree"] = len(callers.get(node["id"], ()))
|
|
25439
|
+
count_callers(self.nodes, self.edges)
|
|
25449
25440
|
# Atomic write: stream to a sibling tmp file then os.replace. A
|
|
25450
25441
|
# concurrent reader (MCP prewarm, web `/api/health`, another CLI
|
|
25451
25442
|
# invocation) that stats the destination mid-write would otherwise
|
|
@@ -758,7 +758,7 @@ class GitHistoryAnalyzer:
|
|
|
758
758
|
|
|
759
759
|
# Sort by commit count
|
|
760
760
|
sorted_symbols = sorted(
|
|
761
|
-
symbol_commits.items(), key=lambda x: len(x[1]),
|
|
761
|
+
symbol_commits.items(), key=lambda x: (-len(x[1]), x[0])
|
|
762
762
|
)[:limit]
|
|
763
763
|
|
|
764
764
|
lines = []
|
|
@@ -923,7 +923,7 @@ class GitHistoryAnalyzer:
|
|
|
923
923
|
|
|
924
924
|
# SYMBOLS MODE
|
|
925
925
|
sorted_symbols = sorted(
|
|
926
|
-
symbol_commits.items(), key=lambda x: len(x[1]),
|
|
926
|
+
symbol_commits.items(), key=lambda x: (-len(x[1]), x[0])
|
|
927
927
|
)[:limit]
|
|
928
928
|
|
|
929
929
|
symbols = []
|
|
@@ -983,7 +983,7 @@ class GitHistoryAnalyzer:
|
|
|
983
983
|
frequent_pairs = [
|
|
984
984
|
(pair, count) for pair, count in pair_counts.items() if count >= 2
|
|
985
985
|
]
|
|
986
|
-
frequent_pairs.sort(key=lambda x: x[1],
|
|
986
|
+
frequent_pairs.sort(key=lambda x: (-x[1], x[0]))
|
|
987
987
|
|
|
988
988
|
for (sym_a, sym_b), count in frequent_pairs[:limit]:
|
|
989
989
|
_, name_a, loc_a = self._get_node_display(sym_a)
|
|
@@ -1081,7 +1081,7 @@ class GitHistoryAnalyzer:
|
|
|
1081
1081
|
frequent_pairs = [
|
|
1082
1082
|
(pair, count) for pair, count in pair_counts.items() if count >= 2
|
|
1083
1083
|
]
|
|
1084
|
-
frequent_pairs.sort(key=lambda x: x[1],
|
|
1084
|
+
frequent_pairs.sort(key=lambda x: (-x[1], x[0]))
|
|
1085
1085
|
frequent_pairs = frequent_pairs[:limit]
|
|
1086
1086
|
|
|
1087
1087
|
if frequent_pairs:
|
|
@@ -1660,26 +1660,43 @@ class GitHistoryAnalyzer:
|
|
|
1660
1660
|
f"(+{stats['added']}/-{stats['removed']})"
|
|
1661
1661
|
)
|
|
1662
1662
|
|
|
1663
|
-
# Show callers if requested
|
|
1664
1663
|
if show_callers and self.querier:
|
|
1665
|
-
|
|
1666
|
-
callers = self.querier.get_callers(node_id)
|
|
1664
|
+
callers, total, possible = self._symbol_callers(node_id)
|
|
1667
1665
|
if callers:
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
lines.append(f" Callers: {
|
|
1666
|
+
names = ", ".join(
|
|
1667
|
+
c["name"] + self.querier.one_of(c["candidates"])
|
|
1668
|
+
for c in callers
|
|
1669
|
+
)
|
|
1670
|
+
hidden = possible - sum(bool(c["candidates"]) for c in callers)
|
|
1671
|
+
hidden_certain = total - len(callers) - hidden
|
|
1672
|
+
more = (
|
|
1673
|
+
f"; {self.querier.more_callers(hidden_certain, hidden)}"
|
|
1674
|
+
if total > len(callers)
|
|
1675
|
+
else ""
|
|
1676
|
+
)
|
|
1677
|
+
lines.append(f" Callers: {names}{more}")
|
|
1680
1678
|
|
|
1681
1679
|
return "\n".join(lines)
|
|
1682
1680
|
|
|
1681
|
+
def _symbol_callers(self, node_id):
|
|
1682
|
+
"""The first five callers of the symbol ``node_id``, certain ones
|
|
1683
|
+
first, each ``{"id", "name", "candidates"}`` where ``candidates`` is
|
|
1684
|
+
the number an unresolved call reaching it only may be one of; the
|
|
1685
|
+
number of its callers; and how many of those only may be."""
|
|
1686
|
+
caller_ids, ambiguous = self.querier.ordered_callers(node_id)
|
|
1687
|
+
callers = []
|
|
1688
|
+
for caller_id in caller_ids[:5]:
|
|
1689
|
+
c_node = self.querier.get_node_info(caller_id)
|
|
1690
|
+
name = (
|
|
1691
|
+
c_node["metadata"].get("name", "?")
|
|
1692
|
+
if c_node
|
|
1693
|
+
else caller_id.split("::")[-1]
|
|
1694
|
+
)
|
|
1695
|
+
callers.append(
|
|
1696
|
+
{"id": caller_id, "name": name, "candidates": ambiguous.get(caller_id)}
|
|
1697
|
+
)
|
|
1698
|
+
return callers, len(caller_ids), len(ambiguous)
|
|
1699
|
+
|
|
1683
1700
|
def get_changes_structured(
|
|
1684
1701
|
self,
|
|
1685
1702
|
commit_range: str | None = None,
|
|
@@ -1742,21 +1759,11 @@ class GitHistoryAnalyzer:
|
|
|
1742
1759
|
}
|
|
1743
1760
|
|
|
1744
1761
|
if show_callers and self.querier:
|
|
1745
|
-
|
|
1746
|
-
if
|
|
1747
|
-
callers = []
|
|
1748
|
-
for caller_id in caller_ids[:5]:
|
|
1749
|
-
c_node = self.querier.get_node_info(caller_id)
|
|
1750
|
-
c_name = (
|
|
1751
|
-
c_node["metadata"].get("name", "?")
|
|
1752
|
-
if c_node
|
|
1753
|
-
else caller_id.split("::")[-1]
|
|
1754
|
-
if "::" in caller_id
|
|
1755
|
-
else "?"
|
|
1756
|
-
)
|
|
1757
|
-
callers.append({"id": caller_id, "name": c_name})
|
|
1762
|
+
callers, total, possible = self._symbol_callers(node_id)
|
|
1763
|
+
if callers:
|
|
1758
1764
|
entry["callers"] = callers
|
|
1759
|
-
entry["callers_total"] =
|
|
1765
|
+
entry["callers_total"] = total
|
|
1766
|
+
entry["callers_possible"] = possible
|
|
1760
1767
|
|
|
1761
1768
|
symbols.append(entry)
|
|
1762
1769
|
|
|
@@ -946,10 +946,12 @@ _GRAPH_NOT_FOUND = "Graph not found. Run descry ensure first."
|
|
|
946
946
|
_INDEX_BUSY = "An index run is already in progress"
|
|
947
947
|
|
|
948
948
|
|
|
949
|
-
def
|
|
950
|
-
"""Why
|
|
951
|
-
|
|
952
|
-
|
|
949
|
+
def limit_error(limit: int, name: str = "limit") -> str | None:
|
|
950
|
+
"""Why the limit or depth ``name`` cannot be ``limit``, or None: a
|
|
951
|
+
negative one slices from the end, and none at all answers as if there
|
|
952
|
+
were nothing to show."""
|
|
953
|
+
if limit < 1:
|
|
954
|
+
return f"{name} must be at least 1, got {limit}."
|
|
953
955
|
return None
|
|
954
956
|
|
|
955
957
|
|
|
@@ -1712,7 +1714,7 @@ class DescryService:
|
|
|
1712
1714
|
|
|
1713
1715
|
async def callers_data(self, name: str, limit: int = 20) -> dict:
|
|
1714
1716
|
"""Callers of a symbol, as data. An exact miss retries with fuzzy=True."""
|
|
1715
|
-
if error :=
|
|
1717
|
+
if error := limit_error(limit):
|
|
1716
1718
|
return {"ok": False, "error": error, "reason": "invalid"}
|
|
1717
1719
|
q = await self._get_querier()
|
|
1718
1720
|
if not q:
|
|
@@ -1721,15 +1723,11 @@ class DescryService:
|
|
|
1721
1723
|
|
|
1722
1724
|
@staticmethod
|
|
1723
1725
|
def _callers_from(q, name: str, limit: int) -> dict:
|
|
1724
|
-
all_callers = q.
|
|
1725
|
-
ambiguous = q.get_ambiguous_callers(name)
|
|
1726
|
+
all_callers, ambiguous = q.ordered_callers(name)
|
|
1726
1727
|
fuzzy = False
|
|
1727
1728
|
if not all_callers:
|
|
1728
|
-
all_callers = q.
|
|
1729
|
-
ambiguous = q.get_ambiguous_callers(name, fuzzy=True)
|
|
1729
|
+
all_callers, ambiguous = q.ordered_callers(name, fuzzy=True)
|
|
1730
1730
|
fuzzy = bool(all_callers)
|
|
1731
|
-
# Certain callers come before those that are one of several candidates.
|
|
1732
|
-
all_callers = sorted(all_callers, key=ambiguous.__contains__)
|
|
1733
1731
|
|
|
1734
1732
|
return {
|
|
1735
1733
|
"ok": True,
|
|
@@ -1738,12 +1736,13 @@ class DescryService:
|
|
|
1738
1736
|
"fuzzy": fuzzy,
|
|
1739
1737
|
"total": len(all_callers),
|
|
1740
1738
|
"callers": all_callers[:limit],
|
|
1741
|
-
"
|
|
1739
|
+
"left_out": all_callers[limit:],
|
|
1740
|
+
"ambiguous": ambiguous,
|
|
1742
1741
|
}
|
|
1743
1742
|
|
|
1744
1743
|
async def callees_data(self, name: str, limit: int = 20) -> dict:
|
|
1745
1744
|
"""Callees of a symbol, as data. Same fuzzy fallback as callers_data."""
|
|
1746
|
-
if error :=
|
|
1745
|
+
if error := limit_error(limit):
|
|
1747
1746
|
return {"ok": False, "error": error, "reason": "invalid"}
|
|
1748
1747
|
q = await self._get_querier()
|
|
1749
1748
|
if not q:
|
|
@@ -1784,12 +1783,15 @@ class DescryService:
|
|
|
1784
1783
|
"node": node,
|
|
1785
1784
|
"total": len(callees),
|
|
1786
1785
|
"callees": callees[:limit],
|
|
1787
|
-
"
|
|
1786
|
+
"left_out": callees[limit:],
|
|
1787
|
+
"ambiguous": ambiguous,
|
|
1788
|
+
# The definitions of the name besides the one answered for
|
|
1789
|
+
"others": len(func_matches) - 1,
|
|
1788
1790
|
}
|
|
1789
1791
|
|
|
1790
1792
|
async def callers(self, name: str, limit: int = 20) -> str:
|
|
1791
1793
|
"""Find all callers of a symbol."""
|
|
1792
|
-
if error :=
|
|
1794
|
+
if error := limit_error(limit):
|
|
1793
1795
|
return ServiceError(f"ERROR: {error}")
|
|
1794
1796
|
# One querier for lookup and rendering: a second fetch returns None
|
|
1795
1797
|
# if the graph file has been removed in between.
|
|
@@ -1808,7 +1810,11 @@ class DescryService:
|
|
|
1808
1810
|
f"No callers of '{name}'. Try descry search to verify symbol exists."
|
|
1809
1811
|
)
|
|
1810
1812
|
else:
|
|
1811
|
-
|
|
1813
|
+
certain, possible, unresolved = q.kinds(
|
|
1814
|
+
callers + data["left_out"], ambiguous
|
|
1815
|
+
)
|
|
1816
|
+
extra = q.possible(possible, unresolved)
|
|
1817
|
+
lines = [f"{certain} caller(s) of '{name}'{fuzzy_note}{extra}:"]
|
|
1812
1818
|
for caller in callers:
|
|
1813
1819
|
node_info = q.get_node_info(caller)
|
|
1814
1820
|
one_of = q.one_of(ambiguous.get(caller))
|
|
@@ -1822,16 +1828,16 @@ class DescryService:
|
|
|
1822
1828
|
else:
|
|
1823
1829
|
lines.append(f" {caller}{one_of}")
|
|
1824
1830
|
if len(callers) < total_count:
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
)
|
|
1831
|
+
certain, possible, unresolved = q.kinds(data["left_out"], ambiguous)
|
|
1832
|
+
more = q.more_callers(certain, possible, unresolved=unresolved)
|
|
1833
|
+
lines.append(f" ... ({more}, limit {limit})")
|
|
1828
1834
|
result = "\n".join(lines)
|
|
1829
1835
|
|
|
1830
1836
|
return await self._format_response(result)
|
|
1831
1837
|
|
|
1832
1838
|
async def callees(self, name: str, limit: int = 20) -> str:
|
|
1833
1839
|
"""Find what a symbol calls."""
|
|
1834
|
-
if error :=
|
|
1840
|
+
if error := limit_error(limit):
|
|
1835
1841
|
return ServiceError(f"ERROR: {error}")
|
|
1836
1842
|
q = await self._get_querier()
|
|
1837
1843
|
if not q:
|
|
@@ -1842,15 +1848,32 @@ class DescryService:
|
|
|
1842
1848
|
|
|
1843
1849
|
node = data["node"]
|
|
1844
1850
|
callees = data["callees"]
|
|
1851
|
+
total = data["total"]
|
|
1845
1852
|
ambiguous = data.get("ambiguous", {})
|
|
1846
1853
|
fuzzy_note = " (fuzzy match)" if data["fuzzy"] else ""
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1854
|
+
# A name several definitions have is answered for one, named by ID
|
|
1855
|
+
display_name = (
|
|
1856
|
+
node["id"] if data["others"] else node.get("metadata", {}).get("name", name)
|
|
1857
|
+
)
|
|
1858
|
+
others = (
|
|
1859
|
+
f"\n({data['others']} other "
|
|
1860
|
+
f"{'match(es) for' if data['fuzzy'] else 'definition(s) of'} '{name}'; "
|
|
1861
|
+
"use a node ID to choose)"
|
|
1862
|
+
if data["others"]
|
|
1863
|
+
else ""
|
|
1864
|
+
)
|
|
1865
|
+
if not total:
|
|
1866
|
+
result = f"'{display_name}'{fuzzy_note} calls no tracked functions.{others}"
|
|
1850
1867
|
else:
|
|
1851
|
-
|
|
1868
|
+
certain, possible, unresolved = q.kinds(
|
|
1869
|
+
callees + data["left_out"], ambiguous
|
|
1870
|
+
)
|
|
1871
|
+
possible = q.possible(possible, unresolved)
|
|
1872
|
+
lines = [
|
|
1873
|
+
f"'{display_name}'{fuzzy_note} calls {certain} function(s){possible}:"
|
|
1874
|
+
]
|
|
1852
1875
|
for callee in callees:
|
|
1853
|
-
one_of = q.
|
|
1876
|
+
one_of = q.callee_note(ambiguous.get(callee))
|
|
1854
1877
|
callee_info = q.get_node_info(callee)
|
|
1855
1878
|
if callee_info:
|
|
1856
1879
|
lineno = callee_info.get("metadata", {}).get("lineno")
|
|
@@ -1861,7 +1884,11 @@ class DescryService:
|
|
|
1861
1884
|
lines.append(f" {callee}{one_of}")
|
|
1862
1885
|
else:
|
|
1863
1886
|
lines.append(f" {callee}{one_of}")
|
|
1864
|
-
|
|
1887
|
+
if len(callees) < total:
|
|
1888
|
+
certain, possible, unresolved = q.kinds(data["left_out"], ambiguous)
|
|
1889
|
+
more = q.more_callers(certain, possible, "callees", unresolved)
|
|
1890
|
+
lines.append(f" ... ({more}, limit {limit})")
|
|
1891
|
+
result = "\n".join(lines) + others
|
|
1865
1892
|
|
|
1866
1893
|
return await self._format_response(result)
|
|
1867
1894
|
|
|
@@ -1879,6 +1906,13 @@ class DescryService:
|
|
|
1879
1906
|
max_output_tokens: int | None = None,
|
|
1880
1907
|
) -> str:
|
|
1881
1908
|
"""Get full context for a symbol."""
|
|
1909
|
+
for value, name in (
|
|
1910
|
+
(depth, "depth"),
|
|
1911
|
+
(max_tokens, "max_tokens"),
|
|
1912
|
+
(callee_budget, "callee_budget"),
|
|
1913
|
+
):
|
|
1914
|
+
if error := limit_error(value, name):
|
|
1915
|
+
return ServiceError(f"ERROR: {error}")
|
|
1882
1916
|
q = await self._get_querier()
|
|
1883
1917
|
if not q:
|
|
1884
1918
|
return ServiceError(f"ERROR: {self._no_graph_error()}")
|
|
@@ -1928,7 +1962,10 @@ class DescryService:
|
|
|
1928
1962
|
result, include_header=False, max_lines=50
|
|
1929
1963
|
)
|
|
1930
1964
|
|
|
1931
|
-
|
|
1965
|
+
traced = q.flow_depth(depth)
|
|
1966
|
+
depth_note = f" [depth={traced}]" if traced > 1 else ""
|
|
1967
|
+
if traced < depth:
|
|
1968
|
+
depth_note += f" [depth {depth} is past [query] max_depth]"
|
|
1932
1969
|
full_note = " [full=true]" if full else ""
|
|
1933
1970
|
head_note = f" [head_lines={head_lines}]" if head_lines else ""
|
|
1934
1971
|
expand_note = " [expand_callees=true]" if expand_callees else ""
|
|
@@ -1951,8 +1988,11 @@ class DescryService:
|
|
|
1951
1988
|
"""Call flow from a starting symbol, as data.
|
|
1952
1989
|
|
|
1953
1990
|
`result` is the markdown trace, or the tree of
|
|
1954
|
-
`GraphQuerier.trace_flow_structured` when `structured
|
|
1991
|
+
`GraphQuerier.trace_flow_structured` when `structured`; `depth` is
|
|
1992
|
+
the depth traced, at most the project's ceiling.
|
|
1955
1993
|
"""
|
|
1994
|
+
if error := limit_error(depth, "depth"):
|
|
1995
|
+
return {"ok": False, "error": error, "reason": "invalid"}
|
|
1956
1996
|
q = await self._get_querier()
|
|
1957
1997
|
if not q:
|
|
1958
1998
|
return {"ok": False, "error": self._no_graph_error()}
|
|
@@ -1965,7 +2005,12 @@ class DescryService:
|
|
|
1965
2005
|
target=target,
|
|
1966
2006
|
inline_threshold=inline_threshold,
|
|
1967
2007
|
)
|
|
1968
|
-
return {
|
|
2008
|
+
return {
|
|
2009
|
+
"ok": True,
|
|
2010
|
+
"error": None,
|
|
2011
|
+
"result": result,
|
|
2012
|
+
"depth": q.flow_depth(depth),
|
|
2013
|
+
}
|
|
1969
2014
|
|
|
1970
2015
|
async def flow(
|
|
1971
2016
|
self,
|
|
@@ -1998,7 +2043,7 @@ class DescryService:
|
|
|
1998
2043
|
Returns `{"ok", "error", "results", "method"}`; `method` is "keyword"
|
|
1999
2044
|
or "hybrid".
|
|
2000
2045
|
"""
|
|
2001
|
-
if error :=
|
|
2046
|
+
if error := limit_error(limit):
|
|
2002
2047
|
return {
|
|
2003
2048
|
"ok": False,
|
|
2004
2049
|
"error": error,
|
|
@@ -2334,8 +2379,8 @@ class DescryService:
|
|
|
2334
2379
|
reported as a missing graph; `reason` classifies it as "invalid",
|
|
2335
2380
|
"unavailable", "no_graph", "loading" or "failed".
|
|
2336
2381
|
"""
|
|
2337
|
-
if
|
|
2338
|
-
reason, error = "invalid",
|
|
2382
|
+
if refused := limit_error(limit):
|
|
2383
|
+
reason, error = "invalid", refused
|
|
2339
2384
|
elif not self._semantic_available or not self._SemanticSearcher:
|
|
2340
2385
|
reason, error = "unavailable", "Semantic search not available."
|
|
2341
2386
|
elif not self.config.graph_path.exists():
|
|
@@ -2644,6 +2689,8 @@ class DescryService:
|
|
|
2644
2689
|
`status` and `path` are those of `GraphQuerier.find_call_path_result`.
|
|
2645
2690
|
Unless `status` is "found", `message` says why there is no path.
|
|
2646
2691
|
"""
|
|
2692
|
+
if error := limit_error(max_depth, "max_depth"):
|
|
2693
|
+
return {"ok": False, "error": error, "reason": "invalid"}
|
|
2647
2694
|
q = await self._get_querier()
|
|
2648
2695
|
if not q:
|
|
2649
2696
|
return {"ok": False, "error": self._no_graph_error()}
|
|
@@ -2925,10 +2972,10 @@ class DescryService:
|
|
|
2925
2972
|
"""Run a GitHistoryAnalyzer method off the event loop, as data.
|
|
2926
2973
|
|
|
2927
2974
|
`result` is what the method returned. On failure `reason` is
|
|
2928
|
-
"invalid" for a
|
|
2975
|
+
"invalid" for a `limit` below 1, "unavailable" when the git history
|
|
2929
2976
|
module is missing and "failed" when the analysis raised.
|
|
2930
2977
|
"""
|
|
2931
|
-
if error :=
|
|
2978
|
+
if error := limit_error(kwargs["limit"]):
|
|
2932
2979
|
return {"ok": False, "error": error, "reason": "invalid"}
|
|
2933
2980
|
if not self._git_available:
|
|
2934
2981
|
return {
|
|
@@ -243,7 +243,7 @@ async def descry_flow(
|
|
|
243
243
|
target: str | None = None,
|
|
244
244
|
inline_threshold: int = 100,
|
|
245
245
|
) -> str:
|
|
246
|
-
"""Trace call flow from a starting symbol. Shows call chains with inline code for small functions. Use for 'how does X reach Y' queries, impact analysis, and understanding execution paths. Direction 'forward' traces callees (what this calls), 'backward' traces callers (what calls this)."""
|
|
246
|
+
"""Trace call flow from a starting symbol. Shows call chains with inline code for small functions. Use for 'how does X reach Y' queries, impact analysis, and understanding execution paths. Direction 'forward' traces callees (what this calls), 'backward' traces callers (what calls this). depth is the levels of calls below the start, at most the project's [query] max_depth (default 3)."""
|
|
247
247
|
return _fenced(
|
|
248
248
|
await _svc().flow(start, direction.value, depth, target, inline_threshold)
|
|
249
249
|
)
|