squackit 0.8.0__tar.gz → 0.8.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.
- {squackit-0.8.0 → squackit-0.8.2}/CHANGELOG.md +89 -0
- {squackit-0.8.0 → squackit-0.8.2}/PKG-INFO +3 -3
- {squackit-0.8.0 → squackit-0.8.2}/pyproject.toml +9 -3
- {squackit-0.8.0 → squackit-0.8.2}/squackit/defaults.py +82 -14
- {squackit-0.8.0 → squackit-0.8.2}/squackit/runtime.py +16 -2
- {squackit-0.8.0 → squackit-0.8.2}/squackit/server.py +22 -4
- {squackit-0.8.0 → squackit-0.8.2}/squackit/tools.py +112 -9
- {squackit-0.8.0 → squackit-0.8.2}/squackit/workflows.py +8 -6
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_defaults.py +27 -8
- squackit-0.8.2/tests/test_sandbox_anchoring.py +223 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_truncation.py +5 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_workflows.py +38 -13
- {squackit-0.8.0 → squackit-0.8.2}/.github/workflows/release.yml +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/.gitignore +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/.mcp.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/.readthedocs.yaml +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/.readthedocs.yml +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/CLAUDE.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/LICENSE +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/README.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/agent-system-prompt.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/architecture.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/configuration.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/index.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/prompts.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/quickstart.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/resources.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/docs/tools.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/mkdocs.yml +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/EXPERIMENT_LOG.md +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/benchmark_models.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/cascade_prototype.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/combo_eval.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/generate_examples.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/grid_generate_examples.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/qwen3_8b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/scoring_input.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_task_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/hard_tasks.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/harvest_grid.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_eval.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary_distilled_xml.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary_fulldocs.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/codegemma_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/codegemma_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/gemma_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/glm4_9b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/granite-code_3b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/granite3.3_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/llama3.2_1b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/llama3.2_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/phi3_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/phi4-mini_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_3b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2.5_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen2_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_4b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3.5_9b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3_0.6b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/qwen3_8b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/smollm2_latest.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_card_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/model_cards.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/qwen3.5_2b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/qwen3_8b.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/multiline_tasks.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/qm_retrieval_eval.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/quartermaster_eval.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/quartermaster_results/retrieval_summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/quartermaster_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/selector_eval.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/scripts/selector_results/summary.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/__init__.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/__main__.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/cli.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/data/examples.json +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/db.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/formatting.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/lackpy_integration.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/prompts.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/session.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/squackit/tool_config.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/conftest.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/fixtures/has_call_filter.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_cli_tools.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_config_consumption.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_formatting_json.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_fts_root.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_prompts.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_resources.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_runtime_config.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_session.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_smoke.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_tool_config.py +0 -0
- {squackit-0.8.0 → squackit-0.8.2}/tests/test_tools.py +0 -0
|
@@ -1,5 +1,94 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.2
|
|
4
|
+
|
|
5
|
+
### Fixed — AST tools could not read a source outside the working directory
|
|
6
|
+
|
|
7
|
+
`find` / `view` / `find_names` / `complexity` build their own connection with
|
|
8
|
+
no root, so fledgling's sandbox confined them to the process working
|
|
9
|
+
directory. An absolute source anywhere else — `find(source="/srv/code/x/**/*.py")`
|
|
10
|
+
— raised a permission error. The connection is now rooted at the source's own
|
|
11
|
+
directory, which is the scope the caller named. Relative sources are untouched:
|
|
12
|
+
they already resolve against the working directory, and re-rooting them would
|
|
13
|
+
double-prefix the path.
|
|
14
|
+
|
|
15
|
+
Each rooted plucker gets its own AST cache file, keyed by root. DuckDB's
|
|
16
|
+
filesystem allow-list belongs to the database *instance*, not the connection,
|
|
17
|
+
and every connection to one file shares an instance — so a single shared cache
|
|
18
|
+
meant the first lockdown won for the life of the process and later roots were
|
|
19
|
+
silently ignored. The cache lives in the temp directory rather than inside the
|
|
20
|
+
root, since a served corpus is mounted read-only.
|
|
21
|
+
|
|
22
|
+
### Fixed — de-vendoring aborted the transaction and blamed the next query
|
|
23
|
+
|
|
24
|
+
When a glob's tree had no `.git`/`.gitmodules` ancestor, the walk up degraded
|
|
25
|
+
to `/` and `_submodule_prefixes('')` read the filesystem root — refused under
|
|
26
|
+
the sandbox. The failure was swallowed by design (de-vendoring is an
|
|
27
|
+
optimization, not a correctness gate), but the aborted DuckDB transaction was
|
|
28
|
+
not rolled back, so the *next* statement failed with "Current transaction is
|
|
29
|
+
aborted" — pointing at the AST cache, which had done nothing wrong. Submodule
|
|
30
|
+
exclusion is now skipped when there is no repository to read, and the fallback
|
|
31
|
+
rolls back so the connection stays usable.
|
|
32
|
+
|
|
33
|
+
### Fixed — `read_source` output was double-spaced
|
|
34
|
+
|
|
35
|
+
Rows carry each line's own trailing newline, and the text renderer joined them
|
|
36
|
+
with another, so every second line was blank. That also broke head/tail
|
|
37
|
+
truncation, whose omission message is inserted by row index.
|
|
38
|
+
|
|
39
|
+
## 0.8.1
|
|
40
|
+
|
|
41
|
+
### Fixed — every code query returned "not found" under fledgling >= 0.13
|
|
42
|
+
|
|
43
|
+
fledgling 0.13 added `sandbox: bool = True` to `connect()`, restricting the
|
|
44
|
+
connection's filesystem allow-list to the project root. squackit's inferred
|
|
45
|
+
default patterns were relative (`**/*.py`), and DuckDB resolves a relative
|
|
46
|
+
glob against the process working directory — not against the root. A server's
|
|
47
|
+
cwd is never the corpus it serves, so the glob landed outside the allow-list
|
|
48
|
+
and the query raised:
|
|
49
|
+
|
|
50
|
+
Permission Error: Cannot access file "**/*.py"
|
|
51
|
+
- file system operations are disabled by configuration
|
|
52
|
+
|
|
53
|
+
`investigate` catches query failures and reports them as a miss, so the
|
|
54
|
+
visible symptom was:
|
|
55
|
+
|
|
56
|
+
No definition found for 'parse_config'. Try a broader pattern or check spelling.
|
|
57
|
+
|
|
58
|
+
for every symbol, in a project where those symbols existed — while the server
|
|
59
|
+
reported all of its tools healthy. A missing-result message is
|
|
60
|
+
indistinguishable from an empty corpus, which is what made this quiet.
|
|
61
|
+
|
|
62
|
+
Patterns are now anchored at the project root wherever they reach DuckDB:
|
|
63
|
+
|
|
64
|
+
- `ProjectDefaults` carries `root` and exposes `code_glob` / `doc_glob`;
|
|
65
|
+
`TOOL_DEFAULTS` names those rather than the raw pattern fields.
|
|
66
|
+
- `scoped_code_pattern` / `scoped_doc_pattern` anchor their results.
|
|
67
|
+
- Caller-supplied *relative* patterns are anchored too, so `docs/**/*.md`
|
|
68
|
+
means "docs in this project" rather than a permission error.
|
|
69
|
+
- Absolute patterns are passed through untouched.
|
|
70
|
+
|
|
71
|
+
The raw `code_pattern` / `doc_pattern` fields stay relative — `prompts.py`
|
|
72
|
+
shows them to users, where an absolute corpus path is noise.
|
|
73
|
+
|
|
74
|
+
### Changed — unscoped `investigate` falls back to the served root, not cwd
|
|
75
|
+
|
|
76
|
+
`resolve_scope_path` gained a `default` argument, giving the chain
|
|
77
|
+
`explicit path -> runtime.active_root -> served project root -> cwd`.
|
|
78
|
+
Previously an unscoped call scoped to the process working directory, which
|
|
79
|
+
was a workable stand-in only when the tool ran from inside the repo. For a
|
|
80
|
+
served corpus it searched an unrelated directory, and under the sandbox it
|
|
81
|
+
failed outright. Cross-project isolation is unchanged: results still come
|
|
82
|
+
from one project, now named explicitly instead of inferred from cwd.
|
|
83
|
+
|
|
84
|
+
### Changed — dependency bounds relaxed to `<1.0`
|
|
85
|
+
|
|
86
|
+
The `fledgling-mcp>=0.12,<0.13` bound excluded 0.13.1, which is where
|
|
87
|
+
fledgling relaxed its own exact `duckdb==1.5.2` pin to a range. Against a
|
|
88
|
+
`duckdb==1.5.5` requirement pip did not report a conflict — it backtracked
|
|
89
|
+
to squackit 0.7.0 and installed that, reporting success while shipping code
|
|
90
|
+
two releases old.
|
|
91
|
+
|
|
3
92
|
## 0.8.0
|
|
4
93
|
|
|
5
94
|
### Added — source text from `find`, and a cache instead of re-parsing
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: squackit
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/teaguesterling/squackit
|
|
6
6
|
Project-URL: Documentation, https://squackit.readthedocs.io
|
|
@@ -16,10 +16,10 @@ Classifier: License :: OSI Approved :: Apache Software License
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
18
|
Requires-Python: >=3.10
|
|
19
|
-
Requires-Dist: ast-pluckit<0
|
|
19
|
+
Requires-Dist: ast-pluckit<1.0,>=0.15
|
|
20
20
|
Requires-Dist: click>=8.0
|
|
21
21
|
Requires-Dist: fastmcp>=3.0
|
|
22
|
-
Requires-Dist: fledgling-mcp<0
|
|
22
|
+
Requires-Dist: fledgling-mcp<1.0,>=0.12
|
|
23
23
|
Provides-Extra: docs
|
|
24
24
|
Requires-Dist: mkdocs-exclude>=1.0; extra == 'docs'
|
|
25
25
|
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "squackit"
|
|
3
|
-
version = "0.8.
|
|
3
|
+
version = "0.8.2"
|
|
4
4
|
description = "Semi-QUalified Agent Companion Kit — the stateful intelligence + MCP server layer for fledgling-equipped agents."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "Apache-2.0"
|
|
@@ -17,8 +17,14 @@ classifiers = [
|
|
|
17
17
|
"License :: OSI Approved :: Apache Software License",
|
|
18
18
|
]
|
|
19
19
|
dependencies = [
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# Upper bounds relaxed to <1.0. The tight `<0.13` on fledgling-mcp excluded
|
|
21
|
+
# 0.13.1, which is where fledgling relaxed its own `duckdb==1.5.2` exact pin
|
|
22
|
+
# to a range. Anything needing a specific DuckDB (e.g. matching a bundled
|
|
23
|
+
# extension set) then conflicted, and pip resolved it by SILENTLY
|
|
24
|
+
# BACKTRACKING to squackit 0.7.0 rather than failing — reporting success
|
|
25
|
+
# while installing code two releases old.
|
|
26
|
+
"ast-pluckit>=0.15,<1.0",
|
|
27
|
+
"fledgling-mcp>=0.12,<1.0",
|
|
22
28
|
"click>=8.0",
|
|
23
29
|
"fastmcp>=3.0",
|
|
24
30
|
]
|
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
import logging
|
|
11
11
|
import subprocess
|
|
12
12
|
import tomllib
|
|
13
|
-
from dataclasses import dataclass, field
|
|
13
|
+
from dataclasses import dataclass, field, fields
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import TYPE_CHECKING
|
|
16
16
|
|
|
@@ -22,7 +22,21 @@ if TYPE_CHECKING:
|
|
|
22
22
|
|
|
23
23
|
@dataclass
|
|
24
24
|
class ProjectDefaults:
|
|
25
|
-
"""Inferred at server startup, cached for the session.
|
|
25
|
+
"""Inferred at server startup, cached for the session.
|
|
26
|
+
|
|
27
|
+
Patterns are stored RELATIVE (``**/*.py``) because they are shown to
|
|
28
|
+
users — prompts.py interpolates ``code_pattern`` into tool guidance,
|
|
29
|
+
where an absolute corpus path is noise. Anything handed to DuckDB
|
|
30
|
+
must instead go through the anchored accessors below.
|
|
31
|
+
|
|
32
|
+
WHY ANCHORING IS MANDATORY: a fledgling connection is sandboxed by
|
|
33
|
+
default (fledgling >= 0.13), which limits the filesystem allow-list to
|
|
34
|
+
``root``. DuckDB resolves a relative glob against the process CWD, not
|
|
35
|
+
against ``root``. A server's CWD is never the corpus it serves, so an
|
|
36
|
+
un-anchored default pattern lands outside the allow-list and the query
|
|
37
|
+
fails with a permission error — which callers upstack turn into "no
|
|
38
|
+
results". See tests/test_sandbox_anchoring.py.
|
|
39
|
+
"""
|
|
26
40
|
|
|
27
41
|
code_pattern: str = "**/*"
|
|
28
42
|
doc_pattern: str = "**/*.md"
|
|
@@ -30,11 +44,40 @@ class ProjectDefaults:
|
|
|
30
44
|
from_rev: str = "HEAD~1"
|
|
31
45
|
to_rev: str = "HEAD"
|
|
32
46
|
languages: list[str] = field(default_factory=list)
|
|
47
|
+
root: str | None = None
|
|
48
|
+
|
|
49
|
+
def anchor(self, pattern: str | Path) -> str:
|
|
50
|
+
"""Resolve *pattern* against the project root.
|
|
51
|
+
|
|
52
|
+
An absolute pattern is returned unchanged: a user who configured
|
|
53
|
+
one meant it, and silently re-rooting it would be surprising.
|
|
54
|
+
With no root known, the pattern is returned as-is, preserving the
|
|
55
|
+
pre-sandbox behaviour for callers that construct defaults directly.
|
|
56
|
+
"""
|
|
57
|
+
p = Path(pattern)
|
|
58
|
+
if self.root is None or p.is_absolute():
|
|
59
|
+
return str(pattern)
|
|
60
|
+
return str(Path(self.root) / p)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def code_glob(self) -> str:
|
|
64
|
+
"""``code_pattern`` anchored at root — use this for queries."""
|
|
65
|
+
return self.anchor(self.code_pattern)
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def doc_glob(self) -> str:
|
|
69
|
+
"""``doc_pattern`` anchored at root — use this for queries."""
|
|
70
|
+
return self.anchor(self.doc_pattern)
|
|
33
71
|
|
|
34
72
|
def scoped_code_pattern(self, path: str | Path) -> str:
|
|
35
|
-
"""Scope the code pattern to a subdirectory path."""
|
|
73
|
+
"""Scope the code pattern to a subdirectory path, anchored at root."""
|
|
36
74
|
filename_glob = self.code_pattern.rsplit("/", 1)[-1]
|
|
37
|
-
return
|
|
75
|
+
return self.anchor(Path(path) / "**" / filename_glob)
|
|
76
|
+
|
|
77
|
+
def scoped_doc_pattern(self, path: str | Path) -> str:
|
|
78
|
+
"""Scope the doc pattern to a subdirectory path, anchored at root."""
|
|
79
|
+
filename_glob = self.doc_pattern.rsplit("/", 1)[-1]
|
|
80
|
+
return self.anchor(Path(path) / "**" / filename_glob)
|
|
38
81
|
|
|
39
82
|
|
|
40
83
|
def apply_defaults(
|
|
@@ -50,9 +93,18 @@ def apply_defaults(
|
|
|
50
93
|
if not mapping:
|
|
51
94
|
return dict(kwargs)
|
|
52
95
|
result = dict(kwargs)
|
|
53
|
-
for param,
|
|
96
|
+
for param, attr_name in mapping.items():
|
|
54
97
|
if result.get(param) is None:
|
|
55
|
-
result[param] = getattr(defaults,
|
|
98
|
+
result[param] = getattr(defaults, attr_name)
|
|
99
|
+
elif attr_name in _PATTERN_ATTRS:
|
|
100
|
+
# An EXPLICIT pattern gets anchored too. A caller passing
|
|
101
|
+
# `docs/**/*.md` means "docs under this project", but DuckDB
|
|
102
|
+
# resolves it against the process cwd — outside the sandbox's
|
|
103
|
+
# allow-list — so it raised a permission error instead of
|
|
104
|
+
# returning rows. Relative means relative-to-root everywhere,
|
|
105
|
+
# for defaults and explicit arguments alike. Absolute patterns
|
|
106
|
+
# are passed through untouched by anchor().
|
|
107
|
+
result[param] = defaults.anchor(result[param])
|
|
56
108
|
return result
|
|
57
109
|
|
|
58
110
|
|
|
@@ -70,14 +122,23 @@ def load_config(root: str | Path) -> dict[str, str]:
|
|
|
70
122
|
return dict(data.get("defaults", {}))
|
|
71
123
|
|
|
72
124
|
|
|
73
|
-
#
|
|
125
|
+
# Attributes in TOOL_DEFAULTS that hold filesystem globs. Values for these
|
|
126
|
+
# params are root-anchored whether they came from the defaults or the caller.
|
|
127
|
+
_PATTERN_ATTRS = {"code_glob", "doc_glob"}
|
|
128
|
+
|
|
129
|
+
# Tool name → {param_name: defaults_attribute_name}
|
|
130
|
+
#
|
|
131
|
+
# These name the ANCHORED accessors (code_glob / doc_glob), not the raw
|
|
132
|
+
# pattern fields. A value substituted here goes straight to DuckDB, so it
|
|
133
|
+
# has to be resolvable from the sandbox's allow-list rather than from the
|
|
134
|
+
# process CWD. The raw code_pattern/doc_pattern fields remain for display.
|
|
74
135
|
TOOL_DEFAULTS: dict[str, dict[str, str]] = {
|
|
75
|
-
"find_definitions": {"file_pattern": "
|
|
76
|
-
"find_in_ast": {"file_pattern": "
|
|
77
|
-
"code_structure": {"file_pattern": "
|
|
78
|
-
"complexity_hotspots": {"file_pattern": "
|
|
79
|
-
"changed_function_summary": {"file_pattern": "
|
|
80
|
-
"doc_outline": {"file_pattern": "
|
|
136
|
+
"find_definitions": {"file_pattern": "code_glob"},
|
|
137
|
+
"find_in_ast": {"file_pattern": "code_glob"},
|
|
138
|
+
"code_structure": {"file_pattern": "code_glob"},
|
|
139
|
+
"complexity_hotspots": {"file_pattern": "code_glob"},
|
|
140
|
+
"changed_function_summary": {"file_pattern": "code_glob"},
|
|
141
|
+
"doc_outline": {"file_pattern": "doc_glob"},
|
|
81
142
|
"file_changes": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
82
143
|
"file_diff": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
83
144
|
"structural_diff": {"from_rev": "from_rev", "to_rev": "to_rev"},
|
|
@@ -216,10 +277,17 @@ def infer_defaults(
|
|
|
216
277
|
doc_pattern=doc_pattern,
|
|
217
278
|
main_branch=main_branch,
|
|
218
279
|
languages=languages,
|
|
280
|
+
root=str(root) if root is not None else None,
|
|
219
281
|
)
|
|
220
282
|
|
|
283
|
+
# Only real dataclass fields are settable. code_glob/doc_glob are
|
|
284
|
+
# read-only properties, so a config file naming one would otherwise
|
|
285
|
+
# raise AttributeError at startup instead of being ignored.
|
|
286
|
+
settable = {f.name for f in fields(defaults)}
|
|
221
287
|
for key, value in overrides.items():
|
|
222
|
-
if
|
|
288
|
+
if key in settable:
|
|
223
289
|
setattr(defaults, key, value)
|
|
290
|
+
else:
|
|
291
|
+
log.warning("ignoring unknown defaults override %r", key)
|
|
224
292
|
|
|
225
293
|
return defaults
|
|
@@ -136,19 +136,33 @@ def reset_runtime() -> SquackitRuntimeConfig:
|
|
|
136
136
|
return _runtime
|
|
137
137
|
|
|
138
138
|
|
|
139
|
-
def resolve_scope_path(
|
|
139
|
+
def resolve_scope_path(
|
|
140
|
+
explicit_path: str | None = None,
|
|
141
|
+
default: str | None = None,
|
|
142
|
+
) -> str:
|
|
140
143
|
"""Resolve a scope path with the runtime config fallback chain.
|
|
141
144
|
|
|
142
|
-
Precedence: explicit_path -> runtime.active_root ->
|
|
145
|
+
Precedence: explicit_path -> runtime.active_root -> *default* -> cwd.
|
|
146
|
+
|
|
143
147
|
Used by tools that scope queries to a directory (investigate, etc.)
|
|
144
148
|
so a single `config(set={"active_root": X})` call configures the whole
|
|
145
149
|
session.
|
|
150
|
+
|
|
151
|
+
*default* is the served project root. It sits ahead of cwd because a
|
|
152
|
+
server's cwd is not the corpus it serves: falling through to cwd scoped
|
|
153
|
+
every unscoped query to an unrelated directory, and — once fledgling
|
|
154
|
+
began sandboxing connections to the project root — put the resulting
|
|
155
|
+
glob outside the filesystem allow-list, so the query failed outright
|
|
156
|
+
rather than merely searching the wrong tree. cwd stays as the last
|
|
157
|
+
resort for callers with no known root.
|
|
146
158
|
"""
|
|
147
159
|
if explicit_path is not None:
|
|
148
160
|
return explicit_path
|
|
149
161
|
runtime_root = _runtime.active_root
|
|
150
162
|
if runtime_root is not None:
|
|
151
163
|
return runtime_root
|
|
164
|
+
if default is not None:
|
|
165
|
+
return default
|
|
152
166
|
return os.getcwd()
|
|
153
167
|
|
|
154
168
|
|
|
@@ -296,7 +296,12 @@ def _register_executor_tool(mcp, presentation: ToolPresentation):
|
|
|
296
296
|
if is_text:
|
|
297
297
|
lines = []
|
|
298
298
|
for row in rows:
|
|
299
|
-
|
|
299
|
+
# Strip each value's own line terminator: this branch joins
|
|
300
|
+
# rows with "\n" itself, so a source line that already ends
|
|
301
|
+
# in one renders double-spaced, every second line blank.
|
|
302
|
+
# Only \r and \n come off — leading indentation is content,
|
|
303
|
+
# and so is any trailing space a line genuinely carries.
|
|
304
|
+
parts = [str(v).rstrip("\r\n") for v in row if v is not None]
|
|
300
305
|
lines.append(" ".join(parts))
|
|
301
306
|
if omission:
|
|
302
307
|
lines.append(omission)
|
|
@@ -594,16 +599,29 @@ def _register_tool(
|
|
|
594
599
|
|
|
595
600
|
# Format output
|
|
596
601
|
if is_text:
|
|
602
|
+
def _cell(value) -> str:
|
|
603
|
+
"""Render one value as a line fragment.
|
|
604
|
+
|
|
605
|
+
Drops the value's own line terminator: this branch joins rows
|
|
606
|
+
with "\\n" itself, and `read_source` returns each line's content
|
|
607
|
+
WITH its trailing newline. Keeping both renders the file
|
|
608
|
+
double-spaced — every second line blank — which also breaks
|
|
609
|
+
head/tail truncation's line accounting, since the omission
|
|
610
|
+
message is inserted by row index. Only \\r and \\n come off;
|
|
611
|
+
leading indentation is content and must survive.
|
|
612
|
+
"""
|
|
613
|
+
return str(value).rstrip("\r\n") if value is not None else ""
|
|
614
|
+
|
|
597
615
|
if len(cols) == 1:
|
|
598
|
-
lines = [
|
|
616
|
+
lines = [_cell(r[0]) for r in rows]
|
|
599
617
|
elif "line_number" in cols and "content" in cols:
|
|
600
618
|
ln_idx = cols.index("line_number")
|
|
601
619
|
ct_idx = cols.index("content")
|
|
602
|
-
lines = [f"{r[ln_idx]:4d} {r[ct_idx]}" for r in rows]
|
|
620
|
+
lines = [f"{r[ln_idx]:4d} {_cell(r[ct_idx])}" for r in rows]
|
|
603
621
|
else:
|
|
604
622
|
lines = []
|
|
605
623
|
for row in rows:
|
|
606
|
-
parts = [
|
|
624
|
+
parts = [_cell(v) for v in row if v is not None]
|
|
607
625
|
lines.append(" ".join(parts))
|
|
608
626
|
if omission:
|
|
609
627
|
lines.insert(_HEAD_TAIL, omission)
|
|
@@ -8,7 +8,10 @@ fledgling macro tools. They take priority over fledgling equivalents
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import hashlib
|
|
11
12
|
import os
|
|
13
|
+
import re
|
|
14
|
+
import tempfile
|
|
12
15
|
|
|
13
16
|
from fledgling.tools import ToolInfo
|
|
14
17
|
from squackit.tool_config import ToolPresentation
|
|
@@ -79,27 +82,41 @@ def _filtered_source(plucker, source: str, peek: str = DEFAULT_PEEK) -> str:
|
|
|
79
82
|
# prefix (the path before the first wildcard) and walk up to the nearest
|
|
80
83
|
# .gitmodules / .git. The denylist is absolute-pattern based, no root needed.
|
|
81
84
|
prefix = resolved.split("*", 1)[0]
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
candidate = prefix if prefix.endswith("/") else os.path.dirname(prefix)
|
|
86
|
+
# Walk up to the repository that owns these files. If there ISN'T one,
|
|
87
|
+
# leave root empty and skip submodule exclusion entirely: there are no
|
|
88
|
+
# submodules to exclude, and the walk otherwise degrades to "/", whose
|
|
89
|
+
# rstrip("/") is the empty string. `_submodule_prefixes('')` then reads
|
|
90
|
+
# the filesystem root — which a sandboxed connection refuses, aborting
|
|
91
|
+
# the transaction and taking down every later query on this connection.
|
|
92
|
+
root = ""
|
|
93
|
+
while candidate and candidate != "/":
|
|
94
|
+
if os.path.exists(os.path.join(candidate, ".gitmodules")) or os.path.exists(
|
|
95
|
+
os.path.join(candidate, ".git")
|
|
86
96
|
):
|
|
97
|
+
root = candidate
|
|
87
98
|
break
|
|
88
|
-
|
|
99
|
+
candidate = os.path.dirname(candidate)
|
|
89
100
|
rq = resolved.replace("'", "''")
|
|
90
|
-
rootq = root.rstrip("/").replace("'", "''")
|
|
91
101
|
|
|
92
102
|
total = db.sql(f"SELECT count(*) FROM glob('{rq}')").fetchone()[0]
|
|
93
103
|
if total <= 1:
|
|
94
104
|
return source
|
|
95
105
|
|
|
106
|
+
submodule_clause = ""
|
|
107
|
+
if root:
|
|
108
|
+
rootq = root.rstrip("/").replace("'", "''")
|
|
109
|
+
submodule_clause = (
|
|
110
|
+
f"AND NOT EXISTS (SELECT 1 FROM _submodule_prefixes('{rootq}') s "
|
|
111
|
+
f"WHERE file LIKE s.prefix || '%') "
|
|
112
|
+
)
|
|
113
|
+
|
|
96
114
|
kept = [
|
|
97
115
|
r[0]
|
|
98
116
|
for r in db.sql(
|
|
99
117
|
f"SELECT file FROM glob('{rq}') "
|
|
100
118
|
f"WHERE NOT _is_vendored_path(file) "
|
|
101
|
-
f"
|
|
102
|
-
f"WHERE file LIKE s.prefix || '%') "
|
|
119
|
+
f"{submodule_clause}"
|
|
103
120
|
f"ORDER BY file"
|
|
104
121
|
).fetchall()
|
|
105
122
|
]
|
|
@@ -115,9 +132,81 @@ def _filtered_source(plucker, source: str, peek: str = DEFAULT_PEEK) -> str:
|
|
|
115
132
|
except Exception:
|
|
116
133
|
# Ignore-policy macros unavailable / SQL error — fall back to the raw
|
|
117
134
|
# glob. Unfiltered (flooded) results beat a broken tool.
|
|
135
|
+
#
|
|
136
|
+
# ROLL BACK FIRST. A failed statement leaves the DuckDB transaction in
|
|
137
|
+
# an aborted state, so swallowing the error without clearing it poisons
|
|
138
|
+
# every later query on this connection with "Current transaction is
|
|
139
|
+
# aborted (please ROLLBACK)" — including the AST cache lookup that runs
|
|
140
|
+
# moments later. The caller then sees a transaction error from a
|
|
141
|
+
# statement that never failed, pointing at the wrong subsystem entirely.
|
|
142
|
+
# Degrading gracefully means the connection is still usable afterwards,
|
|
143
|
+
# which is the whole promise of this except clause.
|
|
144
|
+
try:
|
|
145
|
+
db = plucker._ctx.db
|
|
146
|
+
if db is not None:
|
|
147
|
+
db.execute("ROLLBACK")
|
|
148
|
+
except Exception:
|
|
149
|
+
pass # no transaction open, or the connection is beyond saving
|
|
118
150
|
return source
|
|
119
151
|
|
|
120
152
|
|
|
153
|
+
def _source_root(source: str) -> str | None:
|
|
154
|
+
"""The directory that must be readable for *source* to resolve.
|
|
155
|
+
|
|
156
|
+
A fledgling connection is sandboxed to its root (fledgling >= 0.13), so a
|
|
157
|
+
Plucker built without one is confined to the process working directory and
|
|
158
|
+
cannot read a source anywhere else: ``find(source="/srv/code/x/**/*.py")``
|
|
159
|
+
raised a permission error instead of returning results. These tools take
|
|
160
|
+
their target as an argument, so the target is the scope — rooting the
|
|
161
|
+
connection at the source's own directory keeps the sandbox meaningful while
|
|
162
|
+
letting an explicit path resolve.
|
|
163
|
+
|
|
164
|
+
Returns None when there is nothing to root at (a bare table/view name, or a
|
|
165
|
+
directory that does not exist), leaving pluckit's default behaviour alone.
|
|
166
|
+
"""
|
|
167
|
+
if not source:
|
|
168
|
+
return None
|
|
169
|
+
# A bare identifier is a DuckDB table/view name, not a path.
|
|
170
|
+
if not any(sep in source for sep in ("/", os.sep)):
|
|
171
|
+
return None
|
|
172
|
+
# RELATIVE sources are already handled: they resolve against the working
|
|
173
|
+
# directory, which is what an un-rooted Plucker sandboxes to. Re-rooting
|
|
174
|
+
# them would be actively wrong — _filtered_source joins a relative source
|
|
175
|
+
# onto the connection's repo, so pointing that repo at the source's own
|
|
176
|
+
# directory turns `squackit/**/*.py` into `.../squackit/squackit/**/*.py`
|
|
177
|
+
# and the tool finds nothing.
|
|
178
|
+
if not os.path.isabs(source):
|
|
179
|
+
return None
|
|
180
|
+
head = re.split(r"[*?\[]", source, maxsplit=1)[0]
|
|
181
|
+
directory = head if head.endswith(("/", os.sep)) else os.path.dirname(head)
|
|
182
|
+
if not directory:
|
|
183
|
+
return None
|
|
184
|
+
directory = os.path.abspath(directory)
|
|
185
|
+
return directory if os.path.isdir(directory) else None
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cache_path(root: str | None) -> str:
|
|
189
|
+
"""Where the AST cache database lives for a Plucker rooted at *root*.
|
|
190
|
+
|
|
191
|
+
DuckDB's filesystem allow-list is a property of the database INSTANCE, not
|
|
192
|
+
of the connection, and every connection to the same file shares one
|
|
193
|
+
instance. A single shared cache file therefore means the FIRST lockdown
|
|
194
|
+
wins for the life of the process: a plucker rooted at /srv/code/x reuses
|
|
195
|
+
the instance already locked to the working directory and is refused access
|
|
196
|
+
to its own source, with no indication that its root was ignored.
|
|
197
|
+
|
|
198
|
+
So a rooted plucker gets its own cache file, keyed by the root. It goes in
|
|
199
|
+
the temp directory rather than inside the root: a served corpus is mounted
|
|
200
|
+
read-only, and writing a database into a tree we were only asked to read is
|
|
201
|
+
rude even where it succeeds. Keying by root keeps the cache reusable across
|
|
202
|
+
processes, which is the point of persisting it at all.
|
|
203
|
+
"""
|
|
204
|
+
if root is None:
|
|
205
|
+
return os.path.join(os.getcwd(), ".pluckit.duckdb")
|
|
206
|
+
digest = hashlib.sha256(os.path.abspath(root).encode()).hexdigest()[:16]
|
|
207
|
+
return os.path.join(tempfile.gettempdir(), f"squackit-ast-{digest}.duckdb")
|
|
208
|
+
|
|
209
|
+
|
|
121
210
|
def _make_plucker(source: str, peek: str = DEFAULT_PEEK):
|
|
122
211
|
"""Create a Plucker with AstViewer and Search for tool execution.
|
|
123
212
|
|
|
@@ -135,7 +224,21 @@ def _make_plucker(source: str, peek: str = DEFAULT_PEEK):
|
|
|
135
224
|
# parses with ``peek := 'none'`` and can only ever return NULL. Enabling the
|
|
136
225
|
# cache moves the un-filtered path onto ``ast_select_from`` too, so both
|
|
137
226
|
# paths return source text. It also stops re-parsing on every call.
|
|
138
|
-
|
|
227
|
+
#
|
|
228
|
+
# The cache path is passed EXPLICITLY rather than left to `cache=True`.
|
|
229
|
+
# pluckit derives the default cache file from the repo (`<repo>/.pluckit.duckdb`),
|
|
230
|
+
# so rooting the connection at the source would also move the cache into the
|
|
231
|
+
# tree being analysed — which for a served corpus is a read-only mount, and
|
|
232
|
+
# for anyone else means writing a database into a repo they only asked us to
|
|
233
|
+
# read. Keeping it under the working directory preserves the location this
|
|
234
|
+
# had before the connection gained a root.
|
|
235
|
+
root = _source_root(source)
|
|
236
|
+
p = Plucker(
|
|
237
|
+
plugins=[AstViewer, Search],
|
|
238
|
+
cache=_cache_path(root),
|
|
239
|
+
peek=peek,
|
|
240
|
+
repo=root,
|
|
241
|
+
)
|
|
139
242
|
p._code_source = _filtered_source(p, source, peek)
|
|
140
243
|
return p
|
|
141
244
|
|
|
@@ -95,8 +95,8 @@ def _sorted_table(con, macro_name, kwargs, sort_col, max_rows, descending=True):
|
|
|
95
95
|
def explore(con, defaults, path=None):
|
|
96
96
|
"""First-contact codebase briefing."""
|
|
97
97
|
# Scope patterns to path if provided
|
|
98
|
-
code_pattern = defaults.scoped_code_pattern(path) if path else defaults.
|
|
99
|
-
doc_pattern =
|
|
98
|
+
code_pattern = defaults.scoped_code_pattern(path) if path else defaults.code_glob
|
|
99
|
+
doc_pattern = defaults.scoped_doc_pattern(path) if path else defaults.doc_glob
|
|
100
100
|
|
|
101
101
|
sections = []
|
|
102
102
|
|
|
@@ -149,7 +149,9 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
|
|
|
149
149
|
# Precedence: explicit path -> runtime.active_root -> process cwd.
|
|
150
150
|
# Runtime fallback lets a session set active_root once via
|
|
151
151
|
# `config(set={"active_root": X})` instead of passing path= per call.
|
|
152
|
-
file_pattern = defaults.scoped_code_pattern(
|
|
152
|
+
file_pattern = defaults.scoped_code_pattern(
|
|
153
|
+
resolve_scope_path(path, default=defaults.root)
|
|
154
|
+
)
|
|
153
155
|
|
|
154
156
|
# 1. Find definitions matching the name. Fetch substring candidates, then
|
|
155
157
|
# prefer EXACT name matches: a deep-dive on `ensure_loaded` must not drag
|
|
@@ -269,7 +271,7 @@ def review(con, defaults, from_rev=None, to_rev=None, file_pattern=None):
|
|
|
269
271
|
"""Code review prep for a revision range."""
|
|
270
272
|
from_rev = from_rev or defaults.from_rev
|
|
271
273
|
to_rev = to_rev or defaults.to_rev
|
|
272
|
-
file_pattern = file_pattern or defaults.
|
|
274
|
+
file_pattern = file_pattern or defaults.code_glob
|
|
273
275
|
|
|
274
276
|
sections = []
|
|
275
277
|
|
|
@@ -349,7 +351,7 @@ def review(con, defaults, from_rev=None, to_rev=None, file_pattern=None):
|
|
|
349
351
|
|
|
350
352
|
def search(con, defaults, query, file_pattern=None):
|
|
351
353
|
"""Multi-source search across code, docs, and git."""
|
|
352
|
-
file_pattern = file_pattern or defaults.
|
|
354
|
+
file_pattern = file_pattern or defaults.code_glob
|
|
353
355
|
|
|
354
356
|
sections = []
|
|
355
357
|
|
|
@@ -371,7 +373,7 @@ def search(con, defaults, query, file_pattern=None):
|
|
|
371
373
|
# 3. Documentation sections matching the query
|
|
372
374
|
sections.append(_section("Documentation", lambda: _table(
|
|
373
375
|
con, "doc_outline",
|
|
374
|
-
{"file_pattern": defaults.
|
|
376
|
+
{"file_pattern": defaults.doc_glob, "search": query},
|
|
375
377
|
max_rows=10,
|
|
376
378
|
)))
|
|
377
379
|
|
|
@@ -46,11 +46,14 @@ class TestToolDefaults:
|
|
|
46
46
|
for tool in code_tools:
|
|
47
47
|
assert tool in TOOL_DEFAULTS
|
|
48
48
|
assert "file_pattern" in TOOL_DEFAULTS[tool]
|
|
49
|
-
|
|
49
|
+
# The ANCHORED accessor, not the raw pattern: a substituted value
|
|
50
|
+
# goes straight to DuckDB, which resolves a relative glob against
|
|
51
|
+
# the process cwd rather than the sandboxed project root.
|
|
52
|
+
assert TOOL_DEFAULTS[tool]["file_pattern"] == "code_glob"
|
|
50
53
|
|
|
51
54
|
def test_doc_tools_mapped(self):
|
|
52
55
|
assert "doc_outline" in TOOL_DEFAULTS
|
|
53
|
-
assert TOOL_DEFAULTS["doc_outline"]["file_pattern"] == "
|
|
56
|
+
assert TOOL_DEFAULTS["doc_outline"]["file_pattern"] == "doc_glob"
|
|
54
57
|
|
|
55
58
|
def test_git_tools_mapped(self):
|
|
56
59
|
git_tools = ["file_changes", "file_diff", "structural_diff"]
|
|
@@ -60,12 +63,20 @@ class TestToolDefaults:
|
|
|
60
63
|
assert TOOL_DEFAULTS[tool]["to_rev"] == "to_rev"
|
|
61
64
|
|
|
62
65
|
def test_all_field_names_are_valid(self):
|
|
63
|
-
"""Every value in TOOL_DEFAULTS must name
|
|
64
|
-
|
|
66
|
+
"""Every value in TOOL_DEFAULTS must name something apply_defaults can
|
|
67
|
+
actually read off ProjectDefaults.
|
|
68
|
+
|
|
69
|
+
Checks attributes rather than dataclass fields: the pattern entries
|
|
70
|
+
name read-only properties (code_glob / doc_glob), which apply_defaults
|
|
71
|
+
reaches via getattr just the same. The point of the test is that a
|
|
72
|
+
typo here fails loudly at import rather than substituting None into a
|
|
73
|
+
query, and that survives the field-vs-property distinction.
|
|
74
|
+
"""
|
|
75
|
+
probe = ProjectDefaults()
|
|
65
76
|
for tool, mapping in TOOL_DEFAULTS.items():
|
|
66
|
-
for _param,
|
|
67
|
-
assert
|
|
68
|
-
f"{tool}: '{
|
|
77
|
+
for _param, attr_name in mapping.items():
|
|
78
|
+
assert hasattr(probe, attr_name), (
|
|
79
|
+
f"{tool}: '{attr_name}' is not a ProjectDefaults attribute"
|
|
69
80
|
)
|
|
70
81
|
|
|
71
82
|
|
|
@@ -185,8 +196,16 @@ class TestInferDefaults:
|
|
|
185
196
|
def con(self):
|
|
186
197
|
return create_connection(repo=str(PROJECT_ROOT))
|
|
187
198
|
|
|
199
|
+
@requires_fledgling_source
|
|
188
200
|
def test_code_pattern_is_python(self, con):
|
|
189
|
-
"""This repo is primarily Python, so code_pattern should be **/*.py.
|
|
201
|
+
"""This repo is primarily Python, so code_pattern should be **/*.py.
|
|
202
|
+
|
|
203
|
+
Needs a source checkout, like its doc_pattern sibling below: an
|
|
204
|
+
installed fledgling wheel bundles sql/ as package data, so SQL
|
|
205
|
+
outnumbers Python in that layout and the inferred pattern is
|
|
206
|
+
legitimately **/*.sql. Asserting against the wheel fails on the
|
|
207
|
+
content of the dog-food corpus, not on the inference logic.
|
|
208
|
+
"""
|
|
190
209
|
defaults = infer_defaults(con, root=str(PROJECT_ROOT))
|
|
191
210
|
assert "py" in defaults.code_pattern
|
|
192
211
|
|