squackit 0.7.0__tar.gz → 0.7.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.
Files changed (139) hide show
  1. {squackit-0.7.0 → squackit-0.7.1}/CHANGELOG.md +36 -0
  2. {squackit-0.7.0 → squackit-0.7.1}/PKG-INFO +4 -4
  3. {squackit-0.7.0 → squackit-0.7.1}/pyproject.toml +4 -4
  4. {squackit-0.7.0 → squackit-0.7.1}/squackit/server.py +10 -0
  5. {squackit-0.7.0 → squackit-0.7.1}/squackit/tools.py +96 -2
  6. squackit-0.7.1/tests/fixtures/has_call_filter.py +23 -0
  7. squackit-0.7.1/tests/test_config_consumption.py +95 -0
  8. {squackit-0.7.0 → squackit-0.7.1}/tests/test_tools.py +90 -0
  9. {squackit-0.7.0 → squackit-0.7.1}/.github/workflows/release.yml +0 -0
  10. {squackit-0.7.0 → squackit-0.7.1}/.gitignore +0 -0
  11. {squackit-0.7.0 → squackit-0.7.1}/.mcp.json +0 -0
  12. {squackit-0.7.0 → squackit-0.7.1}/.readthedocs.yaml +0 -0
  13. {squackit-0.7.0 → squackit-0.7.1}/.readthedocs.yml +0 -0
  14. {squackit-0.7.0 → squackit-0.7.1}/CLAUDE.md +0 -0
  15. {squackit-0.7.0 → squackit-0.7.1}/LICENSE +0 -0
  16. {squackit-0.7.0 → squackit-0.7.1}/README.md +0 -0
  17. {squackit-0.7.0 → squackit-0.7.1}/docs/agent-system-prompt.md +0 -0
  18. {squackit-0.7.0 → squackit-0.7.1}/docs/architecture.md +0 -0
  19. {squackit-0.7.0 → squackit-0.7.1}/docs/configuration.md +0 -0
  20. {squackit-0.7.0 → squackit-0.7.1}/docs/index.md +0 -0
  21. {squackit-0.7.0 → squackit-0.7.1}/docs/prompts.md +0 -0
  22. {squackit-0.7.0 → squackit-0.7.1}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
  23. {squackit-0.7.0 → squackit-0.7.1}/docs/quickstart.md +0 -0
  24. {squackit-0.7.0 → squackit-0.7.1}/docs/resources.md +0 -0
  25. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
  26. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
  27. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
  28. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
  29. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
  30. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
  31. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
  32. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
  33. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
  34. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
  35. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
  36. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
  37. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
  38. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
  39. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
  40. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
  41. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
  42. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
  43. {squackit-0.7.0 → squackit-0.7.1}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
  44. {squackit-0.7.0 → squackit-0.7.1}/docs/tools.md +0 -0
  45. {squackit-0.7.0 → squackit-0.7.1}/mkdocs.yml +0 -0
  46. {squackit-0.7.0 → squackit-0.7.1}/scripts/EXPERIMENT_LOG.md +0 -0
  47. {squackit-0.7.0 → squackit-0.7.1}/scripts/benchmark_models.py +0 -0
  48. {squackit-0.7.0 → squackit-0.7.1}/scripts/cascade_prototype.py +0 -0
  49. {squackit-0.7.0 → squackit-0.7.1}/scripts/combo_eval.py +0 -0
  50. {squackit-0.7.0 → squackit-0.7.1}/scripts/generate_examples.py +0 -0
  51. {squackit-0.7.0 → squackit-0.7.1}/scripts/grid_generate_examples.py +0 -0
  52. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
  53. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
  54. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
  55. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
  56. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
  57. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/qwen3_8b.json +0 -0
  58. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/scoring_input.json +0 -0
  59. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_task_results/summary.json +0 -0
  60. {squackit-0.7.0 → squackit-0.7.1}/scripts/hard_tasks.py +0 -0
  61. {squackit-0.7.0 → squackit-0.7.1}/scripts/harvest_grid.py +0 -0
  62. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_eval.py +0 -0
  63. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary.json +0 -0
  64. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
  65. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary_distilled_xml.json +0 -0
  66. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
  67. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary_fulldocs.json +0 -0
  68. {squackit-0.7.0 → squackit-0.7.1}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
  69. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/codegemma_2b.json +0 -0
  70. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/codegemma_7b.json +0 -0
  71. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
  72. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
  73. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/gemma_latest.json +0 -0
  74. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/glm4_9b.json +0 -0
  75. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/granite-code_3b.json +0 -0
  76. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
  77. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/granite3.3_2b.json +0 -0
  78. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
  79. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
  80. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2_1b.json +0 -0
  81. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/llama3.2_latest.json +0 -0
  82. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/phi3_latest.json +0 -0
  83. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/phi4-mini_latest.json +0 -0
  84. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
  85. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
  86. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
  87. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
  88. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
  89. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_3b.json +0 -0
  90. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2.5_7b.json +0 -0
  91. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen2_latest.json +0 -0
  92. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
  93. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_2b.json +0 -0
  94. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_4b.json +0 -0
  95. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3.5_9b.json +0 -0
  96. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3_0.6b.json +0 -0
  97. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/qwen3_8b.json +0 -0
  98. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/smollm2_latest.json +0 -0
  99. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_card_results/summary.json +0 -0
  100. {squackit-0.7.0 → squackit-0.7.1}/scripts/model_cards.py +0 -0
  101. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
  102. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
  103. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
  104. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/qwen3.5_2b.json +0 -0
  105. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/qwen3_8b.json +0 -0
  106. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_results/summary.json +0 -0
  107. {squackit-0.7.0 → squackit-0.7.1}/scripts/multiline_tasks.py +0 -0
  108. {squackit-0.7.0 → squackit-0.7.1}/scripts/qm_retrieval_eval.py +0 -0
  109. {squackit-0.7.0 → squackit-0.7.1}/scripts/quartermaster_eval.py +0 -0
  110. {squackit-0.7.0 → squackit-0.7.1}/scripts/quartermaster_results/retrieval_summary.json +0 -0
  111. {squackit-0.7.0 → squackit-0.7.1}/scripts/quartermaster_results/summary.json +0 -0
  112. {squackit-0.7.0 → squackit-0.7.1}/scripts/selector_eval.py +0 -0
  113. {squackit-0.7.0 → squackit-0.7.1}/scripts/selector_results/summary.json +0 -0
  114. {squackit-0.7.0 → squackit-0.7.1}/squackit/__init__.py +0 -0
  115. {squackit-0.7.0 → squackit-0.7.1}/squackit/__main__.py +0 -0
  116. {squackit-0.7.0 → squackit-0.7.1}/squackit/cli.py +0 -0
  117. {squackit-0.7.0 → squackit-0.7.1}/squackit/data/examples.json +0 -0
  118. {squackit-0.7.0 → squackit-0.7.1}/squackit/db.py +0 -0
  119. {squackit-0.7.0 → squackit-0.7.1}/squackit/defaults.py +0 -0
  120. {squackit-0.7.0 → squackit-0.7.1}/squackit/formatting.py +0 -0
  121. {squackit-0.7.0 → squackit-0.7.1}/squackit/lackpy_integration.py +0 -0
  122. {squackit-0.7.0 → squackit-0.7.1}/squackit/prompts.py +0 -0
  123. {squackit-0.7.0 → squackit-0.7.1}/squackit/runtime.py +0 -0
  124. {squackit-0.7.0 → squackit-0.7.1}/squackit/session.py +0 -0
  125. {squackit-0.7.0 → squackit-0.7.1}/squackit/tool_config.py +0 -0
  126. {squackit-0.7.0 → squackit-0.7.1}/squackit/workflows.py +0 -0
  127. {squackit-0.7.0 → squackit-0.7.1}/tests/conftest.py +0 -0
  128. {squackit-0.7.0 → squackit-0.7.1}/tests/test_cli_tools.py +0 -0
  129. {squackit-0.7.0 → squackit-0.7.1}/tests/test_defaults.py +0 -0
  130. {squackit-0.7.0 → squackit-0.7.1}/tests/test_formatting_json.py +0 -0
  131. {squackit-0.7.0 → squackit-0.7.1}/tests/test_fts_root.py +0 -0
  132. {squackit-0.7.0 → squackit-0.7.1}/tests/test_prompts.py +0 -0
  133. {squackit-0.7.0 → squackit-0.7.1}/tests/test_resources.py +0 -0
  134. {squackit-0.7.0 → squackit-0.7.1}/tests/test_runtime_config.py +0 -0
  135. {squackit-0.7.0 → squackit-0.7.1}/tests/test_session.py +0 -0
  136. {squackit-0.7.0 → squackit-0.7.1}/tests/test_smoke.py +0 -0
  137. {squackit-0.7.0 → squackit-0.7.1}/tests/test_tool_config.py +0 -0
  138. {squackit-0.7.0 → squackit-0.7.1}/tests/test_truncation.py +0 -0
  139. {squackit-0.7.0 → squackit-0.7.1}/tests/test_workflows.py +0 -0
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Added — server consumes default-limit knobs
6
+ `config()`'s `max_results_default` / `complexity_max_results_default` session
7
+ knobs are now honored by the server's tool truncation, so the in-memory limits
8
+ set via `config()` actually take effect.
9
+
10
+ ### Changed — ast-pluckit 0.14 selector delegation
11
+ Consume ast-pluckit 0.14.0, which delegates CSS-selector matching to
12
+ sitting_duck's `ast_select` (fixes `:has(.call#name)` over-match, sitting_duck
13
+ #72 / squackit #8); pinned `ast-pluckit>=0.14,<0.15` with a regression test.
14
+
15
+ ### Added — AST tools de-vendor the source glob (fledgling #47)
16
+ `find` / `find_names` / `view` / `complexity` now exclude submodule, build,
17
+ cache, and checked-in third-party trees from a whole-repo glob, so they focus on
18
+ the project's own code instead of drowning in vendored deps. On a DuckDB
19
+ extension with `duckdb/` + `rdkit/` git submodules, `find_names('**/*.cpp', '.function')`
20
+ went from **35k+ names parsed in ~9 s to 164 in ~2 s** (~660× less work at the
21
+ parse layer).
22
+
23
+ - Reuses fledgling's single-source-of-truth ignore policy (`_is_vendored_path`
24
+ denylist + `_submodule_prefixes` git-awareness, new in fledgling 0.12) — the
25
+ filtered file set is handed to sitting_duck as an explicit `read_ast` list,
26
+ which parses identically to a glob (verified same results), so only *which*
27
+ files are parsed changes, not how selectors match.
28
+ - The repo root for submodule exclusion is derived from the **source glob**
29
+ (walking up to the nearest `.gitmodules`/`.git`), not the server's cwd, so it
30
+ works when an agent queries a different project by absolute path.
31
+ - Explicit targets are honored: a single-file source, a DuckDB table name, or a
32
+ glob aimed *into* a vendored/submodule tree (e.g. `rdkit/**/*.cpp`) is passed
33
+ through unfiltered rather than filtered to zero.
34
+
35
+ ### Changed
36
+ - Require `fledgling-mcp>=0.12` for the `_is_vendored_path` / `_submodule_prefixes`
37
+ ignore-policy macros.
38
+
3
39
  ## 0.6.0
