witan-code 0.21.0__tar.gz → 0.23.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 (110) hide show
  1. {witan_code-0.21.0 → witan_code-0.23.0}/CHANGELOG.md +101 -0
  2. {witan_code-0.21.0 → witan_code-0.23.0}/PKG-INFO +16 -7
  3. {witan_code-0.21.0 → witan_code-0.23.0}/README.md +12 -3
  4. {witan_code-0.21.0 → witan_code-0.23.0}/conftest.py +1 -1
  5. {witan_code-0.21.0 → witan_code-0.23.0}/docs/EDGE_PRECISION_TIERS.md +8 -6
  6. {witan_code-0.21.0 → witan_code-0.23.0}/docs/STAGE2_STITCHING.md +9 -4
  7. {witan_code-0.21.0 → witan_code-0.23.0}/pyproject.toml +15 -5
  8. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_branches.py +1 -2
  9. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_bridge.py +163 -59
  10. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_cli_hooks.py +32 -3
  11. witan_code-0.23.0/tests/test_cli_output_format.py +210 -0
  12. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_cli_selected_target.py +0 -3
  13. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_config_targets.py +0 -1
  14. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_context.py +49 -0
  15. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_edges.py +1 -2
  16. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_github_app.py +2 -1
  17. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_hooks.py +6 -4
  18. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_index_failure_diagnostics.py +22 -14
  19. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_indexer_scope.py +1 -1
  20. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_ingest.py +1 -2
  21. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_remote_store.py +7 -3
  22. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_repo.py +0 -1
  23. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_setup.py +90 -2
  24. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_store.py +4 -4
  25. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_views.py +0 -1
  26. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_visualize.py +4 -1
  27. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/bridge.py +10 -6
  28. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/bridge_extractors.py +71 -34
  29. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/cli.py +85 -37
  30. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/config.py +1 -0
  31. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/context.py +64 -8
  32. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/extensions/pi/codegraph.ts +28 -7
  33. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/github_app.py +2 -2
  34. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/graph.py +2 -2
  35. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/hooks.py +1 -1
  36. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/identity.py +1 -1
  37. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/indexer.py +14 -12
  38. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/ingest.py +2 -2
  39. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/remote/oidc.py +2 -1
  40. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/remote/proxy.py +1 -1
  41. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/remote/store.py +8 -7
  42. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/server.py +8 -10
  43. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/setup.py +15 -3
  44. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/skills/witan-code/SKILL.md +29 -4
  45. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/store.py +7 -7
  46. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/visualize.py +6 -3
  47. witan_code-0.21.0/tests/test_cli_output_format.py +0 -95
  48. {witan_code-0.21.0 → witan_code-0.23.0}/.gitignore +0 -0
  49. {witan_code-0.21.0 → witan_code-0.23.0}/changelog.d/README.md +0 -0
  50. {witan_code-0.21.0 → witan_code-0.23.0}/config/claude.json +0 -0
  51. {witan_code-0.21.0 → witan_code-0.23.0}/config/copilot.json +0 -0
  52. {witan_code-0.21.0 → witan_code-0.23.0}/config/pi.json +0 -0
  53. {witan_code-0.21.0 → witan_code-0.23.0}/docs/BRANCH_INDEXING.md +0 -0
  54. {witan_code-0.21.0 → witan_code-0.23.0}/docs/CLI_REFERENCE.md +0 -0
  55. {witan_code-0.21.0 → witan_code-0.23.0}/docs/PACKAGE_MAP.md +0 -0
  56. {witan_code-0.21.0 → witan_code-0.23.0}/docs/SYMBOL_FORMAT.md +0 -0
  57. {witan_code-0.21.0 → witan_code-0.23.0}/docs/SYMBOL_TABLE.md +0 -0
  58. {witan_code-0.21.0 → witan_code-0.23.0}/docs/USER_GUIDE.md +0 -0
  59. {witan_code-0.21.0 → witan_code-0.23.0}/install.sh +0 -0
  60. {witan_code-0.21.0 → witan_code-0.23.0}/tests/__init__.py +0 -0
  61. {witan_code-0.21.0 → witan_code-0.23.0}/tests/conftest.py +0 -0
  62. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_batched_deletes.py +0 -0
  63. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_chunked_load.py +0 -0
  64. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_cli_dispatch.py +0 -0
  65. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_config.py +0 -0
  66. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_crossrepo.py +0 -0
  67. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_elicit.py +0 -0
  68. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_enrichment.py +0 -0
  69. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_graph.py +0 -0
  70. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_identity.py +0 -0
  71. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_index_reports_its_store.py +0 -0
  72. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_index_requires_a_repo_key.py +0 -0
  73. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_indexer.py +0 -0
  74. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_languages.py +0 -0
  75. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_maintenance.py +0 -0
  76. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_package_map.py +0 -0
  77. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_reaper.py +0 -0
  78. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  79. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_refusal_audit.py +0 -0
  80. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_remote_proxy.py +1 -1
  81. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_server.py +0 -0
  82. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_stitch.py +0 -0
  83. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_symbol_table.py +0 -0
  84. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_tasks.py +0 -0
  85. {witan_code-0.21.0 → witan_code-0.23.0}/tests/test_tool_schema.py +0 -0
  86. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/__init__.py +0 -0
  87. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/__main__.py +0 -0
  88. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/edges.py +0 -0
  89. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/elicit.py +0 -0
  90. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/maintenance.py +0 -0
  91. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/output.py +0 -0
  92. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/package_map.py +0 -0
  93. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries/bridge.gq +0 -0
  94. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries/code_mutations.gq +0 -0
  95. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries/code_read.gq +0 -0
  96. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries/delete.gq +0 -0
  97. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/bash.scm +0 -0
  98. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/hcl.scm +0 -0
  99. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/python.scm +0 -0
  100. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/sql.scm +0 -0
  101. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/typescript.scm +0 -0
  102. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/queries_ts/yaml.scm +0 -0
  103. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/reaper.py +0 -0
  104. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/remote/__init__.py +0 -0
  105. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/repo.py +0 -0
  106. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/schema/bridge-schema.pg +0 -0
  107. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/schema/code-schema.pg +0 -0
  108. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/selected_target.py +0 -0
  109. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/stitch.py +0 -0
  110. {witan_code-0.21.0 → witan_code-0.23.0}/witan_code/views.py +0 -0
