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.
Files changed (110) hide show
  1. {witan_code-0.22.0 → witan_code-0.23.0}/CHANGELOG.md +51 -0
  2. {witan_code-0.22.0 → witan_code-0.23.0}/PKG-INFO +4 -4
  3. {witan_code-0.22.0 → witan_code-0.23.0}/README.md +2 -2
  4. {witan_code-0.22.0 → witan_code-0.23.0}/conftest.py +1 -1
  5. {witan_code-0.22.0 → witan_code-0.23.0}/docs/EDGE_PRECISION_TIERS.md +8 -6
  6. {witan_code-0.22.0 → witan_code-0.23.0}/docs/STAGE2_STITCHING.md +9 -4
  7. {witan_code-0.22.0 → witan_code-0.23.0}/pyproject.toml +3 -3
  8. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_branches.py +1 -2
  9. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_bridge.py +163 -59
  10. witan_code-0.23.0/tests/test_cli_output_format.py +210 -0
  11. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_selected_target.py +0 -3
  12. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_config_targets.py +0 -1
  13. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_edges.py +1 -2
  14. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_github_app.py +2 -1
  15. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_hooks.py +6 -4
  16. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_failure_diagnostics.py +22 -14
  17. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_indexer_scope.py +1 -1
  18. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_ingest.py +1 -2
  19. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_remote_store.py +7 -3
  20. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_repo.py +0 -1
  21. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_setup.py +0 -3
  22. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_store.py +4 -4
  23. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_views.py +0 -1
  24. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_visualize.py +4 -1
  25. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/bridge.py +10 -6
  26. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/bridge_extractors.py +71 -34
  27. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/cli.py +70 -35
  28. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/config.py +1 -0
  29. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/context.py +5 -1
  30. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/github_app.py +2 -2
  31. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/graph.py +2 -2
  32. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/hooks.py +1 -1
  33. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/identity.py +1 -1
  34. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/indexer.py +14 -12
  35. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/ingest.py +2 -2
  36. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/oidc.py +2 -1
  37. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/proxy.py +1 -1
  38. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/store.py +8 -7
  39. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/server.py +8 -10
  40. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/store.py +7 -7
  41. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/visualize.py +6 -3
  42. witan_code-0.22.0/tests/test_cli_output_format.py +0 -95
  43. {witan_code-0.22.0 → witan_code-0.23.0}/.gitignore +0 -0
  44. {witan_code-0.22.0 → witan_code-0.23.0}/changelog.d/README.md +0 -0
  45. {witan_code-0.22.0 → witan_code-0.23.0}/config/claude.json +0 -0
  46. {witan_code-0.22.0 → witan_code-0.23.0}/config/copilot.json +0 -0
  47. {witan_code-0.22.0 → witan_code-0.23.0}/config/pi.json +0 -0
  48. {witan_code-0.22.0 → witan_code-0.23.0}/docs/BRANCH_INDEXING.md +0 -0
  49. {witan_code-0.22.0 → witan_code-0.23.0}/docs/CLI_REFERENCE.md +0 -0
  50. {witan_code-0.22.0 → witan_code-0.23.0}/docs/PACKAGE_MAP.md +0 -0
  51. {witan_code-0.22.0 → witan_code-0.23.0}/docs/SYMBOL_FORMAT.md +0 -0
  52. {witan_code-0.22.0 → witan_code-0.23.0}/docs/SYMBOL_TABLE.md +0 -0
  53. {witan_code-0.22.0 → witan_code-0.23.0}/docs/USER_GUIDE.md +0 -0
  54. {witan_code-0.22.0 → witan_code-0.23.0}/install.sh +0 -0
  55. {witan_code-0.22.0 → witan_code-0.23.0}/tests/__init__.py +0 -0
  56. {witan_code-0.22.0 → witan_code-0.23.0}/tests/conftest.py +0 -0
  57. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_batched_deletes.py +0 -0
  58. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_chunked_load.py +0 -0
  59. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_dispatch.py +0 -0
  60. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_cli_hooks.py +0 -0
  61. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_config.py +0 -0
  62. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_context.py +0 -0
  63. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_crossrepo.py +0 -0
  64. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_elicit.py +0 -0
  65. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_enrichment.py +0 -0
  66. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_graph.py +0 -0
  67. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_identity.py +0 -0
  68. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_reports_its_store.py +0 -0
  69. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_index_requires_a_repo_key.py +0 -0
  70. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_indexer.py +0 -0
  71. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_languages.py +0 -0
  72. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_maintenance.py +0 -0
  73. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_package_map.py +0 -0
  74. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_reaper.py +0 -0
  75. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_rebuild_honours_the_repo_key.py +0 -0
  76. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_refusal_audit.py +0 -0
  77. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_remote_proxy.py +1 -1
  78. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_server.py +0 -0
  79. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_stitch.py +0 -0
  80. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_symbol_table.py +0 -0
  81. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_tasks.py +0 -0
  82. {witan_code-0.22.0 → witan_code-0.23.0}/tests/test_tool_schema.py +0 -0
  83. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/__init__.py +0 -0
  84. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/__main__.py +0 -0
  85. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/edges.py +0 -0
  86. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/elicit.py +0 -0
  87. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/extensions/pi/codegraph.ts +0 -0
  88. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/maintenance.py +0 -0
  89. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/output.py +0 -0
  90. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/package_map.py +0 -0
  91. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/bridge.gq +0 -0
  92. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/code_mutations.gq +0 -0
  93. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/code_read.gq +0 -0
  94. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries/delete.gq +0 -0
  95. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/bash.scm +0 -0
  96. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/hcl.scm +0 -0
  97. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/python.scm +0 -0
  98. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/sql.scm +0 -0
  99. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/typescript.scm +0 -0
  100. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/queries_ts/yaml.scm +0 -0
  101. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/reaper.py +0 -0
  102. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/remote/__init__.py +0 -0
  103. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/repo.py +0 -0
  104. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/schema/bridge-schema.pg +0 -0
  105. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/schema/code-schema.pg +0 -0
  106. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/selected_target.py +0 -0
  107. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/setup.py +0 -0
  108. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/skills/witan-code/SKILL.md +0 -0
  109. {witan_code-0.22.0 → witan_code-0.23.0}/witan_code/stitch.py +0 -0
  110. {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.22.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<5,>=4
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/*` | `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.
@@ -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.
@@ -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.22.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,<5",
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.22.0"
247
+ current_version = "0.23.0"
248
248
  allow_dirty = true
249
249
 
250
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