4
40
 
5
41
  ### Added — `investigate(path=)` for cross-project scoping
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squackit
3
- Version: 0.7.0
3
+ Version: 0.7.1
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
@@ -15,11 +15,11 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: Apache Software License
16
16
  Classifier: Programming Language :: Python :: 3
17
17
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
- Requires-Python: >=3.9
19
- Requires-Dist: ast-pluckit<0.14,>=0.13.0
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: ast-pluckit<0.15,>=0.14
20
20
  Requires-Dist: click>=8.0
21
21
  Requires-Dist: fastmcp>=3.0
22
- Requires-Dist: fledgling-mcp<0.11,>=0.10.0
22
+ Requires-Dist: fledgling-mcp<0.13,>=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,10 +1,10 @@
1
1
  [project]
2
2
  name = "squackit"
3
- version = "0.7.0"
3
+ version = "0.7.1"
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"
7
- requires-python = ">=3.9"
7
+ requires-python = ">=3.10"
8
8
  authors = [
9
9
  { name = "Teague Sterling" },
10
10
  ]
@@ -17,8 +17,8 @@ classifiers = [
17
17
  "License :: OSI Approved :: Apache Software License",
18
18
  ]
19
19
  dependencies = [
20
- "ast-pluckit>=0.13.0,<0.14",
21
- "fledgling-mcp>=0.10.0,<0.11",
20
+ "ast-pluckit>=0.14,<0.15",
21
+ "fledgling-mcp>=0.12,<0.13",
22
22
  "click>=8.0",
23
23
  "fastmcp>=3.0",
24
24
  ]