@@ -8,6 +8,107 @@ a MINOR bump may include breaking changes).
8
8
 
9
9
  <!-- scriv-insert-here -->
10
10
 
11
+ ## [0.23.0] - 2026-09-28
12
+
13
+ ### Added
14
+
15
+ - **Python package providers.** A `pyproject.toml` whose directory holds a
16
+ `mitol/<pkg>/__init__.py` namespace child now provides `mitol.<pkg>`, so
17
+ `mitol.*` imports join to ol-django. Previously only `@mitodl/*` npm
18
+ packages had a provider extractor and no Python import could form an edge.
19
+ An existing graph gains these providers on ol-django's next full-repo
20
+ `witan-code index` (the provider files are not source files, so the
21
+ unchanged-file skip does not apply to them); `witan-code reindex` on it
22
+ works too.
23
+
24
+ - **Repo-level provider files that stop producing bindings lose their old
25
+ rows.** A `pyproject.toml`, `package.json`, Pulumi config or OpenAPI spec
26
+ that emitted nothing on this run was never purged, so on a run that could
27
+ not purge by the collected file list (e.g. indexing outside a repository
28
+ root) its previous bindings stayed. Every such file scanned is now purged
29
+ before its fresh bindings are written.
30
+
31
+ ### Changed
32
+
33
+ - Allow cyclopts 5 (`cyclopts>=4,<6`). Under cyclopts 5, `--help` shows a
34
+ value placeholder after each option (e.g. `--target STR`), and a usage error
35
+ such as an unknown option exits with code 2 instead of 1.
36
+
37
+ ### Fixed
38
+
39
+ - **`witan code` listings emit `rows: []` on an empty result under
40
+ `--output-format`.** `symbols`, `stitch`, `stitch --unresolved` and `repos`
41
+ each returned early with a Rich sentence on stdout before the format was
42
+ consulted, so an empty result was not JSON/TOML/YAML. `_render_table` now
43
+ takes the empty message and prints it in `txt` mode only. `branches` never
44
+ consulted the format at all and now dumps `{repo, views, error}` rows, with
45
+ `--prune`'s progress lines on stderr under a structured format; the `repos`
46
+ unreadable-store warning and `doctor`'s summary and error lines move to
47
+ stderr there too.
48
+ - **Table titles and cells are escaped for Rich.** A symbol, path or title
49
+ containing `[...]` was read as markup and lost from the rendered table.
50
+
51
+ - **Endpoint bindings form cross-repo edges again.** Every endpoint consumer
52
+ scored 0.0 because the extractor only matches relative paths and the
53
+ `relative_url` heuristic subtracted 0.5 from the 0.5 baseline, so
54
+ `code_repo_dependencies` and the other heuristic-tier views filtered every
55
+ one out at their 0.5 cutoff. The `relative_url` penalty, its unreachable
56
+ `explicit_hostname` boost, and the `generated_file` penalty are removed. A
57
+ generated client for a repo's own API is still suppressed by
58
+ `self_provided_key`. Stored confidences are computed at index time, so
59
+ existing graphs need `witan-code reindex` (not `index`, which skips
60
+ unchanged files) on the consumer repos before these edges appear.
61
+
62
+ ## [0.22.0] - 2026-09-25
63
+
64
+ ### Added
65
+
66
+ - **`witan-code inject-context --client pi`.** The status block told every
67
+ agent to run Claude's `ToolSearch` and then call `code_*` tools directly,
68
+ including under Pi, which has no `ToolSearch` and reaches MCP tools only
69
+ through pi-mcp-adapter's `mcp` proxy under a server-prefixed name. With
70
+ `--client pi` the block instead says to find the exact name with
71
+ `mcp({ search: "code_find_definition callers impact" })` and call it with
72
+ `mcp({ tool: "<name>", args: {...} })`, and points at `/skill:witan-code`.
73
+ The default (`--client claude`, what the Claude hook runs) is unchanged.
74
+
75
+ ### Changed
76
+
77
+ - **The Pi extension passes `--client pi`**, so a Pi install needs a
78
+ `witan-code` CLI from this release or later: an older one rejects the flag
79
+ and the extension, as with any failure, injects no block.
80
+ - **The `witan-code` skill's tool reference is client-neutral**, with short
81
+ notes on reaching the tools from Claude Code (`ToolSearch`) and Pi (the
82
+ `mcp` proxy).
83
+
84
+ - `witan-code setup --agent pi` (and `--agent all` when Pi is detected) now
85
+ warns when the pi-mcp-adapter Pi package is not declared in Pi's settings,
86
+ since Pi ignores the witan-code MCP entry in `~/.pi/agent/mcp.json` without
87
+ it. The warning comes from agent-config-kit's preflight via
88
+ `witan_core.cli.report_install`. The README and the `setup` help document
89
+ the prerequisite (`pi install npm:pi-mcp-adapter`).
90
+
91
+ - The `agent-config-kit` floor is raised from `>=0.7` to `>=0.10`, the first
92
+ release whose `InstallResult` carries the `prerequisites` that warning is
93
+ built from; on an older agent-config-kit the warning silently never
94
+ appeared.
95
+
96
+ - The `witan-core` floor is raised from `>=0.39` to `>=0.40`, the first
97
+ release whose `report_install` prints those `prerequisites`; on 0.39 the
98
+ server still runs but the warning is silently dropped.
99
+
100
+ ### Fixed
101
+
102
+ - **The Pi extension now waits as long for the code-graph status block as
103
+ Claude does.** `codegraph.ts`'s `before_agent_start` gave `witan-code
104
+ inject-context` 5s, against the 15s the Claude `UserPromptSubmit` hook gets,
105
+ so a cold store read that ran past 5s was killed before it could fill the
106
+ cache, and that prompt, and the next cold one, got no block. Both
107
+ now read one value (`INJECT_CONTEXT_TIMEOUT_SECONDS` in `witan_code.setup`,
108
+ `INJECT_CONTEXT_TIMEOUT_MS` in the extension), and a test fails if they, or
109
+ the `configs/pi/extensions` mirror, drift. Every other Pi handler stays
110
+ detached, and a timeout still degrades to no context.
111
+
11
112
  ## [0.21.0] - 2026-09-23
12
113
 
13
114
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: witan-code
3
- Version: 0.21.0
3
+ Version: 0.23.0
4
4
  Summary: witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)
5
5
  Project-URL: Homepage, https://github.com/mitodl/agent-kit/tree/main/mcp/servers/witan-code
6
6
  Project-URL: Repository, https://github.com/mitodl/agent-kit
@@ -10,8 +10,8 @@ License-Expression: BSD-3-Clause
10
10
  Keywords: agent,code-graph,coding-agent,mcp,static-analysis,tree-sitter
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.11
13
- Requires-Dist: agent-config-kit<1,>=0.7
14
- Requires-Dist: cyclopts<5,>=4
13
+ Requires-Dist: agent-config-kit<1,>=0.10
14
+ Requires-Dist: cyclopts<6,>=4
15
15
  Requires-Dist: fastmcp<5,>=4
16
16
  Requires-Dist: joserfc<2,>=1.0
17
17
  Requires-Dist: pydantic<3,>=2
@@ -26,7 +26,7 @@ Requires-Dist: tree-sitter-sql<0.4,>=0.3
26
26
  Requires-Dist: tree-sitter-typescript<0.24,>=0.23
27
27
  Requires-Dist: tree-sitter-yaml<0.8,>=0.7
28
28
  Requires-Dist: tree-sitter<0.26,>=0.25
29
- Requires-Dist: witan-core[cli,observability,remote,sentry]<1,>=0.39
29
+ Requires-Dist: witan-core[cli,observability,remote,sentry]<1,>=0.40
30
30
  Provides-Extra: tasks
31
31
  Requires-Dist: fastmcp-tasks<5,>=4; extra == 'tasks'
32
32
  Description-Content-Type: text/markdown
@@ -136,7 +136,7 @@ edges):
136
136
  |---|---|---|---|
137
137
  | `env_var` | Pulumi `<app>:env_vars` keys in `Pulumi.*.yaml` | `get_string("NAME",…)` / `os.environ` / `process.env.X` / `env("NAME")` | the `UPPER_SNAKE` name |
138
138
  | `endpoint` | drf-spectacular OpenAPI spec (`paths.<path>.<method>`) | `/api/…` path literals in TS (generated client / fetch) | normalized path (params → `{}`) |
139
- | `package` | a repo whose `package.json` name is `@mitodl/*` | `from mitol.*` / `include("mitol.*")` / `@mitodl/*` imports | the package name |
139
+ | `package` | a repo whose `package.json` name is `@mitodl/*`, or a `pyproject.toml` next to a `mitol/<pkg>/__init__.py` namespace child (provides `mitol.<pkg>`) | `from mitol.*` / `include("mitol.*")` / `@mitodl/*` imports | the package name |
140
140
  | `service` | `applications/<svc>/__main__.py` (repo / image / service name) | the deployed repo itself (key_norm = its canonical URI) | `repo:<uri>` / `image:…` / `name:…` |
141
141
 
142
142
  Generic env names (`DEBUG`, `PORT`, `SECRET_KEY`, …) are flagged `generic` and
@@ -146,7 +146,7 @@ every repo.
146
146
  The bridge is written in a **separate phase after the per-repo store write**, so
147
147
  the two stores' advisory write locks never nest (no deadlock) and a bridge
148
148
  failure never corrupts a per-repo store. A full-repo index runs the repo-level
149
- provider extractors (OpenAPI / Pulumi / service / `package.json`) and clears
149
+ provider extractors (OpenAPI / Pulumi / service / `package.json` / `pyproject.toml`) and clears
150
150
  bindings for files deleted from disk; all purging is per-file, so unchanged
151
151
  (skipped) files — and, for a narrow target like the reindex hook, sibling
152
152
  files — keep their bindings.
@@ -299,6 +299,12 @@ witan-code setup --dry-run # preview without writing
299
299
  witan-code setup --author "Jane Doe" # attribution (default: git config user.name)
300
300
  ```
301
301
 
302
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read only by the
303
+ third-party pi-mcp-adapter Pi package, since Pi core has no MCP support:
304
+ install it with `pi install npm:pi-mcp-adapter`, restart Pi, and confirm it
305
+ in `pi list`. `witan-code setup --agent pi` warns when it cannot find the
306
+ package declared in Pi's settings.
307
+
302
308
  If `witan` is *also* installed and witan-code is importable in that same
303
309
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
304
310
  `uvx` invocation), `witan setup` folds this same bundle in automatically —
@@ -715,7 +721,10 @@ A Pi equivalent of all four lives in one extension,
715
721
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
716
722
  `ToolSearch` call that makes the `code_*` tools callable when the harness
717
723
  delivers them deferred — followed by a `code_find_definition` →
718
- `code_callers`/`code_impact` call template.
724
+ `code_callers`/`code_impact` call template. `--client pi` (what the Pi
725
+ extension passes) swaps the `ToolSearch` step for pi-mcp-adapter's `mcp`
726
+ proxy: `mcp({ search: ... })` for the exact server-prefixed name, then
727
+ `mcp({ tool: ..., args: {...} })`. The default stays Claude's.
719
728
  Independent of `witan`'s own `inject-context` hook (no cross-package
720
729
  coupling) — register it alone for a witan-code-only install. Prints
721
730
  nothing when the repo has neither a store nor an index in flight.
@@ -103,7 +103,7 @@ edges):
103
103
  |---|---|---|---|
104
104
  | `env_var` | Pulumi `<app>:env_vars` keys in `Pulumi.*.yaml` | `get_string("NAME",…)` / `os.environ` / `process.env.X` / `env("NAME")` | the `UPPER_SNAKE` name |
105
105
  | `endpoint` | drf-spectacular OpenAPI spec (`paths.<path>.<method>`) | `/api/…` path literals in TS (generated client / fetch) | normalized path (params → `{}`) |
106
- | `package` | a repo whose `package.json` name is `@mitodl/*` | `from mitol.*` / `include("mitol.*")` / `@mitodl/*` imports | the package name |
106
+ | `package` | a repo whose `package.json` name is `@mitodl/*`, or a `pyproject.toml` next to a `mitol/<pkg>/__init__.py` namespace child (provides `mitol.<pkg>`) | `from mitol.*` / `include("mitol.*")` / `@mitodl/*` imports | the package name |
107
107
  | `service` | `applications/<svc>/__main__.py` (repo / image / service name) | the deployed repo itself (key_norm = its canonical URI) | `repo:<uri>` / `image:…` / `name:…` |
108
108
 
109
109
  Generic env names (`DEBUG`, `PORT`, `SECRET_KEY`, …) are flagged `generic` and
@@ -113,7 +113,7 @@ every repo.
113
113
  The bridge is written in a **separate phase after the per-repo store write**, so
114
114
  the two stores' advisory write locks never nest (no deadlock) and a bridge
115
115
  failure never corrupts a per-repo store. A full-repo index runs the repo-level
116
- provider extractors (OpenAPI / Pulumi / service / `package.json`) and clears
116
+ provider extractors (OpenAPI / Pulumi / service / `package.json` / `pyproject.toml`) and clears
117
117
  bindings for files deleted from disk; all purging is per-file, so unchanged
118
118
  (skipped) files — and, for a narrow target like the reindex hook, sibling
119
119
  files — keep their bindings.
@@ -266,6 +266,12 @@ witan-code setup --dry-run # preview without writing
266
266
  witan-code setup --author "Jane Doe" # attribution (default: git config user.name)
267
267
  ```
268
268
 
269
+ On Pi, the MCP server entry (`~/.pi/agent/mcp.json`) is read only by the
270
+ third-party pi-mcp-adapter Pi package, since Pi core has no MCP support:
271
+ install it with `pi install npm:pi-mcp-adapter`, restart Pi, and confirm it
272
+ in `pi list`. `witan-code setup --agent pi` warns when it cannot find the
273
+ package declared in Pi's settings.
274
+
269
275
  If `witan` is *also* installed and witan-code is importable in that same
270
276
  environment (e.g. via the `--with` in the `uv tool install`/MCP server's
271
277
  `uvx` invocation), `witan setup` folds this same bundle in automatically —
@@ -682,7 +688,10 @@ A Pi equivalent of all four lives in one extension,
682
688
  can tell "no cross-repo consumers" from "no cross-repo data"), and the
683
689
  `ToolSearch` call that makes the `code_*` tools callable when the harness
684
690
  delivers them deferred — followed by a `code_find_definition` →
685
- `code_callers`/`code_impact` call template.
691
+ `code_callers`/`code_impact` call template. `--client pi` (what the Pi
692
+ extension passes) swaps the `ToolSearch` step for pi-mcp-adapter's `mcp`
693
+ proxy: `mcp({ search: ... })` for the exact server-prefixed name, then
694
+ `mcp({ tool: ..., args: {...} })`. The default stays Claude's.
686
695
  Independent of `witan`'s own `inject-context` hook (no cross-package
687
696
  coupling) — register it alone for a witan-code-only install. Prints
688
697
  nothing when the repo has neither a store nor an index in flight.
@@ -23,4 +23,4 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
23
23
 
24
24
  # The import is the point: `testsupport.hermetic` redirects the environment at
25
25
  # module scope. The hook re-export is what lets this conftest report a leak.
26
- from testsupport.hermetic import pytest_sessionfinish # noqa: E402,F401
26
+ from testsupport.hermetic import pytest_sessionfinish # noqa: F401
@@ -35,12 +35,14 @@ up twice at two different trust levels.
35
35
 
36
36
  ```python
37
37
  TypedEdge(
38
- precision, # "precise" | "heuristic" | "fuzzy"
39
- consumer_repo, provider_repo,
40
- kind, key_norm,
41
- canonical_symbol, # consumer's symbol string; precise tier only, else None
42
- confidence, # 1.0 for precise; the heuristic tier's score otherwise
43
- evidence, # tuple[dict] of {repo, file, line} — see below
38
+ precision, # "precise" | "heuristic" | "fuzzy"
39
+ consumer_repo,
40
+ provider_repo,
41
+ kind,
42
+ key_norm,
43
+ canonical_symbol, # consumer's symbol string; precise tier only, else None
44
+ confidence, # 1.0 for precise; the heuristic tier's score otherwise
45
+ evidence, # tuple[dict] of {repo, file, line} — see below
44
46
  )
45
47
  ```
46
48
 
@@ -45,10 +45,15 @@ way a written edge could.
45
45
 
46
46
  ```python
47
47
  PreciseEdge(
48
- consumer_repo, consumer_symbol,
49
- provider_repo, provider_symbol,
50
- kind, scheme,
51
- match_count, preferred, ambiguous_version,
48
+ consumer_repo,
49
+ consumer_symbol,
50
+ provider_repo,
51
+ provider_symbol,
52
+ kind,
53
+ scheme,
54
+ match_count,
55
+ preferred,
56
+ ambiguous_version,
52
57
  )
53
58
  ```
54
59
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "witan-code"
3
- version = "0.21.0"
3
+ version = "0.23.0"
4
4
  description = "witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)"
5
5
  readme = "README.md"
6
6
  license = "BSD-3-Clause"
@@ -17,7 +17,11 @@ dependencies = [
17
17
  # Same blind spot as the witan-core floors below — the workspace resolves
18
18
  # agent-config-kit by path — except there is NO `check-core-floor`
19
19
  # equivalent for this dependency, so this line is the only guard.
20
- "agent-config-kit>=0.7,<1",
20
+ # >=0.10: `witan-code setup`'s install report promises a warning when Pi
21
+ # lacks pi-mcp-adapter, which comes from `InstallResult.prerequisites`,
22
+ # added in 0.10.0. witan_core's `report_install` reads it with getattr, so
23
+ # an older resolution does not fail — it just silently drops the warning.
24
+ "agent-config-kit>=0.10,<1",
21
25
  # `remote`: the ADR-0005 client stack (OIDC device grant + MCP-client
22
26
  # proxy) behind `witan-code login` and the read commands' remote mode.
23
27
  # Costs nothing extra in practice — fastmcp is already required here and
@@ -151,9 +155,15 @@ dependencies = [
151
155
  # code-graph writes parallel agents lose to each other. And
152
156
  # `ClusterGraphMissing` sets `log_safe_message`, which only 0.39 reads: on
153
157
  # 0.38 its text never reaches the `mcp.tool_call` log line.
154
- "witan-core[cli,remote,observability,sentry]>=0.39,<1",
158
+ #
159
+ # ★ >=0.40 IS A BEHAVIOUR FLOOR too: `witan-code setup --agent pi` promises
160
+ # a warning when Pi lacks pi-mcp-adapter, and
161
+ # `witan_core.cli.report_install` only prints `InstallResult.prerequisites`
162
+ # from 0.40.0. It reads the field with getattr, so 0.39 imports and runs —
163
+ # and silently drops the warning.
164
+ "witan-core[cli,remote,observability,sentry]>=0.40,<1",
155
165
  "fastmcp>=4,<5",
156
- "cyclopts>=4,<5",
166
+ "cyclopts>=4,<6",
157
167
  # [targets.<name>] override models (config.py) — also transitively pulled
158
168
  # in by fastmcp, declared explicitly since config.py imports it directly.
159
169
  "pydantic>=2,<3",
@@ -234,7 +244,7 @@ testpaths = ["tests"]
234
244
  packages = ["witan_code"]
235
245
 
236
246
  [tool.bumpversion]
237
- current_version = "0.21.0"
247
+ current_version = "0.23.0"
238
248
  allow_dirty = true
239
249
 
240
250
  [[tool.bumpversion.files]]
@@ -6,9 +6,8 @@ import subprocess
6
6
 
7
7
  import pytest
8
8
 
9
- from witan_core.omnigraph import OmnigraphConflict
10
-
11
9
  from witan_code import repo as repo_module
10
+ from witan_core.omnigraph import OmnigraphConflict
12
11
 
13
12
  from .conftest import SAMPLE, requires_stack
14
13
 
@@ -14,128 +14,104 @@ from witan_code.bridge_extractors import (
14
14
  _compute_file_confidence,
15
15
  adjust_confidence,
16
16
  extract_file_bindings,
17
+ extract_repo_bindings,
17
18
  )
18
19
 
19
20
  from .conftest import requires_stack
20
21
 
21
-
22
22
  # ── Confidence scoring unit tests ─────────────────────────────────
23
23
  # All tests operate on _compute_file_confidence directly; no omnigraph needed.
24
24
 
25
25
 
26
26
  def test_load_test_path_penalizes_score():
27
27
  """load_test_path penalty: file under a load_testing/ directory."""
28
- score = _compute_file_confidence(
29
- "load_testing/backend/client/v0/api.ts", "", "/api/v0/courses/"
30
- )
28
+ score = _compute_file_confidence("load_testing/backend/client/v0/api.ts", "")
31
29
  assert score < 0.5, f"load_test_path should penalize; got {score}"
32
30
 
33
31
 
34
32
  def test_k6_path_segment_penalizes_score():
35
33
  """load_test_path penalty applies to /k6/ path segments."""
36
- score = _compute_file_confidence("perf/k6/scenarios.ts", "", "/api/v0/courses/")
34
+ score = _compute_file_confidence("perf/k6/scenarios.ts", "")
37
35
  assert score < 0.5, f"k6 path segment should penalize; got {score}"
38
36
 
39
37
 
40
38
  def test_load_test_framework_import_penalizes():
41
39
  """load_test_framework penalty: file imports k6."""
42
40
  content = "import http from 'k6/http';\nimport { sleep } from 'k6';\n"
43
- score = _compute_file_confidence("client.ts", content, "/api/v0/courses/")
41
+ score = _compute_file_confidence("client.ts", content)
44
42
  assert score < 0.5, f"k6 framework import should penalize; got {score}"
45
43
 
46
44
 
47
45
  def test_load_test_framework_locust_penalizes():
48
46
  """load_test_framework penalty applies to locust imports."""
49
47
  content = "from 'locust' import HttpUser\n"
50
- score = _compute_file_confidence("test_load.ts", content, "/api/v0/things/")
48
+ score = _compute_file_confidence("test_load.ts", content)
51
49
  assert score < 0.5, f"locust import should penalize; got {score}"
52
50
 
53
51
 
54
52
  def test_load_test_framework_artillery_penalizes():
55
53
  """load_test_framework penalty applies to artillery imports."""
56
54
  content = "import stuff from 'artillery';\n"
57
- score = _compute_file_confidence("perf.ts", content, "/api/v0/things/")
55
+ score = _compute_file_confidence("perf.ts", content)
58
56
  assert score < 0.5, f"artillery import should penalize; got {score}"
59
57
 
60
58
 
61
- def test_relative_url_penalizes_score():
62
- """relative_url penalty: URL has no scheme."""
63
- score = _compute_file_confidence("src/client.ts", "", "/api/v0/profiles/")
64
- assert score < 0.5, f"relative URL should penalize; got {score}"
59
+ def test_plain_client_clears_the_edge_cutoff():
60
+ """A consumer with no signal either way sits at the cutoff, so it forms an edge.
65
61
 
66
-
67
- def test_generated_file_header_heavily_penalizes():
68
- """generated_file penalty: file with DO NOT EDIT header."""
69
- content = "// DO NOT EDIT — generated by openapi-generator\nexport const x = 1;"
70
- score = _compute_file_confidence("src/generated.ts", content, "/api/v0/courses/")
71
- assert score < 0.1, f"generated file header should heavily penalize; got {score}"
62
+ Every path `_TS_ENDPOINT` extracts is relative; penalizing that put every
63
+ endpoint consumer below `build_graph`'s 0.5 cutoff.
64
+ """
65
+ assert _compute_file_confidence("src/client.ts", "") == 0.5
72
66
 
73
67
 
74
- def test_generated_file_at_generated_penalizes():
75
- """generated_file penalty: file with @generated annotation."""
76
- content = "/* @generated */\nexport const x = 1;"
77
- score = _compute_file_confidence("src/gen.ts", content, "/api/v0/courses/")
78
- assert score < 0.1, f"@generated file should heavily penalize; got {score}"
68
+ def test_generated_client_is_not_penalized():
69
+ """A generated OpenAPI client is exact consumer evidence; self_provided_key
70
+ handles one generated for the repo's own API."""
71
+ content = "// DO NOT EDIT — generated by openapi-generator\nexport const x = 1;"
72
+ assert _compute_file_confidence("src/generated.ts", content) == 0.5
79
73
 
