anatomize 0.2.0__tar.gz → 0.2.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.
- {anatomize-0.2.0 → anatomize-0.2.1}/.github/workflows/ci.yml +3 -3
- {anatomize-0.2.0 → anatomize-0.2.1}/.gitignore +4 -2
- {anatomize-0.2.0 → anatomize-0.2.1}/CHANGELOG.md +15 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/PKG-INFO +20 -10
- {anatomize-0.2.0 → anatomize-0.2.1}/README.md +19 -9
- {anatomize-0.2.0 → anatomize-0.2.1}/docs/GUIDE.md +16 -4
- {anatomize-0.2.0 → anatomize-0.2.1}/docs/REPOMIX_VS_ANATOMIZE.md +1 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/__init__.py +1 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/cli.py +49 -7
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/config.py +10 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/exclude.py +26 -3
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/formats/json_fmt.py +1 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/formats/markdown_fmt.py +1 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/formats/yaml_fmt.py +1 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/generators/main.py +6 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/deps.py +17 -1
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/discovery.py +51 -2
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/formats.py +52 -37
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/ignore.py +14 -7
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/jsonl.py +1 -3
- anatomize-0.2.1/src/anatomize/pack/overview.py +41 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/runner.py +241 -19
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/version.py +1 -1
- anatomize-0.2.1/tests/e2e/test_pack_explain_selection.py +113 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_pack_hybrid_cli.py +29 -14
- anatomize-0.2.1/tests/unit/test_pack_binary_sniff.py +47 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_discovery.py +29 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_overview.py +2 -6
- anatomize-0.2.1/tests/unit/test_pack_prefix_style.py +44 -0
- anatomize-0.2.0/src/anatomize/pack/overview.py +0 -134
- {anatomize-0.2.0 → anatomize-0.2.1}/.gitattributes +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/.github/workflows/publish-pypi.yml +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/.github/workflows/publish-testpypi.yml +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/CONTRIBUTING.md +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/LICENSE +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/SKILL.md +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/pyproject.toml +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/scripts/bench_pack.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/discovery.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/extractor.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/parser.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/policy.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/core/types.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/formats/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/formats/payloads.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/generators/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/compress.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/limit.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/match.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/mode.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/pyright_lsp.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/representations.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/slicing.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/summaries.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/tokens.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/tree.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/pack/uses.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/py.typed +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/schemas/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/schemas/hierarchy.schema.json +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/schemas/module.schema.json +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/src/anatomize/validation.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/README.md +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_cli.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_init.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_pack_cli.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_pack_pyright_uses.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/e2e/test_pack_removed_flags.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/project_src/src/excluded/skip.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/project_src/src/ns_pkg/sub/mod.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/project_src/src/pkg/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/project_src/src/pkg/mod.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/project_src/src/top_level.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/sample_package/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/fixtures/sample_package/models.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/integration/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/__init__.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_config.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_discovery.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_exclude.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_extractor.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_formats.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_generator.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_deps.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_hybrid.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_limit.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pack_output_safety.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_parser.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_pyright_lsp_uri.py +0 -0
- {anatomize-0.2.0 → anatomize-0.2.1}/tests/unit/test_uses_symbol_positions.py +0 -0
|
@@ -48,8 +48,8 @@ jobs:
|
|
|
48
48
|
. .venv-smoke/bin/activate
|
|
49
49
|
python -m pip install -U pip
|
|
50
50
|
python -m pip install dist/*.whl
|
|
51
|
-
anatomize generate tests/fixtures/project_src/src --output .
|
|
52
|
-
anatomize validate .
|
|
51
|
+
anatomize generate tests/fixtures/project_src/src --output .anatomy-smoke -f json
|
|
52
|
+
anatomize validate .anatomy-smoke --source tests/fixtures/project_src/src
|
|
53
53
|
|
|
54
54
|
pyright-e2e:
|
|
55
55
|
runs-on: ubuntu-latest
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
- name: Install Pyright
|
|
70
70
|
run: |
|
|
71
71
|
npm i -g pyright
|
|
72
|
-
pyright
|
|
72
|
+
command -v pyright
|
|
73
73
|
command -v pyright-langserver
|
|
74
74
|
|
|
75
75
|
- name: Install
|
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.2.1 - 2026-02-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `anatomize pack --prefix standard|minimal` to control pack prefix verbosity (token overhead vs guidance).
|
|
9
|
+
- `anatomize pack --explain-selection` to write a deterministic selection report for debugging include/ignore/slice behavior.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Hybrid mode now supports markdown/plain output (JSONL is optional, and required only for `--fit-to-max-output`).
|
|
13
|
+
- Pack ignore rules now retain rule provenance (default vs standard ignore files vs CLI) for selection reporting.
|
|
14
|
+
- Binary sniffing reads only a small prefix for performance (instead of reading entire files).
|
|
15
|
+
- Dependency-closure failures now include an import chain for faster debugging.
|
|
16
|
+
|
|
17
|
+
### Infrastructure
|
|
18
|
+
- CI Pyright job avoids invoking commands that require an LSP transport without `--stdio`.
|
|
19
|
+
|
|
5
20
|
## 0.2.0 - 2026-02-01
|
|
6
21
|
|
|
7
22
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: anatomize
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Deterministic, token-efficient codebase packs and skeleton maps for AI review (Python)
|
|
5
5
|
Project-URL: Homepage, https://github.com/BradSegal/anatomize
|
|
6
6
|
Project-URL: Documentation, https://github.com/BradSegal/anatomize#readme
|
|
@@ -114,6 +114,13 @@ anatomize pack . --output codebase.md
|
|
|
114
114
|
# Full bundle
|
|
115
115
|
anatomize pack . --format markdown --output codebase.md
|
|
116
116
|
|
|
117
|
+
# Minimal prefix (lower token overhead)
|
|
118
|
+
anatomize pack . --prefix minimal --output codebase.md
|
|
119
|
+
|
|
120
|
+
# Explain selection (why files were included/excluded)
|
|
121
|
+
# (writes `codebase.md.selection.json` by default)
|
|
122
|
+
anatomize pack . --explain-selection --output codebase.md
|
|
123
|
+
|
|
117
124
|
# Filter by globs
|
|
118
125
|
anatomize pack . --include "src/**" --ignore "**/__pycache__/**" --output src-only.md
|
|
119
126
|
|
|
@@ -147,13 +154,13 @@ anatomize pack . --token-count-tree --output codebase.md
|
|
|
147
154
|
# JSONL (stream-friendly)
|
|
148
155
|
anatomize pack . --format jsonl --output codebase.jsonl
|
|
149
156
|
|
|
150
|
-
# Hybrid mode (
|
|
151
|
-
# - defaults to
|
|
157
|
+
# Hybrid mode (summaries + selective fill; token-efficient)
|
|
158
|
+
# - defaults to markdown when --format and the output extension are not specified
|
|
152
159
|
# - Python files default to summary; non-Python defaults to metadata-only
|
|
153
|
-
anatomize pack . --mode hybrid --output hybrid.
|
|
160
|
+
anatomize pack . --mode hybrid --output hybrid.md
|
|
154
161
|
|
|
155
|
-
# Hybrid: include full content for a slice and fit within a hard token budget
|
|
156
|
-
anatomize pack . --mode hybrid --max-output 50_000t --fit-to-max-output \
|
|
162
|
+
# Hybrid: include full content for a slice and fit within a hard token budget (JSONL only)
|
|
163
|
+
anatomize pack . --mode hybrid --format jsonl --max-output 50_000t --fit-to-max-output \
|
|
157
164
|
--content "src/pkg/**" --output hybrid.slice.jsonl
|
|
158
165
|
```
|
|
159
166
|
|
|
@@ -219,8 +226,9 @@ symlinks: forbid # forbid|files|dirs|all
|
|
|
219
226
|
workers: 0 # 0 = auto
|
|
220
227
|
|
|
221
228
|
pack:
|
|
222
|
-
format: markdown # markdown|plain|json|xml|jsonl
|
|
223
|
-
mode: bundle # bundle|hybrid
|
|
229
|
+
format: markdown # markdown|plain|json|xml|jsonl (hybrid supports markdown|plain|jsonl)
|
|
230
|
+
mode: bundle # bundle|hybrid (hybrid is token-efficient summaries + selective fill)
|
|
231
|
+
prefix: standard # standard|minimal
|
|
224
232
|
output: anatomize-pack.md # if the extension is known, it must match `format`
|
|
225
233
|
include: []
|
|
226
234
|
ignore: []
|
|
@@ -281,8 +289,10 @@ When `anatomize generate` runs from `.anatomize.yaml`, it writes one skeleton di
|
|
|
281
289
|
Each pack artifact starts with a lightweight, deterministic overview (and, if enabled, a structure tree) before file blocks/records.
|
|
282
290
|
|
|
283
291
|
Token reporting:
|
|
284
|
-
- **Artifact tokens**: exact tokens of the written output file(s).
|
|
285
|
-
- **Content tokens**: tokens of file contents only (useful for budgeting
|
|
292
|
+
- **Artifact tokens**: exact tokens of the written output file(s) (returned by the Python API).
|
|
293
|
+
- **Content tokens**: tokens of file contents only (returned by the Python API; useful for budgeting).
|
|
294
|
+
|
|
295
|
+
Pack artifacts intentionally do **not** embed token counts (agents don’t need them; they waste tokens).
|
|
286
296
|
|
|
287
297
|
---
|
|
288
298
|
|
|
@@ -75,6 +75,13 @@ anatomize pack . --output codebase.md
|
|
|
75
75
|
# Full bundle
|
|
76
76
|
anatomize pack . --format markdown --output codebase.md
|
|
77
77
|
|
|
78
|
+
# Minimal prefix (lower token overhead)
|
|
79
|
+
anatomize pack . --prefix minimal --output codebase.md
|
|
80
|
+
|
|
81
|
+
# Explain selection (why files were included/excluded)
|
|
82
|
+
# (writes `codebase.md.selection.json` by default)
|
|
83
|
+
anatomize pack . --explain-selection --output codebase.md
|
|
84
|
+
|
|
78
85
|
# Filter by globs
|
|
79
86
|
anatomize pack . --include "src/**" --ignore "**/__pycache__/**" --output src-only.md
|
|
80
87
|
|
|
@@ -108,13 +115,13 @@ anatomize pack . --token-count-tree --output codebase.md
|
|
|
108
115
|
# JSONL (stream-friendly)
|
|
109
116
|
anatomize pack . --format jsonl --output codebase.jsonl
|
|
110
117
|
|
|
111
|
-
# Hybrid mode (
|
|
112
|
-
# - defaults to
|
|
118
|
+
# Hybrid mode (summaries + selective fill; token-efficient)
|
|
119
|
+
# - defaults to markdown when --format and the output extension are not specified
|
|
113
120
|
# - Python files default to summary; non-Python defaults to metadata-only
|
|
114
|
-
anatomize pack . --mode hybrid --output hybrid.
|
|
121
|
+
anatomize pack . --mode hybrid --output hybrid.md
|
|
115
122
|
|
|
116
|
-
# Hybrid: include full content for a slice and fit within a hard token budget
|
|
117
|
-
anatomize pack . --mode hybrid --max-output 50_000t --fit-to-max-output \
|
|
123
|
+
# Hybrid: include full content for a slice and fit within a hard token budget (JSONL only)
|
|
124
|
+
anatomize pack . --mode hybrid --format jsonl --max-output 50_000t --fit-to-max-output \
|
|
118
125
|
--content "src/pkg/**" --output hybrid.slice.jsonl
|
|
119
126
|
```
|
|
120
127
|
|
|
@@ -180,8 +187,9 @@ symlinks: forbid # forbid|files|dirs|all
|
|
|
180
187
|
workers: 0 # 0 = auto
|
|
181
188
|
|
|
182
189
|
pack:
|
|
183
|
-
format: markdown # markdown|plain|json|xml|jsonl
|
|
184
|
-
mode: bundle # bundle|hybrid
|
|
190
|
+
format: markdown # markdown|plain|json|xml|jsonl (hybrid supports markdown|plain|jsonl)
|
|
191
|
+
mode: bundle # bundle|hybrid (hybrid is token-efficient summaries + selective fill)
|
|
192
|
+
prefix: standard # standard|minimal
|
|
185
193
|
output: anatomize-pack.md # if the extension is known, it must match `format`
|
|
186
194
|
include: []
|
|
187
195
|
ignore: []
|
|
@@ -242,8 +250,10 @@ When `anatomize generate` runs from `.anatomize.yaml`, it writes one skeleton di
|
|
|
242
250
|
Each pack artifact starts with a lightweight, deterministic overview (and, if enabled, a structure tree) before file blocks/records.
|
|
243
251
|
|
|
244
252
|
Token reporting:
|
|
245
|
-
- **Artifact tokens**: exact tokens of the written output file(s).
|
|
246
|
-
- **Content tokens**: tokens of file contents only (useful for budgeting
|
|
253
|
+
- **Artifact tokens**: exact tokens of the written output file(s) (returned by the Python API).
|
|
254
|
+
- **Content tokens**: tokens of file contents only (returned by the Python API; useful for budgeting).
|
|
255
|
+
|
|
256
|
+
Pack artifacts intentionally do **not** embed token counts (agents don’t need them; they waste tokens).
|
|
247
257
|
|
|
248
258
|
---
|
|
249
259
|
|
|
@@ -163,7 +163,7 @@ Hybrid mode is intended for token efficiency and navigability:
|
|
|
163
163
|
- Python files default to `summary` (signatures/imports/line numbers).
|
|
164
164
|
- Non-Python files default to `meta` (metadata only) unless explicitly summarized or included as content.
|
|
165
165
|
- If `--format` is omitted, it is inferred from `--output` when the extension is known (`.md|.txt|.json|.xml|.jsonl`).
|
|
166
|
-
- Hybrid
|
|
166
|
+
- Hybrid supports **markdown**, **plain**, and **jsonl** output. If `--format` is not provided, it defaults to markdown when `--mode hybrid` is used and the output extension is unknown.
|
|
167
167
|
|
|
168
168
|
### Output formats
|
|
169
169
|
- `--format markdown` (best for human review)
|
|
@@ -171,6 +171,11 @@ Hybrid mode is intended for token efficiency and navigability:
|
|
|
171
171
|
- `--format json` / `--format xml` (best for machine processing)
|
|
172
172
|
- `--format jsonl` (stream-friendly, line-delimited JSON)
|
|
173
173
|
|
|
174
|
+
### Prefix style (token overhead vs guidance)
|
|
175
|
+
Control the amount of non-code guidance emitted at the top of markdown/plain pack artifacts:
|
|
176
|
+
- `--prefix standard` (default): includes a short “How to use” section.
|
|
177
|
+
- `--prefix minimal`: omits guidance and keeps only a compact, deterministic summary.
|
|
178
|
+
|
|
174
179
|
### Content encoding (robustness)
|
|
175
180
|
File contents can break Markdown structure (e.g. embedded ``` fences). Control how content is emitted with:
|
|
176
181
|
- `--content-encoding fence-safe` (default): picks a fence length that cannot occur in the content.
|
|
@@ -178,6 +183,11 @@ File contents can break Markdown structure (e.g. embedded ``` fences). Control h
|
|
|
178
183
|
|
|
179
184
|
Note: Markdown output intentionally disallows `--content-encoding verbatim`.
|
|
180
185
|
|
|
186
|
+
### Selection report (debugging file inclusion)
|
|
187
|
+
If you need to understand why paths were included or excluded, use:
|
|
188
|
+
- `--explain-selection` to write a deterministic selection report.
|
|
189
|
+
- `--explain-selection-output PATH` to choose where it is written.
|
|
190
|
+
|
|
181
191
|
### Hybrid representations (fill-in controls)
|
|
182
192
|
In hybrid mode, each file record has an explicit representation:
|
|
183
193
|
- `meta`: path/language/is_binary/size/content_tokens
|
|
@@ -197,7 +207,7 @@ Supported non-Python summaries:
|
|
|
197
207
|
- Markdown: headings outline
|
|
198
208
|
|
|
199
209
|
### Fit-to-budget (explicit)
|
|
200
|
-
Hybrid mode can deterministically fit within a hard cap:
|
|
210
|
+
Hybrid mode can deterministically fit within a hard cap (JSONL only):
|
|
201
211
|
- `--max-output 50_000t --fit-to-max-output`
|
|
202
212
|
|
|
203
213
|
If enabled, `pack` may downgrade representations (e.g. `summary` → `meta`) to satisfy `--max-output` and emits an auditable `selection_trace`.
|
|
@@ -205,8 +215,10 @@ If it cannot satisfy the cap, it fails.
|
|
|
205
215
|
|
|
206
216
|
### Token metrics
|
|
207
217
|
`pack` reports:
|
|
208
|
-
- **Artifact tokens**: exact tokens of the written output file(s).
|
|
209
|
-
- **Content tokens**: tokens for file contents only (useful for budgeting per-file).
|
|
218
|
+
- **Artifact tokens**: exact tokens of the written output file(s) (returned by the Python API).
|
|
219
|
+
- **Content tokens**: tokens for file contents only (returned by the Python API; useful for budgeting per-file).
|
|
220
|
+
|
|
221
|
+
Pack artifacts intentionally do **not** embed token counts (agents don’t need them; they waste tokens).
|
|
210
222
|
|
|
211
223
|
Encoding is controlled by:
|
|
212
224
|
- `--token-encoding` (default: `cl100k_base`)
|
|
@@ -9,7 +9,7 @@ Example
|
|
|
9
9
|
>>> from anatomize.formats import write_skeleton
|
|
10
10
|
>>> gen = SkeletonGenerator(sources=["./src"])
|
|
11
11
|
>>> skeleton = gen.generate(level="modules")
|
|
12
|
-
>>> write_skeleton(skeleton, ".
|
|
12
|
+
>>> write_skeleton(skeleton, ".anatomy", formats=[OutputFormat.YAML])
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
from anatomize.core.types import (
|
|
@@ -23,7 +23,7 @@ from anatomize.core.policy import SymlinkPolicy
|
|
|
23
23
|
from anatomize.core.types import ResolutionLevel
|
|
24
24
|
from anatomize.formats import OutputFormat, write_skeleton
|
|
25
25
|
from anatomize.generators.main import SkeletonGenerator
|
|
26
|
-
from anatomize.pack.formats import ContentEncoding, PackFormat
|
|
26
|
+
from anatomize.pack.formats import ContentEncoding, PackFormat, PrefixStyle, default_output_path
|
|
27
27
|
from anatomize.pack.limit import OutputLimit, parse_output_limit
|
|
28
28
|
from anatomize.pack.mode import PackMode
|
|
29
29
|
from anatomize.pack.slicing import SliceBackend
|
|
@@ -259,7 +259,7 @@ def generate(
|
|
|
259
259
|
Examples
|
|
260
260
|
--------
|
|
261
261
|
anatomize generate ./src
|
|
262
|
-
anatomize generate ./src --output .
|
|
262
|
+
anatomize generate ./src --output .anatomy --level signatures
|
|
263
263
|
anatomize generate ./src -f yaml -f json -f markdown
|
|
264
264
|
"""
|
|
265
265
|
try:
|
|
@@ -530,8 +530,8 @@ def validate(
|
|
|
530
530
|
|
|
531
531
|
Examples
|
|
532
532
|
--------
|
|
533
|
-
anatomize validate .
|
|
534
|
-
anatomize validate .
|
|
533
|
+
anatomize validate .anatomy --source ./src
|
|
534
|
+
anatomize validate .anatomy -s ./src -s ./tests --fix
|
|
535
535
|
"""
|
|
536
536
|
try:
|
|
537
537
|
from anatomize.validation import validate_skeleton_dir
|
|
@@ -644,6 +644,24 @@ def pack(
|
|
|
644
644
|
str | None,
|
|
645
645
|
typer.Option("--mode", help="Pack mode: bundle or hybrid (hybrid requires jsonl)."),
|
|
646
646
|
] = None,
|
|
647
|
+
prefix: Annotated[
|
|
648
|
+
str | None,
|
|
649
|
+
typer.Option("--prefix", help="Pack prefix style: standard or minimal."),
|
|
650
|
+
] = None,
|
|
651
|
+
explain_selection: Annotated[
|
|
652
|
+
bool,
|
|
653
|
+
typer.Option(
|
|
654
|
+
"--explain-selection",
|
|
655
|
+
help="Write a deterministic selection report (why files were included/excluded).",
|
|
656
|
+
),
|
|
657
|
+
] = False,
|
|
658
|
+
explain_selection_output: Annotated[
|
|
659
|
+
Path | None,
|
|
660
|
+
typer.Option(
|
|
661
|
+
"--explain-selection-output",
|
|
662
|
+
help="Selection report output path (default derived from --output).",
|
|
663
|
+
),
|
|
664
|
+
] = None,
|
|
647
665
|
include: Annotated[
|
|
648
666
|
list[str] | None,
|
|
649
667
|
typer.Option("--include", help="Include pattern(s). Repeatable allowlist; when absent, includes all."),
|
|
@@ -844,15 +862,25 @@ def pack(
|
|
|
844
862
|
)
|
|
845
863
|
else:
|
|
846
864
|
if resolved_mode is PackMode.HYBRID:
|
|
847
|
-
if inferred_from_output is not None
|
|
848
|
-
|
|
849
|
-
|
|
865
|
+
if inferred_from_output is not None:
|
|
866
|
+
if inferred_from_output not in (PackFormat.JSONL, PackFormat.MARKDOWN, PackFormat.PLAIN):
|
|
867
|
+
raise ValueError("--mode hybrid supports only markdown, plain, or jsonl output")
|
|
868
|
+
resolved_fmt = inferred_from_output
|
|
869
|
+
else:
|
|
870
|
+
resolved_fmt = PackFormat.MARKDOWN
|
|
850
871
|
else:
|
|
851
872
|
resolved_fmt = inferred_from_output if inferred_from_output is not None else pack_cfg.format
|
|
852
873
|
|
|
853
874
|
resolved_deps = bool(entry) if deps is None else deps
|
|
854
875
|
resolved_backend = pack_cfg.slice_backend if slice_backend is None else SliceBackend(slice_backend)
|
|
855
876
|
|
|
877
|
+
resolved_prefix = pack_cfg.prefix if prefix is None else PrefixStyle(prefix)
|
|
878
|
+
if resolved_mode is PackMode.HYBRID and resolved_fmt not in (
|
|
879
|
+
PackFormat.JSONL,
|
|
880
|
+
PackFormat.MARKDOWN,
|
|
881
|
+
PackFormat.PLAIN,
|
|
882
|
+
):
|
|
883
|
+
raise ValueError("--mode hybrid supports only markdown, plain, or jsonl output")
|
|
856
884
|
resolved_include = pack_cfg.include if include is None else include
|
|
857
885
|
resolved_ignore = pack_cfg.ignore if ignore is None else ignore
|
|
858
886
|
resolved_ignore_files = (
|
|
@@ -902,11 +930,23 @@ def pack(
|
|
|
902
930
|
[Path(p) for p in pack_cfg.python_roots] if python_root is None else python_root
|
|
903
931
|
)
|
|
904
932
|
|
|
933
|
+
selection_report_path: Path | None = None
|
|
934
|
+
if explain_selection:
|
|
935
|
+
base = resolved_output if resolved_output is not None else default_output_path(resolved_fmt)
|
|
936
|
+
base = base.resolve()
|
|
937
|
+
selection_report_path = (
|
|
938
|
+
explain_selection_output.resolve()
|
|
939
|
+
if explain_selection_output is not None
|
|
940
|
+
else base.with_name(base.name + ".selection.json")
|
|
941
|
+
)
|
|
942
|
+
|
|
905
943
|
res = run_pack(
|
|
906
944
|
root=root,
|
|
907
945
|
output=resolved_output,
|
|
908
946
|
fmt=resolved_fmt,
|
|
909
947
|
mode=resolved_mode,
|
|
948
|
+
prefix_style=resolved_prefix,
|
|
949
|
+
selection_report_output=selection_report_path,
|
|
910
950
|
include=resolved_include,
|
|
911
951
|
ignore=resolved_ignore,
|
|
912
952
|
ignore_files=resolved_ignore_files,
|
|
@@ -941,6 +981,8 @@ def pack(
|
|
|
941
981
|
|
|
942
982
|
for a in res.artifacts:
|
|
943
983
|
typer.echo(f"Wrote: {a.path}")
|
|
984
|
+
if selection_report_path is not None:
|
|
985
|
+
typer.echo(f"Selection report: {selection_report_path}")
|
|
944
986
|
total_artifact_tokens = sum(a.tokens for a in res.artifacts)
|
|
945
987
|
typer.echo(f"Artifact tokens: {total_artifact_tokens:,} ({resolved_token_encoding})")
|
|
946
988
|
typer.echo(f"Content tokens: {res.content_tokens:,} ({resolved_token_encoding})")
|
|
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field
|
|
|
14
14
|
from anatomize.core.policy import SymlinkPolicy
|
|
15
15
|
from anatomize.core.types import ResolutionLevel
|
|
16
16
|
from anatomize.formats import OutputFormat
|
|
17
|
-
from anatomize.pack.formats import ContentEncoding, PackFormat, infer_pack_format_from_output_path
|
|
17
|
+
from anatomize.pack.formats import ContentEncoding, PackFormat, PrefixStyle, infer_pack_format_from_output_path
|
|
18
18
|
from anatomize.pack.mode import PackMode
|
|
19
19
|
from anatomize.pack.slicing import SliceBackend
|
|
20
20
|
from anatomize.pack.summaries import SummaryConfig
|
|
@@ -26,6 +26,7 @@ class PackConfig(BaseModel):
|
|
|
26
26
|
format: PackFormat = PackFormat.MARKDOWN
|
|
27
27
|
mode: PackMode = PackMode.BUNDLE
|
|
28
28
|
output: str | None = None
|
|
29
|
+
prefix: PrefixStyle = PrefixStyle.STANDARD
|
|
29
30
|
include: list[str] = Field(default_factory=list)
|
|
30
31
|
ignore: list[str] = Field(default_factory=list)
|
|
31
32
|
ignore_files: list[str] = Field(default_factory=list)
|
|
@@ -54,6 +55,13 @@ class PackConfig(BaseModel):
|
|
|
54
55
|
model_config = {"extra": "forbid"}
|
|
55
56
|
|
|
56
57
|
def model_post_init(self, __context: Any) -> None:
|
|
58
|
+
if self.mode is PackMode.HYBRID and self.format not in (
|
|
59
|
+
PackFormat.MARKDOWN,
|
|
60
|
+
PackFormat.PLAIN,
|
|
61
|
+
PackFormat.JSONL,
|
|
62
|
+
):
|
|
63
|
+
raise ValueError("pack.mode=hybrid supports only markdown, plain, or jsonl format")
|
|
64
|
+
|
|
57
65
|
if self.output is None:
|
|
58
66
|
return
|
|
59
67
|
inferred = infer_pack_format_from_output_path(Path(self.output))
|
|
@@ -180,6 +188,7 @@ class AnatomizeConfig(BaseModel):
|
|
|
180
188
|
"format": self.pack.format.value,
|
|
181
189
|
"mode": self.pack.mode.value,
|
|
182
190
|
"output": self.pack.output,
|
|
191
|
+
"prefix": self.pack.prefix.value,
|
|
183
192
|
"include": self.pack.include,
|
|
184
193
|
"ignore": self.pack.ignore,
|
|
185
194
|
"ignore_files": self.pack.ignore_files,
|
|
@@ -16,10 +16,12 @@ Supported:
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
+
from collections.abc import Sequence
|
|
19
20
|
from dataclasses import dataclass
|
|
20
21
|
from fnmatch import fnmatchcase
|
|
21
22
|
from functools import cache
|
|
22
23
|
from pathlib import PurePosixPath
|
|
24
|
+
from typing import TypeAlias
|
|
23
25
|
|
|
24
26
|
|
|
25
27
|
@dataclass(frozen=True)
|
|
@@ -35,12 +37,23 @@ class ExcludeRule:
|
|
|
35
37
|
anchored: bool
|
|
36
38
|
directory_only: bool
|
|
37
39
|
has_slash: bool
|
|
40
|
+
source: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
IgnorePattern: TypeAlias = str | tuple[str, str]
|
|
38
44
|
|
|
39
45
|
|
|
40
46
|
class Excluder:
|
|
41
|
-
def __init__(self, patterns:
|
|
47
|
+
def __init__(self, patterns: Sequence[IgnorePattern]) -> None:
|
|
42
48
|
self._rules: list[ExcludeRule] = []
|
|
43
|
-
for
|
|
49
|
+
for item in patterns:
|
|
50
|
+
source = "unspecified"
|
|
51
|
+
raw: str
|
|
52
|
+
if isinstance(item, tuple):
|
|
53
|
+
raw, source = item
|
|
54
|
+
else:
|
|
55
|
+
raw = item
|
|
56
|
+
|
|
44
57
|
parsed = parse_ignore_line(raw, allow_negation=True)
|
|
45
58
|
if parsed is None:
|
|
46
59
|
continue
|
|
@@ -67,20 +80,30 @@ class Excluder:
|
|
|
67
80
|
anchored=anchored,
|
|
68
81
|
directory_only=directory_only,
|
|
69
82
|
has_slash=has_slash,
|
|
83
|
+
source=source,
|
|
70
84
|
)
|
|
71
85
|
)
|
|
72
86
|
|
|
73
87
|
def is_excluded(self, rel_posix: str, *, is_dir: bool) -> bool:
|
|
74
88
|
"""Return True if the given relative path should be excluded."""
|
|
89
|
+
return self.explain(rel_posix, is_dir=is_dir)[0]
|
|
90
|
+
|
|
91
|
+
def rules(self) -> list[ExcludeRule]:
|
|
92
|
+
return list(self._rules)
|
|
93
|
+
|
|
94
|
+
def explain(self, rel_posix: str, *, is_dir: bool) -> tuple[bool, ExcludeRule | None]:
|
|
95
|
+
"""Return (excluded, last_matching_rule_or_None)."""
|
|
75
96
|
rel_posix = rel_posix.strip("/")
|
|
76
97
|
path = PurePosixPath(rel_posix) if rel_posix else PurePosixPath(".")
|
|
77
98
|
|
|
78
99
|
excluded = False
|
|
100
|
+
matched: ExcludeRule | None = None
|
|
79
101
|
for rule in self._rules:
|
|
80
102
|
if not self._matches(path, rule, is_dir=is_dir):
|
|
81
103
|
continue
|
|
82
104
|
excluded = not rule.negated
|
|
83
|
-
|
|
105
|
+
matched = rule
|
|
106
|
+
return excluded, matched
|
|
84
107
|
|
|
85
108
|
def filter_dirnames(self, parent_rel_posix: str, dirnames: list[str]) -> None:
|
|
86
109
|
"""Prune excluded directories in-place for os.walk dirnames."""
|
|
@@ -23,7 +23,7 @@ class MarkdownFormatter:
|
|
|
23
23
|
Example
|
|
24
24
|
-------
|
|
25
25
|
>>> formatter = MarkdownFormatter()
|
|
26
|
-
>>> formatter.write(skeleton, Path(".
|
|
26
|
+
>>> formatter.write(skeleton, Path(".anatomy"))
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
29
|
def write(self, skeleton: Skeleton, output_dir: Path) -> None:
|
|
@@ -69,9 +69,14 @@ class SkeletonGenerator:
|
|
|
69
69
|
"*.pyc",
|
|
70
70
|
".git",
|
|
71
71
|
".venv",
|
|
72
|
-
".
|
|
72
|
+
".anatomy",
|
|
73
|
+
".runtime",
|
|
73
74
|
".mypy_cache",
|
|
74
75
|
".pytest_cache",
|
|
76
|
+
".ruff_cache",
|
|
77
|
+
"dist",
|
|
78
|
+
"build",
|
|
79
|
+
"*.egg-info",
|
|
75
80
|
]
|
|
76
81
|
self._symlinks = symlinks
|
|
77
82
|
self._workers = workers
|
|
@@ -90,10 +90,12 @@ def dependency_closure(entry_files: list[Path], *, index: PythonModuleIndex) ->
|
|
|
90
90
|
raise ValueError("At least one --entry is required for dependency closure")
|
|
91
91
|
|
|
92
92
|
seen: set[Path] = set()
|
|
93
|
+
parent: dict[Path, Path | None] = {}
|
|
93
94
|
queue: list[PythonModule] = []
|
|
94
95
|
for f in entry_files:
|
|
95
96
|
mod = index.module_for_path(f)
|
|
96
97
|
queue.append(mod)
|
|
98
|
+
parent.setdefault(mod.path, None)
|
|
97
99
|
|
|
98
100
|
while queue:
|
|
99
101
|
current = queue.pop()
|
|
@@ -104,16 +106,30 @@ def dependency_closure(entry_files: list[Path], *, index: PythonModuleIndex) ->
|
|
|
104
106
|
for init in index.package_inits_for(current.module):
|
|
105
107
|
if init.path not in seen:
|
|
106
108
|
queue.append(init)
|
|
109
|
+
parent.setdefault(init.path, current.path)
|
|
107
110
|
|
|
108
111
|
for imported in _extract_imported_modules(current, index=index):
|
|
109
112
|
target = index.resolve_module(imported)
|
|
110
113
|
if target is None:
|
|
111
114
|
top = imported.split(".", 1)[0]
|
|
112
115
|
if index.is_local_toplevel(top):
|
|
113
|
-
|
|
116
|
+
chain: list[Path] = [current.path]
|
|
117
|
+
p = current.path
|
|
118
|
+
while True:
|
|
119
|
+
prev = parent.get(p)
|
|
120
|
+
if prev is None:
|
|
121
|
+
break
|
|
122
|
+
p = prev
|
|
123
|
+
chain.append(p)
|
|
124
|
+
chain = list(reversed(chain))
|
|
125
|
+
chain_str = "\n".join(f" - {c}" for c in chain)
|
|
126
|
+
raise ValueError(
|
|
127
|
+
f"Unresolved local import '{imported}' from {current.path}\nImport chain:\n{chain_str}"
|
|
128
|
+
)
|
|
114
129
|
continue
|
|
115
130
|
if target.path not in seen:
|
|
116
131
|
queue.append(target)
|
|
132
|
+
parent.setdefault(target.path, current.path)
|
|
117
133
|
|
|
118
134
|
return sorted(seen)
|
|
119
135
|
|
|
@@ -20,6 +20,16 @@ class DiscoveredPath:
|
|
|
20
20
|
is_binary: bool
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class DiscoveryTraceItem:
|
|
25
|
+
path: str
|
|
26
|
+
is_dir: bool
|
|
27
|
+
decision: str # "included" or "excluded"
|
|
28
|
+
reason: str | None # "ignore" | "include" | None
|
|
29
|
+
matched_pattern: str | None
|
|
30
|
+
matched_source: str | None
|
|
31
|
+
|
|
32
|
+
|
|
23
33
|
def discover_paths(
|
|
24
34
|
root: Path,
|
|
25
35
|
*,
|
|
@@ -27,6 +37,7 @@ def discover_paths(
|
|
|
27
37
|
include_patterns: list[str] | None,
|
|
28
38
|
symlinks: SymlinkPolicy,
|
|
29
39
|
max_file_bytes: int,
|
|
40
|
+
trace: list[DiscoveryTraceItem] | None = None,
|
|
30
41
|
) -> list[DiscoveredPath]:
|
|
31
42
|
root = root.resolve()
|
|
32
43
|
if not root.exists() or not root.is_dir():
|
|
@@ -55,7 +66,22 @@ def discover_paths(
|
|
|
55
66
|
except OSError as e:
|
|
56
67
|
raise ValueError(f"Failed to stat path: {entry}") from e
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
excluded, matched = excluder.explain(rel_posix, is_dir=is_dir)
|
|
70
|
+
if excluded:
|
|
71
|
+
if trace is not None:
|
|
72
|
+
matched_pattern = None
|
|
73
|
+
if matched is not None:
|
|
74
|
+
matched_pattern = matched.pattern + ("/" if matched.directory_only else "")
|
|
75
|
+
trace.append(
|
|
76
|
+
DiscoveryTraceItem(
|
|
77
|
+
path=rel_posix,
|
|
78
|
+
is_dir=is_dir,
|
|
79
|
+
decision="excluded",
|
|
80
|
+
reason="ignore",
|
|
81
|
+
matched_pattern=matched_pattern,
|
|
82
|
+
matched_source=matched.source if matched is not None else None,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
59
85
|
continue
|
|
60
86
|
|
|
61
87
|
if include_patterns and not include_matcher.matches_any(rel_posix, is_dir=is_dir):
|
|
@@ -64,6 +90,17 @@ def discover_paths(
|
|
|
64
90
|
# our simple include matcher cannot prove that cheaply, so we traverse all
|
|
65
91
|
# non-excluded directories and filter files at the leaf.
|
|
66
92
|
if not is_dir:
|
|
93
|
+
if trace is not None:
|
|
94
|
+
trace.append(
|
|
95
|
+
DiscoveryTraceItem(
|
|
96
|
+
path=rel_posix,
|
|
97
|
+
is_dir=False,
|
|
98
|
+
decision="excluded",
|
|
99
|
+
reason="include",
|
|
100
|
+
matched_pattern=None,
|
|
101
|
+
matched_source=None,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
67
104
|
continue
|
|
68
105
|
|
|
69
106
|
if is_dir:
|
|
@@ -95,6 +132,17 @@ def discover_paths(
|
|
|
95
132
|
is_binary=is_binary,
|
|
96
133
|
)
|
|
97
134
|
)
|
|
135
|
+
if trace is not None:
|
|
136
|
+
trace.append(
|
|
137
|
+
DiscoveryTraceItem(
|
|
138
|
+
path=rel_posix,
|
|
139
|
+
is_dir=False,
|
|
140
|
+
decision="included",
|
|
141
|
+
reason=None,
|
|
142
|
+
matched_pattern=None,
|
|
143
|
+
matched_source=None,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
98
146
|
|
|
99
147
|
results.append(
|
|
100
148
|
DiscoveredPath(
|
|
@@ -115,7 +163,8 @@ def discover_paths(
|
|
|
115
163
|
|
|
116
164
|
def _is_binary_file(path: Path, *, sniff_bytes: int = 8192) -> bool:
|
|
117
165
|
try:
|
|
118
|
-
|
|
166
|
+
with path.open("rb") as f:
|
|
167
|
+
data = f.read(sniff_bytes)
|
|
119
168
|
except OSError:
|
|
120
169
|
return True
|
|
121
170
|
if b"\x00" in data:
|