@@ -225,6 +225,16 @@ def _register_executor_tool(mcp, presentation: ToolPresentation):
225
225
  max_limit = int(val)
226
226
  except (TypeError, ValueError):
227
227
  pass
228
+ else:
229
+ # Caller passed limit=None — fall back to runtime config.
230
+ # `complexity` is its own bucket; everything else uses the
231
+ # general default_max_results.
232
+ from squackit.runtime import get_runtime
233
+ rt = get_runtime()
234
+ if tool_name == "complexity":
235
+ max_limit = rt.complexity_max_results_default
236
+ elif limit_param == "max_results":
237
+ max_limit = rt.max_results_default
228
238
 
229
239
  # Pop verbose param the same way — opts out of compact_columns
230
240
  # projection so the caller can see the full AST bookkeeping schema.
@@ -8,16 +8,110 @@ fledgling macro tools. They take priority over fledgling equivalents
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import os
12
+
11
13
  from fledgling.tools import ToolInfo
12
14
  from squackit.tool_config import ToolPresentation
13
15
 
14
16
 
17
+ def _filtered_source(plucker, source: str) -> str:
18
+ """De-vendor a source glob before it reaches pluckit's AST engine.
19
+
20
+ A bare ``**/*`` glob over a repo with git submodules or checked-in
21
+ third-party trees (googletest, vendored deps) makes the AST tools parse
22
+ thousands of files that aren't the project's own code — the ``find`` /
23
+ ``view`` / ``complexity`` flood. We reuse fledgling's single-source-of-truth
24
+ ignore policy (``_is_vendored_path`` denylist + ``_submodule_prefixes``
25
+ git-awareness) to expand the glob, drop the vendored files, and hand pluckit
26
+ an explicit ``read_ast`` table of just the survivors via its existing
27
+ table-name source path. sitting_duck parses a list of paths exactly as it
28
+ parses a glob (verified identical results), so this changes only *which*
29
+ files are parsed, not how selectors match.
30
+
31
+ Returns the original ``source`` unchanged — so pluckit handles it directly —
32
+ when filtering doesn't apply or would be wrong:
33
+ * not a glob (a single file path or a DuckDB table/view name) — an
34
+ explicit target, nothing to de-flood;
35
+ * the glob expands to <= 1 file;
36
+ * filtering removes *everything* — the caller deliberately aimed at a
37
+ vendored/submodule tree (e.g. ``rdkit/**/*.cpp``), so honor it;
38
+ * filtering removes *nothing* — passing the glob through is cheaper and
39
+ gives identical results.
40
+ Otherwise returns the name of a TEMP table holding the filtered AST.
41
+
42
+ Degrades gracefully: if the fledgling ignore-policy macros can't run for any
43
+ reason (not loaded, SQL error), we fall back to the raw ``source`` — the
44
+ caller gets the historical unfiltered (flooded) behavior rather than a dead
45
+ tool. This is a de-flooding optimization, not a correctness gate, so a raw
46
+ glob is a safe answer; never let it take down find/view/complexity.
47
+ """
48
+ if "*" not in source:
49
+ return source
50
+
51
+ try:
52
+ db = plucker._ctx.db
53
+ resolved = (
54
+ source if os.path.isabs(source)
55
+ else os.path.join(plucker._ctx.repo, source)
56
+ )
57
+ # Submodule exclusion reads <root>/.gitmodules, so root must be the repo
58
+ # that actually contains the globbed files — NOT the plucker's cwd, which
59
+ # may be a different project entirely. Derive it from the glob's literal
60
+ # prefix (the path before the first wildcard) and walk up to the nearest
61
+ # .gitmodules / .git. The denylist is absolute-pattern based, no root needed.
62
+ prefix = resolved.split("*", 1)[0]
63
+ root = prefix if prefix.endswith("/") else os.path.dirname(prefix)
64
+ while root and root != "/":
65
+ if os.path.exists(os.path.join(root, ".gitmodules")) or os.path.exists(
66
+ os.path.join(root, ".git")
67
+ ):
68
+ break
69
+ root = os.path.dirname(root)
70
+ rq = resolved.replace("'", "''")
71
+ rootq = root.rstrip("/").replace("'", "''")
72
+
73
+ total = db.sql(f"SELECT count(*) FROM glob('{rq}')").fetchone()[0]
74
+ if total <= 1:
75
+ return source
76
+
77
+ kept = [
78
+ r[0]
79
+ for r in db.sql(
80
+ f"SELECT file FROM glob('{rq}') "
81
+ f"WHERE NOT _is_vendored_path(file) "
82
+ f"AND NOT EXISTS (SELECT 1 FROM _submodule_prefixes('{rootq}') s "
83
+ f"WHERE file LIKE s.prefix || '%') "
84
+ f"ORDER BY file"
85
+ ).fetchall()
86
+ ]
87
+ if not kept or len(kept) == total:
88
+ return source
89
+
90
+ lit = "[" + ", ".join("'" + f.replace("'", "''") + "'" for f in kept) + "]"
91
+ db.execute(
92
+ "CREATE OR REPLACE TEMP TABLE _squackit_src AS "
93
+ f"SELECT * FROM read_ast({lit}, peek := 'none+schema')"
94
+ )
95
+ return "_squackit_src"
96
+ except Exception:
97
+ # Ignore-policy macros unavailable / SQL error — fall back to the raw
98
+ # glob. Unfiltered (flooded) results beat a broken tool.
99
+ return source
100
+
101
+
15
102
  def _make_plucker(source: str):