80
74
 
81
75
  def test_same_origin_helper_penalizes():
82
76
  """same_origin_helper penalty: relative import to a csrf/fetch wrapper."""
83
77
  content = "import { fetchJSONWithCSRF } from '../utils/csrf';\n"
84
- score = _compute_file_confidence("src/actions.ts", content, "/api/v0/profiles/")
78
+ score = _compute_file_confidence("src/actions.ts", content)
85
79
  assert score < 0.5, f"same-origin helper import should penalize; got {score}"
86
80
 
87
81
 
88
82
  def test_same_origin_relative_api_client_penalizes():
89
83
  """same_origin_helper penalty: relative import to an api-client file."""
90
84
  content = "import { apiFetch } from './api-client';\n"
91
- score = _compute_file_confidence("src/actions.ts", content, "/api/v0/courses/")
85
+ score = _compute_file_confidence("src/actions.ts", content)
92
86
  assert score < 0.5, f"relative api-client import should penalize; got {score}"
93
87
 
94
88
 
95
- def test_explicit_hostname_boosts_score():
96
- """explicit_hostname boost: URL has scheme + host (absolute URL)."""
97
- score = _compute_file_confidence(
98
- "src/client.ts", "", "https://api.example.com/api/v0/courses/"
99
- )
100
- assert score > 0.5, f"explicit hostname should boost; got {score}"
101
-
102
-
103
89
  def test_client_directory_boosts_score():
