squackit 0.7.0__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. {squackit-0.7.0 → squackit-0.8.0}/.gitignore +4 -0
  2. squackit-0.8.0/CHANGELOG.md +127 -0
  3. {squackit-0.7.0 → squackit-0.8.0}/PKG-INFO +5 -5
  4. {squackit-0.7.0 → squackit-0.8.0}/pyproject.toml +4 -4
  5. {squackit-0.7.0 → squackit-0.8.0}/squackit/server.py +10 -0
  6. {squackit-0.7.0 → squackit-0.8.0}/squackit/tools.py +125 -5
  7. {squackit-0.7.0 → squackit-0.8.0}/squackit/workflows.py +13 -1
  8. {squackit-0.7.0 → squackit-0.8.0}/tests/conftest.py +32 -0
  9. squackit-0.8.0/tests/fixtures/has_call_filter.py +23 -0
  10. squackit-0.8.0/tests/test_config_consumption.py +95 -0
  11. {squackit-0.7.0 → squackit-0.8.0}/tests/test_defaults.py +3 -1
  12. {squackit-0.7.0 → squackit-0.8.0}/tests/test_tools.py +114 -0
  13. {squackit-0.7.0 → squackit-0.8.0}/tests/test_truncation.py +5 -1
  14. {squackit-0.7.0 → squackit-0.8.0}/tests/test_workflows.py +78 -0
  15. squackit-0.7.0/CHANGELOG.md +0 -56
  16. {squackit-0.7.0 → squackit-0.8.0}/.github/workflows/release.yml +0 -0
  17. {squackit-0.7.0 → squackit-0.8.0}/.mcp.json +0 -0
  18. {squackit-0.7.0 → squackit-0.8.0}/.readthedocs.yaml +0 -0
  19. {squackit-0.7.0 → squackit-0.8.0}/.readthedocs.yml +0 -0
  20. {squackit-0.7.0 → squackit-0.8.0}/CLAUDE.md +0 -0
  21. {squackit-0.7.0 → squackit-0.8.0}/LICENSE +0 -0
  22. {squackit-0.7.0 → squackit-0.8.0}/README.md +0 -0
  23. {squackit-0.7.0 → squackit-0.8.0}/docs/agent-system-prompt.md +0 -0
  24. {squackit-0.7.0 → squackit-0.8.0}/docs/architecture.md +0 -0
  25. {squackit-0.7.0 → squackit-0.8.0}/docs/configuration.md +0 -0
  26. {squackit-0.7.0 → squackit-0.8.0}/docs/index.md +0 -0
  27. {squackit-0.7.0 → squackit-0.8.0}/docs/prompts.md +0 -0
  28. {squackit-0.7.0 → squackit-0.8.0}/docs/proposals/2026-06-04-response-type-improvements.md +0 -0
  29. {squackit-0.7.0 → squackit-0.8.0}/docs/quickstart.md +0 -0
  30. {squackit-0.7.0 → squackit-0.8.0}/docs/resources.md +0 -0
  31. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-10-phase1-handoff.md +0 -0
  32. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-10-squawkit-extraction.md +0 -0
  33. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-11-phase3-pluckit-rewire.md +0 -0
  34. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-12-cli-tool-exposure.md +0 -0
  35. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-12-pluckit-tool-integration.md +0 -0
  36. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-12-session-handoff.md +0 -0
  37. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-16-session-handoff.md +0 -0
  38. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-26-named-fts-collections.md +0 -0
  39. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/plans/2026-04-27-kibitzer-fts-doc-retrieval.md +0 -0
  40. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/skills/exploring-unfamiliar-code/SKILL.md +0 -0
  41. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/skills/exploring-unfamiliar-docs/SKILL.md +0 -0
  42. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/skills/pluck-chains/SKILL.md +0 -0
  43. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/skills/refactoring-with-squackit/SKILL.md +0 -0
  44. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/skills/using-squackit/SKILL.md +0 -0
  45. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/specs/2026-04-10-squawkit-design.md +0 -0
  46. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/specs/2026-04-12-cli-tool-exposure-design.md +0 -0
  47. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/specs/2026-04-12-pluckit-tool-integration-design.md +0 -0
  48. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/specs/2026-04-16-training-data-pipeline-design.md +0 -0
  49. {squackit-0.7.0 → squackit-0.8.0}/docs/superpowers/specs/2026-04-26-named-fts-collections-design.md +0 -0
  50. {squackit-0.7.0 → squackit-0.8.0}/docs/tools.md +0 -0
  51. {squackit-0.7.0 → squackit-0.8.0}/mkdocs.yml +0 -0
  52. {squackit-0.7.0 → squackit-0.8.0}/scripts/EXPERIMENT_LOG.md +0 -0
  53. {squackit-0.7.0 → squackit-0.8.0}/scripts/benchmark_models.py +0 -0
  54. {squackit-0.7.0 → squackit-0.8.0}/scripts/cascade_prototype.py +0 -0
  55. {squackit-0.7.0 → squackit-0.8.0}/scripts/combo_eval.py +0 -0
  56. {squackit-0.7.0 → squackit-0.8.0}/scripts/generate_examples.py +0 -0
  57. {squackit-0.7.0 → squackit-0.8.0}/scripts/grid_generate_examples.py +0 -0
  58. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/deepseek-coder-v2_16b.json +0 -0
  59. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/qwen2.5-coder_0.5b.json +0 -0
  60. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/qwen2.5-coder_3b.json +0 -0
  61. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/qwen2.5-coder_7b.json +0 -0
  62. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/qwen3.5_2b.json +0 -0
  63. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/qwen3_8b.json +0 -0
  64. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/scoring_input.json +0 -0
  65. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_task_results/summary.json +0 -0
  66. {squackit-0.7.0 → squackit-0.8.0}/scripts/hard_tasks.py +0 -0
  67. {squackit-0.7.0 → squackit-0.8.0}/scripts/harvest_grid.py +0 -0
  68. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_eval.py +0 -0
  69. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary.json +0 -0
  70. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary_deepseek_plain.json +0 -0
  71. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary_distilled_xml.json +0 -0
  72. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary_editor_3b_7b.json +0 -0
  73. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary_fulldocs.json +0 -0
  74. {squackit-0.7.0 → squackit-0.8.0}/scripts/iterative_results/summary_plaintext_3b7b.json +0 -0
  75. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/codegemma_2b.json +0 -0
  76. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/codegemma_7b.json +0 -0
  77. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/deepseek-coder-v2_16b.json +0 -0
  78. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/deepseek-llm_7b.json +0 -0
  79. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/gemma_latest.json +0 -0
  80. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/glm4_9b.json +0 -0
  81. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/granite-code_3b.json +0 -0
  82. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/granite3.1-dense_2b.json +0 -0
  83. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/granite3.3_2b.json +0 -0
  84. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/llama2-uncensored_7b.json +0 -0
  85. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/llama3.2-vision_11b.json +0 -0
  86. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/llama3.2_1b.json +0 -0
  87. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/llama3.2_latest.json +0 -0
  88. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/phi3_latest.json +0 -0
  89. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/phi4-mini_latest.json +0 -0
  90. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5-coder_0.5b.json +0 -0
  91. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5-coder_1.5b.json +0 -0
  92. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5-coder_3b.json +0 -0
  93. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5-coder_7b.json +0 -0
  94. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5_1.5b.json +0 -0
  95. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5_3b.json +0 -0
  96. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2.5_7b.json +0 -0
  97. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen2_latest.json +0 -0
  98. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3.5_0.8b.json +0 -0
  99. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3.5_2b.json +0 -0
  100. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3.5_4b.json +0 -0
  101. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3.5_9b.json +0 -0
  102. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3_0.6b.json +0 -0
  103. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/qwen3_8b.json +0 -0
  104. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/smollm2_latest.json +0 -0
  105. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_card_results/summary.json +0 -0
  106. {squackit-0.7.0 → squackit-0.8.0}/scripts/model_cards.py +0 -0
  107. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/deepseek-coder-v2_16b.json +0 -0
  108. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/qwen2.5-coder_3b.json +0 -0
  109. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/qwen2.5-coder_7b.json +0 -0
  110. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/qwen3.5_2b.json +0 -0
  111. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/qwen3_8b.json +0 -0
  112. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_results/summary.json +0 -0
  113. {squackit-0.7.0 → squackit-0.8.0}/scripts/multiline_tasks.py +0 -0
  114. {squackit-0.7.0 → squackit-0.8.0}/scripts/qm_retrieval_eval.py +0 -0
  115. {squackit-0.7.0 → squackit-0.8.0}/scripts/quartermaster_eval.py +0 -0
  116. {squackit-0.7.0 → squackit-0.8.0}/scripts/quartermaster_results/retrieval_summary.json +0 -0
  117. {squackit-0.7.0 → squackit-0.8.0}/scripts/quartermaster_results/summary.json +0 -0
  118. {squackit-0.7.0 → squackit-0.8.0}/scripts/selector_eval.py +0 -0
  119. {squackit-0.7.0 → squackit-0.8.0}/scripts/selector_results/summary.json +0 -0
  120. {squackit-0.7.0 → squackit-0.8.0}/squackit/__init__.py +0 -0
  121. {squackit-0.7.0 → squackit-0.8.0}/squackit/__main__.py +0 -0
  122. {squackit-0.7.0 → squackit-0.8.0}/squackit/cli.py +0 -0
  123. {squackit-0.7.0 → squackit-0.8.0}/squackit/data/examples.json +0 -0
  124. {squackit-0.7.0 → squackit-0.8.0}/squackit/db.py +0 -0
  125. {squackit-0.7.0 → squackit-0.8.0}/squackit/defaults.py +0 -0
  126. {squackit-0.7.0 → squackit-0.8.0}/squackit/formatting.py +0 -0
  127. {squackit-0.7.0 → squackit-0.8.0}/squackit/lackpy_integration.py +0 -0
  128. {squackit-0.7.0 → squackit-0.8.0}/squackit/prompts.py +0 -0
  129. {squackit-0.7.0 → squackit-0.8.0}/squackit/runtime.py +0 -0
  130. {squackit-0.7.0 → squackit-0.8.0}/squackit/session.py +0 -0
  131. {squackit-0.7.0 → squackit-0.8.0}/squackit/tool_config.py +0 -0
  132. {squackit-0.7.0 → squackit-0.8.0}/tests/test_cli_tools.py +0 -0
  133. {squackit-0.7.0 → squackit-0.8.0}/tests/test_formatting_json.py +0 -0
  134. {squackit-0.7.0 → squackit-0.8.0}/tests/test_fts_root.py +0 -0
  135. {squackit-0.7.0 → squackit-0.8.0}/tests/test_prompts.py +0 -0
  136. {squackit-0.7.0 → squackit-0.8.0}/tests/test_resources.py +0 -0
  137. {squackit-0.7.0 → squackit-0.8.0}/tests/test_runtime_config.py +0 -0
  138. {squackit-0.7.0 → squackit-0.8.0}/tests/test_session.py +0 -0
  139. {squackit-0.7.0 → squackit-0.8.0}/tests/test_smoke.py +0 -0
  140. {squackit-0.7.0 → squackit-0.8.0}/tests/test_tool_config.py +0 -0
