witan-code 0.22.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.
- {witan_code-0.22.0 → witan_code-0.23.0}/CHANGELOG.md +51 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/PKG-INFO +4 -4
- {witan_code-0.22.0 → witan_code-0.23.0}/README.md +2 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/conftest.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/EDGE_PRECISION_TIERS.md +8 -6
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/STAGE2_STITCHING.md +9 -4
- {witan_code-0.22.0 → witan_code-0.23.0}/pyproject.toml +3 -3
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_branches.py +1 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_bridge.py +163 -59
- witan_code-0.23.0/tests/test_cli_output_format.py +210 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_selected_target.py +0 -3
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_config_targets.py +0 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_edges.py +1 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_github_app.py +2 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_hooks.py +6 -4
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_failure_diagnostics.py +22 -14
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_indexer_scope.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_ingest.py +1 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_remote_store.py +7 -3
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_repo.py +0 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_setup.py +0 -3
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_store.py +4 -4
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_views.py +0 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_visualize.py +4 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/bridge.py +10 -6
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/bridge_extractors.py +71 -34
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/cli.py +70 -35
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/config.py +1 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/context.py +5 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/github_app.py +2 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/graph.py +2 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/hooks.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/identity.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/indexer.py +14 -12
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/ingest.py +2 -2
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/oidc.py +2 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/proxy.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/store.py +8 -7
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/server.py +8 -10
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/store.py +7 -7
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/visualize.py +6 -3
- witan_code-0.22.0/tests/test_cli_output_format.py +0 -95
- {witan_code-0.22.0 → witan_code-0.23.0}/.gitignore +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/changelog.d/README.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/config/claude.json +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/config/copilot.json +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/config/pi.json +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/BRANCH_INDEXING.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/CLI_REFERENCE.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/PACKAGE_MAP.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/SYMBOL_FORMAT.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/SYMBOL_TABLE.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/docs/USER_GUIDE.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/install.sh +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/__init__.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/conftest.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_batched_deletes.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_chunked_load.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_dispatch.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_hooks.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_config.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_context.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_crossrepo.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_elicit.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_enrichment.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_graph.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_identity.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_reports_its_store.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_requires_a_repo_key.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_indexer.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_languages.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_maintenance.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_package_map.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_reaper.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_refusal_audit.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_remote_proxy.py +1 -1
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_server.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_stitch.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_symbol_table.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_tasks.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_tool_schema.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/__init__.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/__main__.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/edges.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/elicit.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/extensions/pi/codegraph.ts +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/maintenance.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/output.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/package_map.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/bridge.gq +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/code_mutations.gq +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/code_read.gq +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/delete.gq +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/bash.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/hcl.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/python.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/sql.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/typescript.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/yaml.scm +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/reaper.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/__init__.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/repo.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/schema/bridge-schema.pg +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/schema/code-schema.pg +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/selected_target.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/setup.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/skills/witan-code/SKILL.md +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/stitch.py +0 -0
- {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/views.py +0 -0
|
@@ -8,6 +8,57 @@ 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
|
+
|
|
11
62
|
## [0.22.0] - 2026-09-25
|
|
12
63
|
|
|
13
64
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: witan-code
|
|
3
|
-
Version: 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
|
|
@@ -11,7 +11,7 @@ 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
13
|
Requires-Dist: agent-config-kit<1,>=0.10
|
|
14
|
-
Requires-Dist: cyclopts<
|
|
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
|
|
@@ -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
|
|
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.
|
|
@@ -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
|
|
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.
|
|
@@ -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:
|
|
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,
|
|
39
|
-
consumer_repo,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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"
|
|
@@ -163,7 +163,7 @@ dependencies = [
|
|
|
163
163
|
# and silently drops the warning.
|
|
164
164
|
"witan-core[cli,remote,observability,sentry]>=0.40,<1",
|
|
165
165
|
"fastmcp>=4,<5",
|
|
166
|
-
"cyclopts>=4,<
|
|
166
|
+
"cyclopts>=4,<6",
|
|
167
167
|
# [targets.<name>] override models (config.py) — also transitively pulled
|
|
168
168
|
# in by fastmcp, declared explicitly since config.py imports it directly.
|
|
169
169
|
"pydantic>=2,<3",
|
|
@@ -244,7 +244,7 @@ testpaths = ["tests"]
|
|
|
244
244
|
packages = ["witan_code"]
|
|
245
245
|
|
|
246
246
|
[tool.bumpversion]
|
|
247
|
-
current_version = "0.
|
|
247
|
+
current_version = "0.23.0"
|
|
248
248
|
allow_dirty = true
|
|
249
249
|
|
|
250
250
|
[[tool.bumpversion.files]]
|
|
@@ -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", ""
|
|
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
|
|
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
|
|
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
|
|
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
|
|
62
|
-
"""
|
|
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
|
-
|
|
68
|
-
"""
|
|
69
|
-
|
|
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
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
assert
|
|
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
|
|
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
|
|
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", ""
|
|
106
|
-
|
|
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 = "
|
|
127
|
-
score = _compute_file_confidence("load_testing/k6/api.ts", content
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"https://
|
|
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
|
|
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
|
|
476
|
-
|
|
477
|
-
|
|
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.
|
|
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
|