104
90
  """client_directory boost: file path contains /clients/."""
105
- score = _compute_file_confidence("src/clients/courses.ts", "", "/api/v0/courses/")
106
- assert score > 0.0, "client_directory should boost"
107
- # relative URL still penalizes but client_directory partially offsets
108
- baseline = _compute_file_confidence("src/other/courses.ts", "", "/api/v0/courses/")
91
+ score = _compute_file_confidence("src/clients/courses.ts", "")
92
+ baseline = _compute_file_confidence("src/other/courses.ts", "")
109
93
  assert score > baseline, (
110
94
  f"client dir path ({score}) should score higher than non-client ({baseline})"
111
95
  )
112
96
 
113
97
 
114
- def test_client_and_hostname_both_boost():
115
- """client_directory + explicit_hostname compound boost > 0.7."""
116
- score = _compute_file_confidence(
117
- "src/clients/courses.ts",
118
- "",
119
- "https://api.example.com/api/v0/courses/",
120
- )
121
- assert score > 0.7, f"client dir + hostname should push score > 0.7; got {score}"
122
-
123
-
124
98
  def test_score_clamped_to_zero():
125
99
  """Multiple penalties cannot push score below 0.0."""
126
- content = "// DO NOT EDIT — generated\nimport http from 'k6/http';\n"
127
- score = _compute_file_confidence("load_testing/k6/api.ts", content, "/api/v0/x/")
100
+ content = "import { f } from '../csrf';\nimport http from 'k6/http';\n"
101
+ score = _compute_file_confidence("load_testing/k6/api.ts", content)
128
102
  assert score == 0.0, f"overlapping penalties should clamp at 0.0; got {score}"