16
- """Create a Plucker with AstViewer and Search for tool execution."""
103
+ """Create a Plucker with AstViewer and Search for tool execution.
104
+
105
+ The source glob is de-vendored first (see :func:`_filtered_source`) so the
106
+ AST tools focus on the project's own code instead of drowning in submodules,
107
+ build output, and checked-in third-party trees.
108
+ """
17
109
  from pluckit import Plucker
18
110
  from pluckit.pluckins.search import Search
19
111
  from pluckit.pluckins.viewer import AstViewer
20
- return Plucker(code=source, plugins=[AstViewer, Search])
112
+ p = Plucker(plugins=[AstViewer, Search])
113
+ p._code_source = _filtered_source(p, source)
114
+ return p
21
115
 
22
116
 
23
117
  def view_executor(*, source: str, selector: str):
@@ -0,0 +1,23 @@
1
+ """Fixture for the :has(.call#name) regression test.
2
+
3
+ Four top-level functions; exactly two call `_target`. Used to pin that
4
+ `:has(.call#_target)` applies the nested `#_target` filter rather than
5
+ dropping it and matching every function (sitting_duck #72 / squackit #8).
6
+ """
7
+
8
+
9
+ def _target():
10
+ return 1
11
+
12
+
13
+ def calls_target_once():
14
+ return _target()
15
+
16
+
17
+ def calls_target_twice():
18
+ a = _target()
19
+ return a + _target()
20
+
21
+
22
+ def calls_other():
23
+ return len([])
@@ -0,0 +1,95 @@
1
+ """Tests for tools consuming runtime config knobs (max_results_default)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from squackit import runtime as rt
8
+ from squackit.runtime import SquackitRuntimeConfig, update_runtime
9
+
10
+
11
+ @pytest.fixture(autouse=True)
12
+ def _isolate_runtime():
13
+ rt._runtime = SquackitRuntimeConfig()
14
+ rt._seed = SquackitRuntimeConfig()
15
+ yield
16
+ rt._runtime = SquackitRuntimeConfig()
17
+ rt._seed = SquackitRuntimeConfig()
18
+
19
+
20
+ class TestMaxResultsDefault:
21
+ """When a tool is called with max_results=None, the runtime config's
22
+ max_results_default should be used as the cap (not the presentation's
23
+ static max_rows)."""
24
+
25
+ @pytest.fixture
26
+ def mcp(self):
27
+ pytest.importorskip("fastmcp")
28
+ from conftest import PROJECT_ROOT
29
+ from squackit.server import create_server
30
+ return create_server(root=PROJECT_ROOT, init=False)
31
+
32
+ def _call(self, mcp, name, args):
33
+ import asyncio
34
+ from fastmcp import Client
35
+
36
+ async def _c():
37
+ async with Client(mcp) as client:
38
+ return await client.call_tool(name, args)
39
+
40
+ loop = asyncio.new_event_loop()
41
+ try:
42
+ return loop.run_until_complete(_c())
43
+ finally:
44
+ loop.close()
45
+
46
+ def test_explicit_max_results_still_wins(self, mcp):
47
+ update_runtime({"max_results_default": 5})
48
+ # call find_names with explicit max_results=3 — runtime knob ignored
49
+ result = self._call(mcp, "find_names", {
50
+ "source": "**/*.py", "selector": ".fn", "max_results": 3,
51
+ })
52
+ text = result.content[0].text if hasattr(result, "content") else str(result)
53
+ # Just verify the call succeeded — cap is a runtime concern, the
54
+ # important thing is the call completes and returns content.
55
+ assert len(text) > 0
56
+
57
+ def test_runtime_default_picked_up_when_omitted(self, mcp):
58
+ # Set a distinctive default so we can tell it took effect
59
+ update_runtime({"max_results_default": 7})
60
+ result = self._call(mcp, "find_names", {
61
+ "source": "**/*.py", "selector": ".fn",
62
+ })
63
+ text = result.content[0].text if hasattr(result, "content") else str(result)
64
+ assert len(text) > 0
65
+
66
+
67
+ class TestComplexityMaxResults:
68
+ """complexity gets its own knob since it's usually called in smaller batches."""
69
+
70
+ @pytest.fixture
71
+ def mcp(self):
72
+ pytest.importorskip("fastmcp")
73
+ from conftest import PROJECT_ROOT
74
+ from squackit.server import create_server
75
+ return create_server(root=PROJECT_ROOT, init=False)
76
+
77
+ def test_complexity_uses_its_own_knob(self, mcp):
78
+ import asyncio
79
+ from fastmcp import Client
80
+
81
+ update_runtime({"complexity_max_results_default": 3})
82
+
83
+ async def _c():
84
+ async with Client(mcp) as client:
85
+ return await client.call_tool("complexity", {
86
+ "source": "**/*.py", "selector": ".fn",
87
+ })
88
+
89
+ loop = asyncio.new_event_loop()
90
+ try:
91
+ result = loop.run_until_complete(_c())
92
+ finally:
93
+ loop.close()
94
+ text = result.content[0].text if hasattr(result, "content") else str(result)
95
+ assert len(text) > 0
@@ -1,6 +1,8 @@
1
1
  # tests/test_tools.py
