wayfind 0.1.0__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.
@@ -0,0 +1,38 @@
1
+ name: release
2
+ on:
3
+ push:
4
+ tags:
5
+ - 'v*'
6
+ workflow_dispatch:
7
+
8
+ # Trusted publishing requires `id-token: write`; no token is stored in the repo.
9
+ # See https://docs.pypi.org/trusted-publishers/
10
+ permissions:
11
+ contents: read
12
+ id-token: write
13
+
14
+ jobs:
15
+ build-and-publish:
16
+ name: Build and publish to PyPI
17
+ runs-on: ubuntu-latest
18
+ environment:
19
+ name: pypi
20
+ url: https://pypi.org/project/wayfind/
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: astral-sh/setup-uv@v6
24
+
25
+ - name: The tag matches the pyproject version
26
+ run: |
27
+ TAG="${GITHUB_REF_NAME#v}"
28
+ PYVER="$(grep -E '^version = ' pyproject.toml | sed -E 's/version = "(.*)"/\1/')"
29
+ if [ "$TAG" != "$PYVER" ]; then
30
+ echo "::error::tag $TAG does not match pyproject version $PYVER"
31
+ exit 1
32
+ fi
33
+
34
+ - run: uv sync --locked
35
+ - run: make check
36
+ - run: uv build
37
+ - name: Publish to PyPI (trusted publishing via uv)
38
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,13 @@
1
+ name: tests
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ check:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: astral-sh/setup-uv@v6
12
+ - run: uv sync --locked
13
+ - run: make check
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ dist/
4
+ *.egg-info/
@@ -0,0 +1,37 @@
1
+ # wayfind
2
+
3
+ A tree-sitter map of a git repository: layout, outlines, callers, impact, and
4
+ numbers (metrics, cycles, unused, diff) that report and never judge. Agents use
5
+ it to read a codebase; CI reads its numbers and holds the thresholds itself.
6
+ aegis depends on it from PyPI and mounts its CLI as `aegis map`.
7
+
8
+ ## What done means
9
+
10
+ - `make check` passes: format, lint, typecheck and the tests. CI runs it on
11
+ every push and PR.
12
+ - New behaviour ships with a test that builds a small git repository and runs
13
+ the CLI on it, as `tests/test_wayfind.py` does, watched failing first.
14
+ - A change to a command or its output updates `README.md`, which is the user
15
+ reference. aegis's help page `code-map` describes the same commands; a change
16
+ users of `aegis map` would notice gets a line there when aegis next bumps its
17
+ pin.
18
+ - English everywhere.
19
+
20
+ ## Where everything lives
21
+
22
+ | What | Where |
23
+ |---|---|
24
+ | Every command and what it prints | `README.md` |
25
+ | The design rules and their reasons | `DESIGN.md`, and the package docstring in `src/wayfind/__init__.py` |
26
+ | Open work | GitHub issues |
27
+ | Releases | `CHANGELOG.md`; a `vX.Y.Z` tag publishes to PyPI (`.github/workflows/release.yml`) |
28
+
29
+ ## Working here
30
+
31
+ ```bash
32
+ uv sync
33
+ make check
34
+ uv run wayfind repo # the map of this repository
35
+ ```
36
+
37
+ Work lands through issue, branch, PR and green CI.
@@ -0,0 +1,7 @@
1
+ # Changelog
2
+
3
+ ## v0.1.0
4
+
5
+ - First release as its own package. The map was `aegis map` in aegis up to
6
+ 2.11; it brings its history, the receiver types, `--exclude`, and the
7
+ `metrics`, `cycles`, `unused` and `diff` views.
@@ -0,0 +1,27 @@
1
+ # Design — wayfind
2
+
3
+ The rules and their reasons are in the package docstring
4
+ (`src/wayfind/__init__.py`); this file says how the modules divide the work.
5
+
6
+ ```
7
+ src/wayfind/parse.py one source file into definitions, references, facts and imports
8
+ src/wayfind/specs.py per-language query patterns: receiver types, bases, branches
9
+ src/wayfind/index.py the shared, mtime-refreshed index and name resolution
10
+ src/wayfind/measures.py metrics, cycles, unused and diff over the index
11
+ src/wayfind/views.py the plain-text views
12
+ src/wayfind/cli.py one typer subcommand per view; `app` is what aegis mounts
13
+ ```
14
+
15
+ The index is one pickle per repository under the user's cache directory
16
+ (`$XDG_CACHE_HOME/wayfind`), refreshed by mtime and size and replaced
17
+ atomically, so every process of the user shares it. A cold index parses in a
18
+ pool of processes.
19
+
20
+ It needs no language server. An edge found by name only is printed with `?`;
21
+ deeper levels of `callers` and `impact` follow sure edges only. What a language
22
+ adds over name matching, receiver types, base classes and an exact branch
23
+ count, is one `Spec` of query patterns in `specs.py`, so a language is data,
24
+ not code.
25
+
26
+ The numbers views report and never judge. A threshold belongs to whoever reads
27
+ them: a `jq` filter, a review, or a CI linter reading `--json`.
wayfind-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Piad Morffis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
wayfind-0.1.0/Makefile ADDED
@@ -0,0 +1,15 @@
1
+ .PHONY: check test lint format-check typecheck
2
+
3
+ check: format-check lint typecheck test
4
+
5
+ test:
6
+ uv run pytest -q
7
+
8
+ lint:
9
+ uv run ruff check src/ tests/
10
+
11
+ format-check:
12
+ uv run ruff format --check src/ tests/
13
+
14
+ typecheck:
15
+ uv run ty check src/
wayfind-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: wayfind
3
+ Version: 0.1.0
4
+ Summary: A tree-sitter map of a git repository: layout, outlines, callers, impact and numbers, for agents and CI
5
+ Project-URL: Repository, https://github.com/apiad/wayfind
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.13
9
+ Requires-Dist: tree-sitter-language-pack>=1.22
10
+ Requires-Dist: tree-sitter>=0.25
11
+ Requires-Dist: typer>=0.24.1
12
+ Description-Content-Type: text/markdown
13
+
14
+ # wayfind
15
+
16
+ A tree-sitter map of a git repository, for agents and for CI. It started as
17
+ `aegis map` in [aegis](https://github.com/apiad/aegis), which now uses it as a
18
+ library and mounts the same commands under that name.
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ uv tool install wayfind # or: uvx wayfind repo
24
+ ```
25
+
26
+ `wayfind` reads a git repository with tree-sitter and answers what grep
27
+ cannot say it answered completely: the repository's layout with each module's
28
+ first doc line, a file's outline, every caller of a definition, and everything
29
+ that depends on it. It runs in the shell, needs no server and works in any
30
+ language the pack of tree-sitter grammars has, from Python and TypeScript to
31
+ Go, Rust, Java and C.
32
+
33
+ ## Start with the layout
34
+
35
+ Run `wayfind repo` in the repository for its top folders, with how many
36
+ classes, functions and methods each holds. Give a folder and `--depth 2` to
37
+ open it further: each file then shows its first doc line, which is often
38
+ enough to know where to look. `wayfind outline <path>` lists a file's or a
39
+ folder's definitions with their line, signature and first doc line, and
40
+ `wayfind deps <path>` says which files use which.
41
+
42
+ Read a whole file only once the map has said it is the one you need.
43
+ `wayfind show <symbol>` prints one definition's source without the rest of
44
+ its file.
45
+
46
+ ## Name a definition
47
+
48
+ Every command that takes a symbol accepts a bare name (`save`), a class and
49
+ method (`Store.put`), or a path and a name (`pkg/store.py:helper`), where the
50
+ path may be any ending of the file's path. A name defined in several places
51
+ answers for each; `wayfind find <name>` lists them.
52
+
53
+ ## Callers and impact
54
+
55
+ `wayfind callers <symbol>` lists every call of a definition, grouped by
56
+ file, as the enclosing definition and its line. `--depth 2` also lists the
57
+ callers of those callers, indented. `wayfind impact <symbol>` counts every
58
+ use, not only calls, and follows three levels by default: what you would have
59
+ to check if you changed it, or what would break. `wayfind callees <symbol>` goes the other way.
60
+
61
+ A call on a receiver, such as `self.queues.turn_ended()`, resolves through the
62
+ receiver's type wherever the source states one: a parameter or variable
63
+ annotation, a constructor call assigned to it, a class field, an import under
64
+ another name. When nothing in the receiver's class types an attribute (it is
65
+ set to `None` there and assigned from outside), the map takes the one class
66
+ every assignment to an attribute of that name agrees on, preferring
67
+ assignments through a receiver of the same class, and leaving out tests' fakes
68
+ when it answers for code outside the tests. A call through a base class or
69
+ protocol method also reaches every override in a subclass, since that is what
70
+ may run: `callers` of an implementation lists the calls made through its
71
+ protocol.
72
+
73
+ An edge without a mark is sure. An edge ending in `?` is not: either the
74
+ receiver's type is stated nowhere, so the call matched every method of that
75
+ name, or the receiver may hold more than one class. A call on a receiver never
76
+ matches a module-level function by name, and a call on a type from outside the
77
+ repository (`Path`, `dict`, `json`) matches nothing. Deeper levels follow sure
78
+ edges only.
79
+
80
+ The receiver types work in Python, JavaScript and TypeScript. Other languages
81
+ get the rest of the map with calls matched by name; another language is
82
+ one more `Spec` in `src/wayfind/specs.py` (its docstring says how).
83
+
84
+ ## Numbers for a review
85
+
86
+ Four views give numbers and no verdicts. Which numbers matter, and where a
87
+ line falls, is yours to decide: each takes `--json`, and a gate or a review is
88
+ a filter over it.
89
+
90
+ - `wayfind metrics [path]` has one row per definition, file and
91
+ folder: lines, complexity (counted as ruff's C901 counts it for Python, and
92
+ as decision points for other languages), whether it has a doc, parameters
93
+ with a type out of all, a stated return type, uses of `Any`, calls on a
94
+ receiver of unknown type (the missing types that cost navigation), and
95
+ fan-in and fan-out. Folders carry which folders they use. The text view
96
+ ranks the top rows; `--sort complexity` or `fan-in` and `--top` change it.
97
+ - `wayfind cycles [path]` lists the files, or with `--level package` the
98
+ folders, that use each other in a cycle, with one cycle spelled out. In
99
+ Python a use in an annotation counts.
100
+ - `wayfind unused [path]` lists definitions no reference names outside
101
+ their own body, and those only tests name. It goes by name, so a function
102
+ reached through a string (a `getattr` dispatch, a route table, HTML) is
103
+ listed; dunders, decorated registrations, overrides, handlers assigned to
104
+ `on…` properties and members of object literals are left out.
105
+ - `wayfind diff [base]` reads a branch: every definition the branch touched
106
+ since `base` (by default the merge-base with `origin/main`, uncommitted work
107
+ included), whether it was added, removed, moved or modified, its lines and
108
+ complexity before and after, the sure users outside the branch, and for a
109
+ removed one every place that still names it.
110
+
111
+ For example, the definitions over 50 lines and those ruff's C901 would flag:
112
+
113
+ ```bash
114
+ wayfind metrics src --json | jq '[.functions[] | select(.lines > 50)] | length'
115
+ wayfind metrics src --json | jq -r '.functions[] | select(.complexity > 10) | .id'
116
+ ```
117
+
118
+ ## The index
119
+
120
+ The first command in a repository parses every file `git ls-files` lists in
121
+ a known language, in a pool of processes. That takes a few seconds on a
122
+ thousand files, and stderr says how many it parsed. Ignored files are never
123
+ indexed; `--exclude <path or glob>` (or `-x`, repeatable) leaves out more,
124
+ such as a `legacy/` folder or generated code, from the views and from what a
125
+ name may match. An excluded file keeps its place in the index, so changing
126
+ the exclusions re-parses nothing. After that each command re-parses only the
127
+ files whose modification time or size changed, so a query costs about a
128
+ second. The index lives in the user's cache directory, one file per
129
+ repository, and every session of the same user shares it.
130
+
131
+ Each grammar downloads the first time the map meets its language. On a host
132
+ without internet those files are skipped, and stderr names how many of which
133
+ language.
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ uv sync
139
+ make check # format, lint, typecheck, tests
140
+ ```
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,131 @@
1
+ # wayfind
2
+
3
+ A tree-sitter map of a git repository, for agents and for CI. It started as
4
+ `aegis map` in [aegis](https://github.com/apiad/aegis), which now uses it as a
5
+ library and mounts the same commands under that name.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ uv tool install wayfind # or: uvx wayfind repo
11
+ ```
12
+
13
+ `wayfind` reads a git repository with tree-sitter and answers what grep
14
+ cannot say it answered completely: the repository's layout with each module's
15
+ first doc line, a file's outline, every caller of a definition, and everything
16
+ that depends on it. It runs in the shell, needs no server and works in any
17
+ language the pack of tree-sitter grammars has, from Python and TypeScript to
18
+ Go, Rust, Java and C.
19
+
20
+ ## Start with the layout
21
+
22
+ Run `wayfind repo` in the repository for its top folders, with how many
23
+ classes, functions and methods each holds. Give a folder and `--depth 2` to
24
+ open it further: each file then shows its first doc line, which is often
25
+ enough to know where to look. `wayfind outline <path>` lists a file's or a
26
+ folder's definitions with their line, signature and first doc line, and
27
+ `wayfind deps <path>` says which files use which.
28
+
29
+ Read a whole file only once the map has said it is the one you need.
30
+ `wayfind show <symbol>` prints one definition's source without the rest of
31
+ its file.
32
+
33
+ ## Name a definition
34
+
35
+ Every command that takes a symbol accepts a bare name (`save`), a class and
36
+ method (`Store.put`), or a path and a name (`pkg/store.py:helper`), where the
37
+ path may be any ending of the file's path. A name defined in several places
38
+ answers for each; `wayfind find <name>` lists them.
39
+
40
+ ## Callers and impact
41
+
42
+ `wayfind callers <symbol>` lists every call of a definition, grouped by
43
+ file, as the enclosing definition and its line. `--depth 2` also lists the
44
+ callers of those callers, indented. `wayfind impact <symbol>` counts every
45
+ use, not only calls, and follows three levels by default: what you would have
46
+ to check if you changed it, or what would break. `wayfind callees <symbol>` goes the other way.
47
+
48
+ A call on a receiver, such as `self.queues.turn_ended()`, resolves through the
49
+ receiver's type wherever the source states one: a parameter or variable
50
+ annotation, a constructor call assigned to it, a class field, an import under
51
+ another name. When nothing in the receiver's class types an attribute (it is
52
+ set to `None` there and assigned from outside), the map takes the one class
53
+ every assignment to an attribute of that name agrees on, preferring
54
+ assignments through a receiver of the same class, and leaving out tests' fakes
55
+ when it answers for code outside the tests. A call through a base class or
56
+ protocol method also reaches every override in a subclass, since that is what
57
+ may run: `callers` of an implementation lists the calls made through its
58
+ protocol.
59
+
60
+ An edge without a mark is sure. An edge ending in `?` is not: either the
61
+ receiver's type is stated nowhere, so the call matched every method of that
62
+ name, or the receiver may hold more than one class. A call on a receiver never
63
+ matches a module-level function by name, and a call on a type from outside the
64
+ repository (`Path`, `dict`, `json`) matches nothing. Deeper levels follow sure
65
+ edges only.
66
+
67
+ The receiver types work in Python, JavaScript and TypeScript. Other languages
68
+ get the rest of the map with calls matched by name; another language is
69
+ one more `Spec` in `src/wayfind/specs.py` (its docstring says how).
70
+
71
+ ## Numbers for a review
72
+
73
+ Four views give numbers and no verdicts. Which numbers matter, and where a
74
+ line falls, is yours to decide: each takes `--json`, and a gate or a review is
75
+ a filter over it.
76
+
77
+ - `wayfind metrics [path]` has one row per definition, file and
78
+ folder: lines, complexity (counted as ruff's C901 counts it for Python, and
79
+ as decision points for other languages), whether it has a doc, parameters
80
+ with a type out of all, a stated return type, uses of `Any`, calls on a
81
+ receiver of unknown type (the missing types that cost navigation), and
82
+ fan-in and fan-out. Folders carry which folders they use. The text view
83
+ ranks the top rows; `--sort complexity` or `fan-in` and `--top` change it.
84
+ - `wayfind cycles [path]` lists the files, or with `--level package` the
85
+ folders, that use each other in a cycle, with one cycle spelled out. In
86
+ Python a use in an annotation counts.
87
+ - `wayfind unused [path]` lists definitions no reference names outside
88
+ their own body, and those only tests name. It goes by name, so a function
89
+ reached through a string (a `getattr` dispatch, a route table, HTML) is
90
+ listed; dunders, decorated registrations, overrides, handlers assigned to
91
+ `on…` properties and members of object literals are left out.
92
+ - `wayfind diff [base]` reads a branch: every definition the branch touched
93
+ since `base` (by default the merge-base with `origin/main`, uncommitted work
94
+ included), whether it was added, removed, moved or modified, its lines and
95
+ complexity before and after, the sure users outside the branch, and for a
96
+ removed one every place that still names it.
97
+
98
+ For example, the definitions over 50 lines and those ruff's C901 would flag:
99
+
100
+ ```bash
101
+ wayfind metrics src --json | jq '[.functions[] | select(.lines > 50)] | length'
102
+ wayfind metrics src --json | jq -r '.functions[] | select(.complexity > 10) | .id'
103
+ ```
104
+
105
+ ## The index
106
+
107
+ The first command in a repository parses every file `git ls-files` lists in
108
+ a known language, in a pool of processes. That takes a few seconds on a
109
+ thousand files, and stderr says how many it parsed. Ignored files are never
110
+ indexed; `--exclude <path or glob>` (or `-x`, repeatable) leaves out more,
111
+ such as a `legacy/` folder or generated code, from the views and from what a
112
+ name may match. An excluded file keeps its place in the index, so changing
113
+ the exclusions re-parses nothing. After that each command re-parses only the
114
+ files whose modification time or size changed, so a query costs about a
115
+ second. The index lives in the user's cache directory, one file per
116
+ repository, and every session of the same user shares it.
117
+
118
+ Each grammar downloads the first time the map meets its language. On a host
119
+ without internet those files are skipped, and stderr names how many of which
120
+ language.
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ uv sync
126
+ make check # format, lint, typecheck, tests
127
+ ```
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "wayfind"
3
+ version = "0.1.0"
4
+ description = "A tree-sitter map of a git repository: layout, outlines, callers, impact and numbers, for agents and CI"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "tree-sitter>=0.25",
10
+ "tree-sitter-language-pack>=1.22",
11
+ "typer>=0.24.1",
12
+ ]
13
+
14
+ [project.scripts]
15
+ wayfind = "wayfind.cli:app"
16
+
17
+ [project.urls]
18
+ Repository = "https://github.com/apiad/wayfind"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "ruff>=0.15.9,<0.16",
27
+ "ty==0.0.29",
28
+ "pytest>=8.0.0",
29
+ ]
30
+
31
+ [tool.ruff]
32
+ target-version = "py313"
33
+
34
+ [tool.pytest.ini_options]
35
+ testpaths = ["tests"]
@@ -0,0 +1,32 @@
1
+ """wayfind: a tree-sitter map of a git repository, for agents.
2
+
3
+ It answers what grep cannot say it answered completely: the repository's
4
+ layout with each module's first doc line, a file's outline, and every caller
5
+ of a symbol and everything that depends on it.
6
+
7
+ The rules, with their reasons:
8
+
9
+ - **Heuristics, no language server.** tree-sitter parses any of the pack's
10
+ languages in milliseconds and needs nothing installed per language. The
11
+ pack's tags queries give definitions and calls for the languages that have
12
+ them; any other language falls back to nodes whose type reads like a
13
+ definition (parse.py).
14
+ - **Every edge says how it was found.** An edge is *sure* when the name is
15
+ defined in the same file, when the using file's imports name both the
16
+ target's module and the name, or when the receiver's type is stated
17
+ (index.py says where from). Anything else is a name match, printed with
18
+ ``?``. Measured against jedi on aegis, where this map began, sure edges were right 96% of
19
+ the time and both kinds together found 97% of real references; the ``?``
20
+ edges were mostly common method names, so ``impact`` expands through sure
21
+ edges only (index.py).
22
+ - **One index per repository, shared, refreshed by mtime.** Every session of
23
+ the user reads the same cache file, and each query re-parses only the files
24
+ whose mtime or size changed, so a warm query costs a stat per file. The file
25
+ is replaced atomically, so two sessions writing it at once lose a refresh,
26
+ never the index.
27
+ - **Numbers, not verdicts.** metrics, cycles, unused and diff report per
28
+ definition, file and folder, with ``--json``; thresholds belong to whoever
29
+ reads them (measures.py).
30
+ - **A CLI, not MCP tools.** Every MCP tool's schema rides on every request of
31
+ every session; a subcommand costs nothing until an agent runs it.
32
+ """