129
103
 
130
104
 
131
105
  def test_score_clamped_to_one():
132
106
  """Score cannot exceed 1.0 even with all boosts stacking."""
133
- score = _compute_file_confidence(
134
- "src/clients/integrations/service.ts",
135
- "",
136
- "https://api.example.com/api/v0/courses/",
107
+ b = _ep_consumer(confidence=0.9)
108
+ adjust_confidence(
109
+ b,
110
+ consumer_repo="https://github.com/test/repo-a",
111
+ provider_keys=frozenset(),
112
+ has_known_provider_package=True,
137
113
  )
138
- assert score <= 1.0, f"score must not exceed 1.0; got {score}"
114
+ assert b.confidence == 1.0, f"score must clamp at 1.0; got {b.confidence}"
139
115
 
140
116
 
141
117
  # ── Store-level adjust_confidence tests ───────────────────────────
@@ -312,6 +288,89 @@ def test_csrf_client_still_extracts_package_and_env():
312
288
  assert b.confidence == 1.0
313
289
 
314
290
 
291
+ OPENAPI_SPEC = """\
292
+ openapi: 3.0.3
293
+ paths:
294
+ /api/v1/courses/{id}/:
295
+ get: {}
296
+ """
297
+
298
+
299
+ def _rows(bindings, repo):
300
+ return [
301
+ {
302
+ "kind": b.kind,
303
+ "key_norm": b.key_norm,
304
+ "role": b.role,
305
+ "repo": repo,
306
+ "confidence": b.confidence,
307
+ }
308
+ for b in bindings
309
+ ]
310
+
311
+
312
+ def test_extracted_endpoint_and_package_bindings_form_edges(tmp_path):
313
+ """What the extractors actually emit must survive build_graph's cutoff.
314
+
315
+ Regression: every endpoint consumer scored 0.0 (relative_url penalty on
316
+ paths that are always relative), and no Python package provider was ever
317
+ emitted, so neither kind produced a single edge on the deployed graph.
318
+ """
319
+ from witan_code import visualize
320
+
321
+ consumer = "https://github.com/test/frontend"
322
+ provider = "https://github.com/test/backend"
323
+ lib = "https://github.com/test/ol-django"
324
+
325
+ backend = tmp_path / "backend"
326
+ (backend / "openapi" / "specs").mkdir(parents=True)
327
+ (backend / "openapi" / "specs" / "v1.yaml").write_text(OPENAPI_SPEC)
328
+ ol_django = tmp_path / "ol-django"
329
+ pkg = ol_django / "src" / "common" / "mitol" / "common"
330
+ pkg.mkdir(parents=True)
331
+ (pkg / "__init__.py").write_text("")
332
+ (ol_django / "src" / "common" / "pyproject.toml").write_text(
333
+ '[project]\nname = "mitol-django-common"\n'
334
+ )
335
+
336
+ rows = (
337
+ _rows(
338
+ extract_file_bindings(NORMAL_TS_CLIENT, "typescript", "src/client.ts"),
339
+ consumer,
340
+ )
341
+ + _rows(
342
+ extract_file_bindings(
343
+ "from mitol.common.utils import now_in_utc\n", "python", "app.py"
344
+ ),
345
+ consumer,
346
+ )
347
+ + _rows(extract_repo_bindings(backend, provider)[0], provider)
348
+ + _rows(extract_repo_bindings(ol_django, lib)[0], lib)
349
+ )
350
+ graph = visualize.build_graph(rows)
351
+
352
+ assert dict(graph.edges[(consumer, provider)].kinds) == {"endpoint": 1}
353
+ assert dict(graph.edges[(consumer, lib)].kinds) == {"package": 1}
354
+
355
+
356
+ def test_python_package_provider_only_for_namespace_children(tmp_path):
357
+ """An app repo's own top-level packages are not published providers."""
358
+ app = tmp_path / "app"
359
+ (app / "courses").mkdir(parents=True)
360
+ (app / "courses" / "__init__.py").write_text("")
361
+ (app / "pyproject.toml").write_text('[project]\nname = "app"\n')
362
+ assert extract_repo_bindings(app, "https://github.com/test/app") == (
363
+ [],
364
+ {"pyproject.toml"},
365
+ )
366
+
367
+ # A regular (non-namespace) `mitol` package is the app's own, not shared.
368
+ (app / "mitol" / "thing").mkdir(parents=True)
369
+ (app / "mitol" / "__init__.py").write_text("")
370
+ (app / "mitol" / "thing" / "__init__.py").write_text("")
371
+ assert extract_repo_bindings(app, "https://github.com/test/app")[0] == []
372
+
373
+
315
374
  # Repo A (mit-learn-like): a Django settings consumer + a NextJS endpoint/env
316
375
  # consumer.
317
376
  A_SETTINGS = """\
@@ -472,10 +531,9 @@ def test_known_provider_package_boost_sourced_from_stage1_symbol_table(
472
531
  re-derivation from raw InterfaceBinding rows — verify the confidence
473
532
  boost actually lands end-to-end through a real index + bridge write.
474
533
 
475
- D_CLIENT_TS's endpoint consumer has only the relative_url penalty
476
- (-0.5, no other signal fires): base 0.5 - 0.5 = 0.0. The co-located
477
- package import of the repo-c-provided package should then boost it by
478
- +0.3 to 0.3.
534
+ No file-level signal fires on D_CLIENT_TS's endpoint consumer, so it
535
+ starts at the 0.5 baseline. The co-located package import of the
536
+ repo-c-provided package should then boost it by +0.3 to 0.8.
479
537
  """
480
538
  from witan_code import config as cfg_mod
481
539
  from witan_code import indexer
@@ -513,7 +571,53 @@ def test_known_provider_package_boost_sourced_from_stage1_symbol_table(
513
571
  if r["repo"] == "https://github.com/test/repo-d" and r["kind"] == "endpoint"
514
572
  ]
515
573
  assert len(rows) == 1
516
- assert rows[0]["confidence"] == pytest.approx(0.3)
574
+ assert rows[0]["confidence"] == pytest.approx(0.8)
575
+
576
+
577
+ @requires_stack
578
+ def test_tier_b_source_that_stops_emitting_loses_its_rows(tmp_path, monkeypatch):
579
+ """A pyproject.toml whose mitol/<pkg>/ was deleted must not keep providing it.
580
+
581
+ The toml is unchanged and still on disk. On a run that cannot purge by the
582
+ collected file list (here: no repository root), nothing put it in the
583
+ purge set until Tier B started reporting the sources it scanned.
584
+ """
585
+ import shutil
586
+
587
+ from witan_code import config as cfg_mod
588
+ from witan_code import indexer
589
+ from witan_code import server as srv
590
+ from witan_code.graph import OmnigraphClient
591
+
592
+ monkeypatch.setenv("WITAN_CODE_DIR", str(tmp_path / "code"))
593
+ monkeypatch.setenv("WITAN_REPO", "https://github.com/test/ol-django")
594
+ cfg = cfg_mod.load()
595
+ monkeypatch.setattr(srv, "cfg", cfg)
596
+ srv._clients.clear()
597
+
598
+ repo = tmp_path / "ol-django"
599
+ pkg = repo / "mitol" / "common"
600
+ pkg.mkdir(parents=True)
601
+ (pkg / "__init__.py").write_text("")
602
+ (repo / "pyproject.toml").write_text('[project]\nname = "mitol-django-common"\n')
603
+
604
+ client = OmnigraphClient(
605
+ str(cfg_mod.bridge_store_path(cfg.code_dir)), cfg.queries_dir
606
+ )
607
+
608
+ def providers():
609
+ return [
610
+ r["key_norm"]
611
+ for r in client.read("bridge.gq", "all_bindings", {})
612
+ if r["kind"] == "package" and r["role"] == "provider"
613
+ ]
614
+
615
+ _index(srv, indexer, repo)
616
+ assert providers() == ["mitol.common"]
617
+
618
+ shutil.rmtree(pkg)
619
+ _index(srv, indexer, repo)
620
+ assert providers() == []
517
621
 
518
622
 
519
623
  @requires_stack