@@ -15,3 +15,7 @@ site/
15
15
  .kibitzer/
16
16
  scripts/example_grid_out/
17
17
  .lackpy/
18
+
19
+ # pluckit AST cache (created in the working repo when the cache is enabled)
20
+ .pluckit.duckdb
21
+ .pluckit.duckdb.wal
@@ -0,0 +1,127 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0
4
+
5
+ ### Added — source text from `find`, and a cache instead of re-parsing
6
+ `find` returns source text via a caller-chosen peek extent, and the AST cache
7
+ is enabled so repeated calls query a materialized table rather than re-parsing
8
+ the tree every time. Selection is delegated to sitting_duck's `ast_select`
9
+ rather than reimplemented here.
10
+
11
+ Cached tables now also carry `start_column` / `end_column`. These were not
12
+ zeroed before — they were *absent*, since `read_ast` only adds them under
13
+ `source := 'full'`. On minified input every node reports `start_line = 1`, so
14
+ character offsets are the only way to isolate a node: peek says how much text a
15
+ node carries, the columns say where it is.
16
+
17
+ ### Fixed — `investigate` preferred substrings over exact names
18
+ `investigate(name)` resolved definitions with a LIKE substring
19
+ (`name_pattern='%name%'`), so a deep-dive on `ensure_loaded` dragged in
20
+ `_ensure_loaded` — including vendored copies under `.venv`. Because the Source
21
+ and Calls sections key off `defs[0]`, the briefing could describe the **wrong
22
+ function** entirely. Exact matches now win.
23
+
24
+ ### Changed — requires `ast-pluckit>=0.15,<0.16`
25
+ This package calls `Plucker(..., cache=True, peek=...)`. That keyword arrived in
26
+ pluckit 0.15.0; the 0.14.0 the previous constraint allowed does not have it, so
27
+ a normal install resolved a pluckit whose every tool call raised `TypeError`.
28
+ The suite had hidden this by running with `PYTHONPATH` pointed at a checkout;
29
+ it is now verified against the published wheel.
30
+
31
+ ### Fixed — tests that need a fledgling checkout now skip
32
+ Six tests read repo-shaped content (a `docs/` tree, `fledgling/pro/__main__.py`)
33
+ that only a fledgling source checkout has. Under the installed-package layout
34
+ they failed on assertions about *content* — reading as bugs in squackit's own
35
+ defaults inference — instead of saying what was missing. They now skip, naming
36
+ `FLEDGLING_REPO_PATH`.
37
+
38
+ ## 0.7.1
39
+
40
+ ### Added — server consumes default-limit knobs
41
+ `config()`'s `max_results_default` / `complexity_max_results_default` session
42
+ knobs are now honored by the server's tool truncation, so the in-memory limits
43
+ set via `config()` actually take effect.
44
+
45
+ ### Changed — ast-pluckit 0.14 selector delegation
46
+ Consume ast-pluckit 0.14.0, which delegates CSS-selector matching to
47
+ sitting_duck's `ast_select` (fixes `:has(.call#name)` over-match, sitting_duck
48
+ #72 / squackit #8); pinned `ast-pluckit>=0.14,<0.15` with a regression test.
49
+
50
+ ### Added — AST tools de-vendor the source glob (fledgling #47)
51
+ `find` / `find_names` / `view` / `complexity` now exclude submodule, build,
52
+ cache, and checked-in third-party trees from a whole-repo glob, so they focus on
53
+ the project's own code instead of drowning in vendored deps. On a DuckDB
54
+ extension with `duckdb/` + `rdkit/` git submodules, `find_names('**/*.cpp', '.function')`
55
+ went from **35k+ names parsed in ~9 s to 164 in ~2 s** (~660× less work at the
56
+ parse layer).
57
+
58
+ - Reuses fledgling's single-source-of-truth ignore policy (`_is_vendored_path`
59
+ denylist + `_submodule_prefixes` git-awareness, new in fledgling 0.12) — the
60
+ filtered file set is handed to sitting_duck as an explicit `read_ast` list,
61
+ which parses identically to a glob (verified same results), so only *which*
62
+ files are parsed changes, not how selectors match.
63
+ - The repo root for submodule exclusion is derived from the **source glob**
64
+ (walking up to the nearest `.gitmodules`/`.git`), not the server's cwd, so it
65
+ works when an agent queries a different project by absolute path.
66
+ - Explicit targets are honored: a single-file source, a DuckDB table name, or a
67
+ glob aimed *into* a vendored/submodule tree (e.g. `rdkit/**/*.cpp`) is passed
68
+ through unfiltered rather than filtered to zero.
69
+
70
+ ### Changed
71
+ - Require `fledgling-mcp>=0.12` for the `_is_vendored_path` / `_submodule_prefixes`
72
+ ignore-policy macros.
73
+
74
+ ## 0.6.0
75
+
76
+ ### Added — `investigate(path=)` for cross-project scoping
77
+ `investigate(name, path=)` accepts an explicit `path` argument and scopes the
78
+ symbol-lookup to that path's `scoped_code_pattern`. Without `path=`, defaults
79
+ to the process `cwd`-scoped pattern (was: the global registry pattern, which
80
+ substring-matched across every indexed project — e.g. `investigate("main")`
81
+ returned hits from vendored JS in unrelated repos). Pre-existing
82
+ `file_pattern=` overrides still work.
83
+
84
+ ### Added — per-root FTS search
85
+ `search_*` tools now accept `root=<dir>` to index + search any repo on the
86
+ fly. Indexes are LRU-cached per root for the session, so repeated queries
87
+ in the same root stay cheap.
88
+
89
+ ### Fixed — agent-ergonomics findings
90
+ - FTS now fails loud on index errors instead of silently returning empty.
91
+ - `investigate` no longer mislabels callers in cross-file results.
92
+ - Selector docs verified end-to-end against the running tool surface.
93
+ - `lackpy` 0.12 dropped top-level re-exports; switched to submodule imports
94
+ to keep import-time light.
95
+
96
+ ### Docs
97
+ - `:has(.call#NAME)` works now that pluckit delegates selector compilation
98
+ to sitting_duck (was a notable known-bug in 0.5.0).
99
+ - Tool count corrected in README (~20 was wrong; actually ~35 / ~40 full).
100
+ - Receiver-qualified call selectors (#4) marked DONE.
101
+ - Response-type improvement proposals collected in `docs/proposals/`.
102
+
103
+ ## 0.5.0
104
+
105
+ ### Changed — migrate off pluckit/fledgling private internals
106
+ squackit no longer reaches into private connection attributes; it now uses the
107
+ public, SemVer'd contract from fledgling 0.10 and pluckit 0.13:
108
+
109
+ - `con._con` → `con.con`; `con._tools` → `con.tools` (`server.py`, `cli.py`).
110
+ - The lazy FTS rebuild (which poked `con._con` + a private `_fts_built` flag)
111
+ now delegates to fledgling's `Connection.ensure_fts()`. squackit keeps only
112
+ the `_FTS_MACROS` gating (which tools need FTS).
113
+ - `plucker._registry.pluckins` → `plucker.pluckins`; `Chain._MUTATION_OPS`
114
+ → `Chain.MUTATION_OPS` (`tools.py`).
115
+
116
+ ### Dependencies — declare what we actually use, with compatible ranges
117
+ - `ast-pluckit>=0.13.0,<0.14` (was `>=0.9.0`) — needs the public `pluckins`
118
+ accessor + `pluckins.search`/`viewer`.
119
+ - `fledgling-mcp>=0.10.0,<0.11` — **newly declared.** squackit imports
120
+ `fledgling.tools.ToolInfo` and needs the `Connection.con/.tools/.ensure_fts`
121
+ contract; declaring it makes `Plucker.connection` always a `fledgling.Connection`
122
+ (ending the bare-duckdb surprise that caused the `'_con' missing` failures).
123
+
124
+ ### Net
125
+ The brittle, undocumented, conditionally-present coupling is gone: a grep for
126
+ private reach-ins (`con._con`, `con._tools`, `_registry`, `_MUTATION_OPS`,
127
+ `_fts_built`) returns zero. The suite now composes by a versioned public API.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: squackit
3
- Version: 0.7.0
3
+ Version: 0.8.0
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.16,>=0.15
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.8.0"
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.15,<0.16",
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,136 @@ 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
 
15
- def _make_plucker(source: str):
16
- """Create a Plucker with AstViewer and Search for tool execution."""
17
+ #: How much source text ``read_ast`` puts in the ``peek`` column when squackit
18
+ #: materializes its own table. ``'smart'`` matches what pluckit's cache uses by
19
+ #: default, so a query returns the same peek whether or not de-vendoring kicked in.
20
+ DEFAULT_PEEK = "smart"
21
+
22
+
23
+ def _peek_arg(peek: str) -> str:
24
+ """Render a ``peek`` value as a ``read_ast`` named argument.
25
+
26
+ Accepts sitting_duck's vocabulary: ``'none'`` (suppress the text),
27
+ ``'smart'`` (short bounded preview), ``'full'`` (whole node), or an integer
28
+ size. ``+schema`` is appended so the column stays in the projection even for
29
+ ``'none'`` — squackit names ``peek`` in its compact columns, and a column
30
+ that disappears breaks the projection instead of merely emptying it.
31
+ """
32
+ value = str(peek).strip() or DEFAULT_PEEK
33
+ return f"peek := '{value}+schema'" if "+" not in value else f"peek := '{value}'"
34
+
35
+
36
+ def _filtered_source(plucker, source: str, peek: str = DEFAULT_PEEK) -> str:
37
+ """De-vendor a source glob before it reaches pluckit's AST engine.
38
+
39
+ A bare ``**/*`` glob over a repo with git submodules or checked-in
40
+ third-party trees (googletest, vendored deps) makes the AST tools parse
41
+ thousands of files that aren't the project's own code — the ``find`` /
42
+ ``view`` / ``complexity`` flood. We reuse fledgling's single-source-of-truth
43
+ ignore policy (``_is_vendored_path`` denylist + ``_submodule_prefixes``
44
+ git-awareness) to expand the glob, drop the vendored files, and hand pluckit
45
+ an explicit ``read_ast`` table of just the survivors via its existing
46
+ table-name source path. sitting_duck parses a list of paths exactly as it
47
+ parses a glob (verified identical results), so this changes only *which*
48
+ files are parsed, not how selectors match.
49
+
50
+ Returns the original ``source`` unchanged — so pluckit handles it directly —
51
+ when filtering doesn't apply or would be wrong:
52
+ * not a glob (a single file path or a DuckDB table/view name) — an
53
+ explicit target, nothing to de-flood;
54
+ * the glob expands to <= 1 file;
55
+ * filtering removes *everything* — the caller deliberately aimed at a
56
+ vendored/submodule tree (e.g. ``rdkit/**/*.cpp``), so honor it;
57
+ * filtering removes *nothing* — passing the glob through is cheaper and
58
+ gives identical results.
59
+ Otherwise returns the name of a TEMP table holding the filtered AST.
60
+
61
+ Degrades gracefully: if the fledgling ignore-policy macros can't run for any
62
+ reason (not loaded, SQL error), we fall back to the raw ``source`` — the
63
+ caller gets the historical unfiltered (flooded) behavior rather than a dead
64
+ tool. This is a de-flooding optimization, not a correctness gate, so a raw
65
+ glob is a safe answer; never let it take down find/view/complexity.
66
+ """
67
+ if "*" not in source:
68
+ return source
69
+
70
+ try:
71
+ db = plucker._ctx.db
72
+ resolved = (
73
+ source if os.path.isabs(source)
74
+ else os.path.join(plucker._ctx.repo, source)
75
+ )
76
+ # Submodule exclusion reads <root>/.gitmodules, so root must be the repo
77
+ # that actually contains the globbed files — NOT the plucker's cwd, which
78
+ # may be a different project entirely. Derive it from the glob's literal
79
+ # prefix (the path before the first wildcard) and walk up to the nearest
80
+ # .gitmodules / .git. The denylist is absolute-pattern based, no root needed.
81
+ prefix = resolved.split("*", 1)[0]
82
+ root = prefix if prefix.endswith("/") else os.path.dirname(prefix)
83
+ while root and root != "/":
84
+ if os.path.exists(os.path.join(root, ".gitmodules")) or os.path.exists(
85
+ os.path.join(root, ".git")
86
+ ):
87
+ break
88
+ root = os.path.dirname(root)
89
+ rq = resolved.replace("'", "''")
90
+ rootq = root.rstrip("/").replace("'", "''")
91
+
92
+ total = db.sql(f"SELECT count(*) FROM glob('{rq}')").fetchone()[0]
93
+ if total <= 1:
94
+ return source
95
+
96
+ kept = [
97
+ r[0]
98
+ for r in db.sql(
99
+ f"SELECT file FROM glob('{rq}') "
100
+ f"WHERE NOT _is_vendored_path(file) "
101
+ f"AND NOT EXISTS (SELECT 1 FROM _submodule_prefixes('{rootq}') s "
102
+ f"WHERE file LIKE s.prefix || '%') "
103
+ f"ORDER BY file"
104
+ ).fetchall()
105
+ ]
106
+ if not kept or len(kept) == total:
107
+ return source
108
+
109
+ lit = "[" + ", ".join("'" + f.replace("'", "''") + "'" for f in kept) + "]"
110
+ db.execute(
111
+ "CREATE OR REPLACE TEMP TABLE _squackit_src AS "
112
+ f"SELECT * FROM read_ast({lit}, {_peek_arg(peek)})"
113
+ )
114
+ return "_squackit_src"
115
+ except Exception:
116
+ # Ignore-policy macros unavailable / SQL error — fall back to the raw
117
+ # glob. Unfiltered (flooded) results beat a broken tool.
118
+ return source
119
+
120
+
121
+ def _make_plucker(source: str, peek: str = DEFAULT_PEEK):
122
+ """Create a Plucker with AstViewer and Search for tool execution.
123
+
124
+ The source glob is de-vendored first (see :func:`_filtered_source`) so the
125
+ AST tools focus on the project's own code instead of drowning in submodules,
126
+ build output, and checked-in third-party trees.
127
+ """
17
128
  from pluckit import Plucker
18
129
  from pluckit.pluckins.search import Search
19
130
  from pluckit.pluckins.viewer import AstViewer
20
- return Plucker(code=source, plugins=[AstViewer, Search])
131
+ # cache=True is what makes peek reachable. pluckit resolves a source three
132
+ # ways: a table name goes straight to ``ast_select_from`` (peek comes from
133
+ # the materialized table), a cached source is materialized then read the
134
+ # same way, and everything else falls through to ``ast_select`` — which
135
+ # parses with ``peek := 'none'`` and can only ever return NULL. Enabling the
136
+ # cache moves the un-filtered path onto ``ast_select_from`` too, so both
137
+ # paths return source text. It also stops re-parsing on every call.
138
+ p = Plucker(plugins=[AstViewer, Search], cache=True, peek=peek)
139
+ p._code_source = _filtered_source(p, source, peek)
140
+ return p
21
141
 
22
142
 
23
143
  def view_executor(*, source: str, selector: str):
@@ -26,9 +146,9 @@ def view_executor(*, source: str, selector: str):
26
146
  return p.view(selector)
27
147
 
28
148
 
29
- def find_executor(*, source: str, selector: str):
149
+ def find_executor(*, source: str, selector: str, peek: str = DEFAULT_PEEK):
30
150
  """Execute a find query, returning matched AST nodes as a relation."""
31
- p = _make_plucker(source)
151
+ p = _make_plucker(source, peek)
32
152
  return p.find(selector).relation
33
153
 
34
154
 
@@ -151,7 +151,13 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
151
151
  # `config(set={"active_root": X})` instead of passing path= per call.
152
152
  file_pattern = defaults.scoped_code_pattern(resolve_scope_path(path))
153
153
 
154
- # 1. Find definitions matching the name
154
+ # 1. Find definitions matching the name. Fetch substring candidates, then
155
+ # prefer EXACT name matches: a deep-dive on `ensure_loaded` must not drag
156
+ # in `_ensure_loaded` (or `main` -> `remain`). The substring pollutes the
157
+ # Definition table AND — because Source/Calls key off defs[0] — can make
158
+ # the whole briefing describe the wrong symbol. Fall back to the full
159
+ # substring set only when nothing matches exactly, preserving the
160
+ # forgiving lookup for partial names / typos.
155
161
  try:
156
162
  rel = con.find_definitions(
157
163
  file_pattern=file_pattern, name_pattern=f"%{name}%",
@@ -162,6 +168,12 @@ def investigate(con, defaults, name, file_pattern=None, path=None):
162
168
  defs = []
163
169
  def_cols = []
164
170
 
171
+ if defs and "name" in def_cols:
172
+ _n_idx = def_cols.index("name")
173
+ _exact = [d for d in defs if d[_n_idx] == name]
174
+ if _exact:
175
+ defs = _exact
176
+
165
177
  if not defs:
166
178
  return f"No definition found for '{name}'. Try a broader pattern or check spelling."
167
179
 
@@ -67,6 +67,38 @@ def __getattr__(name):
67
67
  raise AttributeError(f"module 'conftest' has no attribute {name!r}")
68
68
 
69
69
 
70
+ def _fledgling_source_available() -> bool:
71
+ """True when the discovered root is a fledgling *source checkout*.
72
+
73
+ ``_discover_fledgling_repo`` accepts an installed-package layout (a wheel
74
+ that bundles ``sql/`` inside the package) because that is enough for the
75
+ macros. It is not enough for tests that read repo-shaped content — a
76
+ ``docs/`` tree, or module files like ``fledgling/pro/__main__.py`` — which
77
+ a wheel does not lay out the same way.
78
+
79
+ Without this check those tests fail on an assertion about the *content*
80
+ ("expected doc_pattern to start with docs/"), which points at squackit's
81
+ own defaults logic rather than at the missing checkout, and sends anyone
82
+ debugging it in the wrong direction.
83
+ """
84
+ try:
85
+ root = _get_project_root()
86
+ except RuntimeError:
87
+ return False
88
+ return os.path.isdir(os.path.join(root, "docs")) and os.path.isfile(
89
+ os.path.join(root, "fledgling", "pro", "__main__.py")
90
+ )
91
+
92
+
93
+ requires_fledgling_source = pytest.mark.skipif(
94
+ not _fledgling_source_available(),
95
+ reason=(
96
+ "needs a fledgling source checkout (found an installed-package "
97
+ "layout). Set FLEDGLING_REPO_PATH to a fledgling clone."
98
+ ),
99
+ )
100
+
101
+
70
102
  def load_sql(con, filename):
71
103
  """Load a SQL macro file into a DuckDB connection."""
72
104
  path = os.path.join(_get_project_root(), "sql", filename)
@@ -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
@@ -6,7 +6,7 @@ from dataclasses import fields
6
6
 
7
7
  from squackit.db import create_connection
8
8
  from squackit.defaults import ProjectDefaults, TOOL_DEFAULTS, apply_defaults, load_config, infer_defaults
9
- from conftest import PROJECT_ROOT
9
+ from conftest import PROJECT_ROOT, requires_fledgling_source
10
10
 
11
11
 
12
12
  class TestProjectDefaults:
@@ -194,6 +194,7 @@ class TestInferDefaults:
194
194
  defaults = infer_defaults(con, root=str(PROJECT_ROOT))
195
195
  assert "Python" in defaults.languages
196
196
 
197
+ @requires_fledgling_source
197
198
  def test_doc_pattern_finds_docs_dir(self, con):
198
199
  """This repo has a docs/ directory."""
199
200
  defaults = infer_defaults(con, root=str(PROJECT_ROOT))
@@ -242,6 +243,7 @@ class TestServerIntegration:
242
243
  assert hasattr(server, "_defaults")
243
244
  assert isinstance(server._defaults, ProjectDefaults)
244
245
 
246
+ @requires_fledgling_source
245
247
  def test_server_defaults_inferred(self, server):
246
248
  """Defaults reflect this project (Python, docs/)."""
247
249
  assert "py" in server._defaults.code_pattern