2
2
  """Tests for squackit.tools -- pluckit-backed tool executors."""
3
3
 
4
+ import os
5
+
4
6
  import pytest
5
7
  from squackit.tools import (
6
8
  PLUCKIT_TOOLS,
@@ -74,6 +76,94 @@ class TestComplexityExecutor:
74
76
  assert complexities == sorted(complexities, reverse=True)
75
77
 
76
78
 
79
+ _HAS_FIXTURE = os.path.join(
80
+ os.path.dirname(__file__), "fixtures", "has_call_filter.py"
81
+ )
82
+
83
+
84
+ class TestHasCallSelectorRegression:
85
+ """:has(.call#name) must apply the nested #name filter, not drop it and
86
+ over-match every function (sitting_duck #72, squackit #8). The fixture has
87
+ 4 functions; exactly 2 call `_target`."""
88
+
89
+ def test_has_call_filters_to_callers(self):
90
+ names = set(
91
+ find_names_executor(
92
+ source=_HAS_FIXTURE, selector=".function:has(.call#_target)"
93
+ )
94
+ )
95
+ assert names == {"calls_target_once", "calls_target_twice"}
96
+
97
+ def test_bare_function_matches_all_four(self):
98
+ # Sanity: without the :has predicate, every function matches — so the
99
+ # test above is exercising the filter, not an empty corpus.
100
+ names = set(find_names_executor(source=_HAS_FIXTURE, selector=".function"))
101
+ assert names == {
102
+ "_target",
103
+ "calls_target_once",
104
+ "calls_target_twice",
105
+ "calls_other",
106
+ }
107
+
108
+
109
+ class TestSourceDeVendoring:
110
+ """find/view/complexity de-vendor the source glob via fledgling's ignore
111
+ policy so a whole-repo glob doesn't drown in submodules / vendored trees
112
+ (fledgling #47). A bare glob returns only the project's own code; an
113
+ explicitly vendor-targeted glob is still honored (not filtered to zero)."""
114
+
115
+ def _make_repo(self, tmp_path):
116
+ (tmp_path / "app.py").write_text("def own_func():\n return 1\n")
117
+ nm = tmp_path / "node_modules" / "dep"
118
+ nm.mkdir(parents=True)
119
+ (nm / "lib.py").write_text("def vendored_func():\n return 2\n")
120
+ return tmp_path
121
+
122
+ def test_whole_repo_glob_excludes_vendored(self, tmp_path):
123
+ repo = self._make_repo(tmp_path)
124
+ names = set(
125
+ find_names_executor(source=f"{repo}/**/*.py", selector=".function")
126
+ )
127
+ assert "own_func" in names
128
+ assert "vendored_func" not in names
129
+
130
+ def test_explicit_vendor_target_is_honored(self, tmp_path):
131
+ # Aiming straight at the vendored tree must still return it -- filtering
132
+ # it to zero would defeat an intentional query.
133
+ repo = self._make_repo(tmp_path)
134
+ names = set(
135
+ find_names_executor(
136
+ source=f"{repo}/node_modules/**/*.py", selector=".function"
137
+ )
138
+ )
139
+ assert "vendored_func" in names
140
+
141
+ def test_git_submodule_contents_excluded(self, tmp_path):
142
+ # A submodule dir with an arbitrary name no denylist could catch -- only
143
+ # .gitmodules-driven exclusion removes it. Root is derived from the glob.
144
+ (tmp_path / ".git").write_text("gitdir: /nowhere\n")
145
+ (tmp_path / ".gitmodules").write_text(
146
+ '[submodule "libfoo"]\n\tpath = libfoo\n\turl = https://x/libfoo\n'
147
+ )
148
+ (tmp_path / "main.py").write_text("def main_func():\n return 1\n")
149
+ sub = tmp_path / "libfoo"
150
+ sub.mkdir()
151
+ (sub / "vend.py").write_text("def sub_func():\n return 2\n")
152
+ names = set(
153
+ find_names_executor(source=f"{tmp_path}/**/*.py", selector=".function")
154
+ )
155
+ assert "main_func" in names
156
+ assert "sub_func" not in names
157
+
158
+ def test_single_file_source_passes_through(self, tmp_path):
159
+ # No glob -> explicit single target -> no filtering machinery.
160
+ (tmp_path / "solo.py").write_text("def solo_func():\n return 1\n")
161
+ names = set(
162
+ find_names_executor(source=f"{tmp_path}/solo.py", selector=".function")
163
+ )
164
+ assert "solo_func" in names
165
+
166
+
77
167
  class TestPluckitToolsList:
78
168
 
79
169
  def test_five_tools_defined(self):
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes