witan-code 0.15.0__tar.gz → 0.16.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 (103) hide show
  1. {witan_code-0.15.0 → witan_code-0.16.0}/.gitignore +3 -0
  2. {witan_code-0.15.0 → witan_code-0.16.0}/CHANGELOG.md +144 -0
  3. {witan_code-0.15.0 → witan_code-0.16.0}/PKG-INFO +54 -9
  4. {witan_code-0.15.0 → witan_code-0.16.0}/README.md +53 -8
  5. witan_code-0.16.0/conftest.py +26 -0
  6. {witan_code-0.15.0 → witan_code-0.16.0}/docs/USER_GUIDE.md +6 -4
  7. {witan_code-0.15.0 → witan_code-0.16.0}/pyproject.toml +2 -2
  8. {witan_code-0.15.0 → witan_code-0.16.0}/tests/conftest.py +44 -2
  9. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_branches.py +45 -0
  10. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_cli_dispatch.py +60 -0
  11. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_context.py +131 -3
  12. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_graph.py +43 -0
  13. witan_code-0.16.0/tests/test_indexer.py +142 -0
  14. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_store.py +263 -0
  15. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/cli.py +207 -3
  16. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/context.py +103 -6
  17. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/graph.py +30 -5
  18. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/indexer.py +25 -1
  19. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries/bridge.gq +14 -0
  20. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/repo.py +22 -0
  21. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/server.py +52 -3
  22. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/skills/witan-code/SKILL.md +9 -0
  23. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/store.py +179 -13
  24. witan_code-0.15.0/tests/test_indexer.py +0 -48
  25. {witan_code-0.15.0 → witan_code-0.16.0}/config/claude.json +0 -0
  26. {witan_code-0.15.0 → witan_code-0.16.0}/config/copilot.json +0 -0
  27. {witan_code-0.15.0 → witan_code-0.16.0}/config/pi.json +0 -0
  28. {witan_code-0.15.0 → witan_code-0.16.0}/docs/BRANCH_INDEXING.md +0 -0
  29. {witan_code-0.15.0 → witan_code-0.16.0}/docs/CLI_REFERENCE.md +0 -0
  30. {witan_code-0.15.0 → witan_code-0.16.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
  31. {witan_code-0.15.0 → witan_code-0.16.0}/docs/PACKAGE_MAP.md +0 -0
  32. {witan_code-0.15.0 → witan_code-0.16.0}/docs/STAGE2_STITCHING.md +0 -0
  33. {witan_code-0.15.0 → witan_code-0.16.0}/docs/SYMBOL_FORMAT.md +0 -0
  34. {witan_code-0.15.0 → witan_code-0.16.0}/docs/SYMBOL_TABLE.md +0 -0
  35. {witan_code-0.15.0 → witan_code-0.16.0}/install.sh +0 -0
  36. {witan_code-0.15.0 → witan_code-0.16.0}/tests/__init__.py +0 -0
  37. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_batched_deletes.py +0 -0
  38. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_bridge.py +0 -0
  39. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_chunked_load.py +0 -0
  40. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_cli_hooks.py +0 -0
  41. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_cli_output_format.py +0 -0
  42. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_config.py +0 -0
  43. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_config_targets.py +0 -0
  44. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_crossrepo.py +0 -0
  45. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_edges.py +0 -0
  46. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_elicit.py +0 -0
  47. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_enrichment.py +0 -0
  48. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_github_app.py +0 -0
  49. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_hooks.py +0 -0
  50. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_identity.py +0 -0
  51. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_index_failure_diagnostics.py +0 -0
  52. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_indexer_scope.py +0 -0
  53. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_ingest.py +0 -0
  54. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_languages.py +0 -0
  55. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_maintenance.py +0 -0
  56. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_package_map.py +0 -0
  57. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_reaper.py +0 -0
  58. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_remote_proxy.py +0 -0
  59. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_remote_store.py +0 -0
  60. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_repo.py +0 -0
  61. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_server.py +0 -0
  62. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_setup.py +0 -0
  63. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_stitch.py +0 -0
  64. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_symbol_table.py +0 -0
  65. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_tasks.py +0 -0
  66. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_tool_schema.py +0 -0
  67. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_views.py +0 -0
  68. {witan_code-0.15.0 → witan_code-0.16.0}/tests/test_visualize.py +0 -0
  69. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/__init__.py +0 -0
  70. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/__main__.py +0 -0
  71. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/bridge.py +0 -0
  72. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/bridge_extractors.py +0 -0
  73. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/config.py +0 -0
  74. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/edges.py +0 -0
  75. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/elicit.py +0 -0
  76. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/extensions/pi/codegraph.ts +0 -0
  77. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/github_app.py +0 -0
  78. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/hooks.py +0 -0
  79. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/identity.py +0 -0
  80. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/ingest.py +0 -0
  81. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/maintenance.py +0 -0
  82. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/output.py +0 -0
  83. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/package_map.py +0 -0
  84. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries/code_mutations.gq +0 -0
  85. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries/code_read.gq +0 -0
  86. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries/delete.gq +0 -0
  87. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/bash.scm +0 -0
  88. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/hcl.scm +0 -0
  89. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/python.scm +0 -0
  90. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/sql.scm +0 -0
  91. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/typescript.scm +0 -0
  92. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/queries_ts/yaml.scm +0 -0
  93. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/reaper.py +0 -0
  94. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/remote/__init__.py +0 -0
  95. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/remote/oidc.py +0 -0
  96. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/remote/proxy.py +0 -0
  97. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/remote/store.py +0 -0
  98. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/schema/bridge-schema.pg +0 -0
  99. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/schema/code-schema.pg +0 -0
  100. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/setup.py +0 -0
  101. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/stitch.py +0 -0
  102. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/views.py +0 -0
  103. {witan_code-0.15.0 → witan_code-0.16.0}/witan_code/visualize.py +0 -0
@@ -36,3 +36,6 @@ mcp-server.log
36
36
 
37
37
  # agent-config-kit remote skill/hook fetch cache
38
38
  .agent-config-kit-cache/
39
+
40
+ # Zensical build output for the witan-context docs site.
41
+ site/
@@ -6,6 +6,150 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0:
7
7
  a MINOR bump may include breaking changes).
8
8
 
9
+ ## [Unreleased]
10
+
11
+ ### Fixed
12
+
13
+ - **A failed cross-repo bridge write is reported instead of swallowed.** The
14
+ production bridge wedged for ~15 hours on 2026-08-25 and nothing said so:
15
+ every CI cycle logged the failure, printed `bindings=0 errors=0`, exited 0,
16
+ and raised no Sentry issue. Two independent causes.
17
+
18
+ The site logged at `warning`. `configure_sentry` installs
19
+ `LoggingIntegration(event_level=ERROR)` so a call site needs no
20
+ `capture_exception`, which makes the level the entire mechanism — and by that
21
+ contract a warning declares the failure "expected and already handled". A
22
+ throwing bridge write is neither. It logs at `error` now; verified end to end
23
+ against a recording transport (warning → 0 events, error → 1 issue with the
24
+ exception attached).
25
+
26
+ Sentry was also never initialised on that path: `configure_observability()`
27
+ ran only in `serve`, and the CI indexer runs `witan code index`. It now runs
28
+ in the CLI's meta launcher for every command, with `instrument=False` to keep
29
+ the OTel auto-instrumentors out of a short-lived CLI. Everything there no-ops
30
+ without its env var, so a developer with no `SENTRY_DSN` pays nothing.
31
+
32
+ The failure is now counted (`errors`) and flagged (`bridge=FAILED` in the
33
+ summary). `bindings=0` previously meant both "nothing to write" and "the
34
+ write threw". Deliberately still non-fatal — the per-repo index succeeds and
35
+ is worth keeping — but "non-fatal" and "unreported" are different claims.
36
+
37
+
38
+ - **The test suite no longer asserts an environment.** Four tests
39
+ (`test_branches.py`'s three branch-view assertions and
40
+ `test_graph.py::test_a_shared_branch_view_needs_an_identity_to_own_it`)
41
+ failed on any machine whose `witan login` had resolved an actor, and passed
42
+ in CI, where a GitHub runner has none. They asserted un-namespaced view
43
+ names and the logged-out refusal prose — the shape identity resolution
44
+ happens to take when there is no identity — because nothing stopped
45
+ `identity.actor_id()` from reading the real `~/.config/witan/config.toml`
46
+ and OIDC token cache.
47
+
48
+ The `_fresh_identity` fixture now points `WITAN_CONFIG` and
49
+ `WITAN_TOKEN_CACHE` at `tmp_path` and clears the four env vars that
50
+ short-circuit ahead of them, so logged-out is deterministic everywhere. A
51
+ new `logged_in_actor` fixture opts back in through `WITAN_ACTOR`, and
52
+ `test_a_logged_in_writer_owns_the_views_it_indexes` uses it to cover the
53
+ half of `witan_code.views` a CI runner cannot reach: on a logged-in machine
54
+ the views carry their owner, on the local store too.
55
+
56
+ ### Changed
57
+
58
+ - **`graph.check_writable` requires `actor`; `None` now means "no identity",
59
+ full stop.** It used to fall back to `identity.actor_id()`, which gave the
60
+ parameter two meanings depending on the caller. Every call site
61
+ (`indexer`, `bridge`, `ingest`) already resolves an actor — it needs one to
62
+ *name* the view it is about to write — so nothing relied on the fallback,
63
+ and for `ingest` it was wrong: a request arriving with no actor was judged
64
+ against the serving process's identity rather than being refused for having
65
+ none.
66
+
67
+ ## [0.16.0] - 2026-08-25
68
+
69
+ ### Added
70
+
71
+ - **`witan-code doctor` and the `code_store_health` tool — a readiness check
72
+ that covers the bridge store.** Every per-repo graph *and* the shared
73
+ `_bridge.omni`, probed with the cheapest query that has to open a store to
74
+ answer. The bridge is the point: it belongs to no repo, so it appears in no
75
+ repo listing, and `code_interface_search` / `code_interface_providers` /
76
+ `code_interface_consumers` / `code_cross_repo_impact` all read it and
77
+ nothing else does. A bridge that cannot be opened breaks all of them while
78
+ `code_indexed_repos` still lists every repo happily.
79
+
80
+ That is not hypothetical. On the machine this was written on, all 54 local
81
+ stores had been unreadable since an omnigraph upgrade — the bridge for six
82
+ weeks — and nothing reported it: the failure surfaced only as an identical
83
+ Rust backtrace per tool call, and the prompt hook went on asserting
84
+ "N other repos indexed, so cross-repo `code_interface_*` resolve" on every
85
+ single prompt while none of them could run.
86
+
87
+ - **`witan-code reindex --rebuild`.** Deletes this repo's store, and the
88
+ bridge store if it is also unreadable, then indexes from scratch. Only the
89
+ stores that actually fail to open: `--rebuild` on a healthy store would
90
+ throw away a working index, and dropping a healthy bridge would take every
91
+ other repo's cross-repo bindings with it.
92
+
93
+ Deliberately not witan's `migrate storage` shape (export with the
94
+ pre-upgrade binary, reload with the new one, keep a `.pre-migrate` copy). A
95
+ memory graph holds the only copy of what it knows; a code graph holds a
96
+ derivation of a checkout that is still on disk. So this needs no old binary
97
+ and keeps no backup — a copy no installed binary can open is just disk, and
98
+ one of these stores is 27 GB.
99
+
100
+ Refuses a path that is not the repo root, since it deletes the whole store:
101
+ `reindex src/ --rebuild` would empty the store and refill only `src/`,
102
+ leaving the rest of the repo unindexed with nothing afterwards reporting it.
103
+ The root is resolved from the parent directory when the path is a FILE —
104
+ `index`/`reindex` accept one, and `git -C <file>` exits 128, which read as
105
+ "not a repo" and skipped the guard on the argument shape most likely to be
106
+ typed by accident.
107
+
108
+ A failed deletion raises instead of being swallowed. `ignore_errors=True`
109
+ left an unreadable store in place while the sidecars went anyway and the CLI
110
+ printed "Deleted … (N freed)", announcing a recovery that had not happened.
111
+
112
+ ### Changed
113
+
114
+ - **A stale on-disk format now fails with a remedy instead of a Rust
115
+ backtrace.** witan-code's `OmnigraphClient` sets the `_STORAGE_MISMATCH_HINT`
116
+ that witan-core has always supported and witan has always set; without it
117
+ the client re-raised omnigraph's raw error — ANSI escapes, `Location:`,
118
+ backtrace boilerplate — once per tool call, advising an export path that
119
+ needs a binary the upgrade replaced.
120
+
121
+ - **`code_indexed_repos` distinguishes "unreadable" from "empty".** New
122
+ `unreadable` field carrying the reason; `files` stays `null`. The two used
123
+ to render identically, so a code graph that had been dead for weeks read as
124
+ a repo nobody had indexed much.
125
+
126
+ - **The `UserPromptSubmit` block no longer claims cross-repo resolution works
127
+ without checking.** It probes the bridge and says plainly when
128
+ `code_interface_*` will fail. A store the current repo cannot read replaces
129
+ the size/freshness line with a warning rather than rendering "? files" —
130
+ unless an index is in flight, which is the one benign way to see a store
131
+ that will not open yet.
132
+
133
+ The verdict is cached on disk, not in the process: `witan-code
134
+ inject-context` is a fresh process per prompt, so a module-level memo would
135
+ cache nothing and the block would cost a second store query on every prompt.
136
+
137
+ The bridge is probed with `bridge.gq/count_bindings`, not the per-repo
138
+ `code_read.gq/count_files` — the bridge schema has no `CodeFile` node, so
139
+ the per-repo probe condemns a perfectly healthy bridge. That is not a
140
+ cosmetic misreport, because `--rebuild` keys off the same verdict: it was
141
+ caught deleting each freshly-rebuilt bridge on the very next repo of a
142
+ rebuild sweep, taking every other repo's bindings with it each time.
143
+
144
+ A REMOTE bridge is always probed rather than gated on `StoreRef.exists`,
145
+ which degrades every failed remote probe to `False` by design. Gating on it
146
+ dropped an unreachable cluster bridge out of the report entirely and let
147
+ `code_store_health` answer `ok: true` while every `code_interface_*` tool
148
+ was failing — the exact condition the check exists to detect. A missing
149
+ LOCAL bridge is still absence, not failure: a repo with no contracts yet has
150
+ none, and reporting that as red would fail a healthy fresh install. The
151
+ prompt hook's own bridge probe carried the same defect and is fixed with it.
152
+
9
153
  ## [0.15.0] - 2026-08-21
10
154
 
11
155
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: witan-code
3
- Version: 0.15.0
3
+ Version: 0.16.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
@@ -41,13 +41,13 @@ repository's symbols (functions, methods, classes, modules) and their
41
41
  relationships, then exposes definition / reference / caller / impact queries to
42
42
  the agent.
43
43
 
44
- It is a self-contained sibling of `witan` (Layer 1) and shares its
45
- subprocess/CLI conventions, but stores a **separate, per-repo, local-only**
46
- graph.
44
+ It is a sibling of `witan` (Layer 1) — it never imports it, though both build on
45
+ `witan-core` — and shares its subprocess/CLI conventions, but stores a
46
+ **separate, per-repo** graph rather than writing into Layer 1's.
47
47
 
48
48
  > **Wiring it into your agents locally** (MCP server, the `PostToolUse` reindex
49
49
  > hook, and the indexer CLI, run straight from your checkout): see
50
- > [Local Development Setup](../../../docs/agent-memory.md#local-development-setup).
50
+ > [Local Development Setup](../../../docs/internals/agent-memory.md#local-development-setup).
51
51
 
52
52
  ## When to use this vs. grep / the `Explore` agent
53
53
 
@@ -70,7 +70,14 @@ resolution, not a true call graph) — see
70
70
  | Layer | Server | Stores | Scope | Synced |
71
71
  |------|--------|--------|-------|--------|
72
72
  | 1 | `witan` | patterns, project facts, lessons, workflow traces | team-wide | yes (S3) |
73
- | 2 | `witan-code` | code symbols + edges | per-repo | **no — local only** |
73
+ | 2 | `witan-code` | code symbols + edges | per-repo | local by default; shareable |
74
+
75
+ Layer 2 was local-only originally and is no longer. A code graph can live on a
76
+ shared `omnigraph-server` (`WITAN_CODE_SERVER` in-cluster, or through the
77
+ deployed witan MCP tier from outside it), where a CI indexer owns each repo's
78
+ default `main` view and every other writer gets its own per-actor branch view.
79
+ On a local store there is one user, who is its writer, and none of that
80
+ arbitration applies. See [`docs/BRANCH_INDEXING.md`](docs/BRANCH_INDEXING.md).
74
81
 
75
82
  The two layers compose through **soft symbol-ID references**. A Layer-1 node
76
83
  (e.g. a `lesson` or `agent_context`) can record symbol ids of the form:
@@ -115,7 +122,7 @@ The per-repo graph stops at a repo boundary, but service-oriented architectures
115
122
  couple repos through **shared contracts**: an env var that infra sets and an app
116
123
  reads, an HTTP endpoint one service serves and another calls, a package one repo
117
124
  publishes and others import. The bridge records these as **interface bindings**
118
- in a single shared, local-only store (`_bridge.omni`, a sibling of the per-repo
125
+ in a single shared store (`_bridge.omni` locally, a sibling of the per-repo
119
126
  stores) so linkages can be queried across every indexed repo.
120
127
 
121
128
  It is **zero-config**: every `index`/reindex of a repo also extracts that repo's
@@ -519,8 +526,9 @@ graph):
519
526
 
520
527
  | Tool | Returns |
521
528
  |------|---------|
522
- | `code_indexed_repos()` | every indexed repo with file count, size, and last-indexed timestamp |
529
+ | `code_indexed_repos()` | every indexed repo with file count, size, and last-indexed timestamp; `files: null` + `unreadable: <why>` for a store no `code_*` tool can query |
523
530
  | `code_indexed_branches(branch=None)` | the in-flight branch views each repo's store carries, and who owns each; pass a git branch to see every writer's view of it |
531
+ | `code_store_health()` | whether every code graph opens — per-repo **and the bridge**; the readiness check to run before reading an empty result as "nothing found" (see [Rebuilding an unreadable store](#rebuilding-an-unreadable-store)) |
524
532
 
525
533
  ## CLI
526
534
 
@@ -529,7 +537,12 @@ graph):
529
537
  - `setup [--dry-run]` — fetch the pinned omnigraph binary to `~/.local/bin/`
530
538
  for standalone use (see [Install](#install)).
531
539
  - `index [PATH]` — incremental; skips files whose content hash is unchanged.
532
- - `reindex [PATH]` — force rebuild a path.
540
+ - `reindex [PATH] [--rebuild] [--yes]` — force rebuild a path. `--rebuild`
541
+ first deletes this repo's store, and the bridge store, *if they cannot be
542
+ read* — see [Rebuilding an unreadable store](#rebuilding-an-unreadable-store).
543
+ - `doctor` — check that every code graph opens: each per-repo store **and the
544
+ shared bridge store**, which belongs to no repo and so appears in no other
545
+ listing. Exits non-zero if any is unreadable, so it works as a check.
533
546
  - `repos` — list all indexed repos with file count, symbol count, and store size.
534
547
  - `branches [--branch B] [--prune]` — list the in-flight branch views per
535
548
  store; `--branch` shows every writer's view of one git branch (how you find
@@ -628,6 +641,38 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
628
641
  `cleanup` GCs old Lance versions to reclaim disk and is destructive, so it
629
642
  requires `--yes`.
630
643
 
644
+ ## Rebuilding an unreadable store
645
+
646
+ omnigraph uses strict single-version storage: a release that bumps the
647
+ on-disk format refuses to open a graph an older binary wrote (`__manifest is
648
+ stamped at internal schema vN, but this omnigraph reads only vM`). Every
649
+ `code_*` read against such a store errors, permanently, until it is rebuilt.
650
+
651
+ `witan-code doctor` is how you find out. It probes every per-repo store **and
652
+ the bridge store**, which is the one that goes unwatched: it belongs to no
653
+ repo, so it is in no repo listing, and `code_interface_search` /
654
+ `code_interface_providers` / `code_interface_consumers` /
655
+ `code_cross_repo_impact` all read it and nothing else does. A bridge that
656
+ cannot be opened breaks all of those while every per-repo listing still looks
657
+ healthy.
658
+
659
+ The fix is **not** witan's `witan migrate storage` (export with the
660
+ pre-upgrade binary, reload with the new one). A code graph is *derived* from a
661
+ checkout that is still on disk, so reindexing rebuilds it from source and
662
+ needs no old binary:
663
+
664
+ ```console
665
+ witan-code doctor # which stores, and why
666
+ witan-code reindex --rebuild # from each affected checkout
667
+ ```
668
+
669
+ `--rebuild` deletes only the stores it could not read — this repo's, and the
670
+ bridge if it is unreadable too — then indexes from scratch. Nothing is kept
671
+ aside: a copy no installed binary can open is just disk. Dropping the bridge
672
+ does cost every *other* repo's cross-repo bindings, and each restores its own
673
+ on its next index, so a machine with many indexed repos wants a
674
+ `witan-code reindex --rebuild` pass over each checkout it cares about.
675
+
631
676
  ## Skill
632
677
 
633
678
  [`witan_code/skills/witan-code/SKILL.md`](witan_code/skills/witan-code/SKILL.md)
@@ -8,13 +8,13 @@ repository's symbols (functions, methods, classes, modules) and their
8
8
  relationships, then exposes definition / reference / caller / impact queries to
9
9
  the agent.
10
10
 
11
- It is a self-contained sibling of `witan` (Layer 1) and shares its
12
- subprocess/CLI conventions, but stores a **separate, per-repo, local-only**
13
- graph.
11
+ It is a sibling of `witan` (Layer 1) — it never imports it, though both build on
12
+ `witan-core` — and shares its subprocess/CLI conventions, but stores a
13
+ **separate, per-repo** graph rather than writing into Layer 1's.
14
14
 
15
15
  > **Wiring it into your agents locally** (MCP server, the `PostToolUse` reindex
16
16
  > hook, and the indexer CLI, run straight from your checkout): see
17
- > [Local Development Setup](../../../docs/agent-memory.md#local-development-setup).
17
+ > [Local Development Setup](../../../docs/internals/agent-memory.md#local-development-setup).
18
18
 
19
19
  ## When to use this vs. grep / the `Explore` agent
20
20
 
@@ -37,7 +37,14 @@ resolution, not a true call graph) — see
37
37
  | Layer | Server | Stores | Scope | Synced |
38
38
  |------|--------|--------|-------|--------|
39
39
  | 1 | `witan` | patterns, project facts, lessons, workflow traces | team-wide | yes (S3) |
40
- | 2 | `witan-code` | code symbols + edges | per-repo | **no — local only** |
40
+ | 2 | `witan-code` | code symbols + edges | per-repo | local by default; shareable |
41
+
42
+ Layer 2 was local-only originally and is no longer. A code graph can live on a
43
+ shared `omnigraph-server` (`WITAN_CODE_SERVER` in-cluster, or through the
44
+ deployed witan MCP tier from outside it), where a CI indexer owns each repo's
45
+ default `main` view and every other writer gets its own per-actor branch view.
46
+ On a local store there is one user, who is its writer, and none of that
47
+ arbitration applies. See [`docs/BRANCH_INDEXING.md`](docs/BRANCH_INDEXING.md).
41
48
 
42
49
  The two layers compose through **soft symbol-ID references**. A Layer-1 node
43
50
  (e.g. a `lesson` or `agent_context`) can record symbol ids of the form:
@@ -82,7 +89,7 @@ The per-repo graph stops at a repo boundary, but service-oriented architectures
82
89
  couple repos through **shared contracts**: an env var that infra sets and an app
83
90
  reads, an HTTP endpoint one service serves and another calls, a package one repo
84
91
  publishes and others import. The bridge records these as **interface bindings**
85
- in a single shared, local-only store (`_bridge.omni`, a sibling of the per-repo
92
+ in a single shared store (`_bridge.omni` locally, a sibling of the per-repo
86
93
  stores) so linkages can be queried across every indexed repo.
87
94
 
88
95
  It is **zero-config**: every `index`/reindex of a repo also extracts that repo's
@@ -486,8 +493,9 @@ graph):
486
493
 
487
494
  | Tool | Returns |
488
495
  |------|---------|
489
- | `code_indexed_repos()` | every indexed repo with file count, size, and last-indexed timestamp |
496
+ | `code_indexed_repos()` | every indexed repo with file count, size, and last-indexed timestamp; `files: null` + `unreadable: <why>` for a store no `code_*` tool can query |
490
497
  | `code_indexed_branches(branch=None)` | the in-flight branch views each repo's store carries, and who owns each; pass a git branch to see every writer's view of it |
498
+ | `code_store_health()` | whether every code graph opens — per-repo **and the bridge**; the readiness check to run before reading an empty result as "nothing found" (see [Rebuilding an unreadable store](#rebuilding-an-unreadable-store)) |
491
499
 
492
500
  ## CLI
493
501
 
@@ -496,7 +504,12 @@ graph):
496
504
  - `setup [--dry-run]` — fetch the pinned omnigraph binary to `~/.local/bin/`
497
505
  for standalone use (see [Install](#install)).
498
506
  - `index [PATH]` — incremental; skips files whose content hash is unchanged.
499
- - `reindex [PATH]` — force rebuild a path.
507
+ - `reindex [PATH] [--rebuild] [--yes]` — force rebuild a path. `--rebuild`
508
+ first deletes this repo's store, and the bridge store, *if they cannot be
509
+ read* — see [Rebuilding an unreadable store](#rebuilding-an-unreadable-store).
510
+ - `doctor` — check that every code graph opens: each per-repo store **and the
511
+ shared bridge store**, which belongs to no repo and so appears in no other
512
+ listing. Exits non-zero if any is unreadable, so it works as a check.
500
513
  - `repos` — list all indexed repos with file count, symbol count, and store size.
501
514
  - `branches [--branch B] [--prune]` — list the in-flight branch views per
502
515
  store; `--branch` shows every writer's view of one git branch (how you find
@@ -595,6 +608,38 @@ mechanisms keep this in check, mirroring that module (deliberately duplicated
595
608
  `cleanup` GCs old Lance versions to reclaim disk and is destructive, so it
596
609
  requires `--yes`.
597
610
 
611
+ ## Rebuilding an unreadable store
612
+
613
+ omnigraph uses strict single-version storage: a release that bumps the
614
+ on-disk format refuses to open a graph an older binary wrote (`__manifest is
615
+ stamped at internal schema vN, but this omnigraph reads only vM`). Every
616
+ `code_*` read against such a store errors, permanently, until it is rebuilt.
617
+
618
+ `witan-code doctor` is how you find out. It probes every per-repo store **and
619
+ the bridge store**, which is the one that goes unwatched: it belongs to no
620
+ repo, so it is in no repo listing, and `code_interface_search` /
621
+ `code_interface_providers` / `code_interface_consumers` /
622
+ `code_cross_repo_impact` all read it and nothing else does. A bridge that
623
+ cannot be opened breaks all of those while every per-repo listing still looks
624
+ healthy.
625
+
626
+ The fix is **not** witan's `witan migrate storage` (export with the
627
+ pre-upgrade binary, reload with the new one). A code graph is *derived* from a
628
+ checkout that is still on disk, so reindexing rebuilds it from source and
629
+ needs no old binary:
630
+
631
+ ```console
632
+ witan-code doctor # which stores, and why
633
+ witan-code reindex --rebuild # from each affected checkout
634
+ ```
635
+
636
+ `--rebuild` deletes only the stores it could not read — this repo's, and the
637
+ bridge if it is unreadable too — then indexes from scratch. Nothing is kept
638
+ aside: a copy no installed binary can open is just disk. Dropping the bridge
639
+ does cost every *other* repo's cross-repo bindings, and each restores its own
640
+ on its next index, so a machine with many indexed repos wants a
641
+ `witan-code reindex --rebuild` pass over each checkout it cares about.
642
+
598
643
  ## Skill
599
644
 
600
645
  [`witan_code/skills/witan-code/SKILL.md`](witan_code/skills/witan-code/SKILL.md)
@@ -0,0 +1,26 @@
1
+ """Load the workspace's shared test-environment guard.
2
+
3
+ This file exists to be imported EARLY. ``testsupport.hermetic`` redirects every
4
+ ambient input — HOME, the XDG dirs, the witan state files and graph stores, the
5
+ terminal width — at import time rather than in a fixture, because the leak it
6
+ exists to stop can happen while pytest is still collecting (importing
7
+ ``witan.server`` creates a graph). A rootdir ``conftest.py`` is the earliest
8
+ hook that runs for every invocation.
9
+
10
+ ★ IMPORTED, not named in ``pytest_plugins``. That setting is only honoured in
11
+ whichever conftest is TOP-LEVEL for the rootdir pytest picked, and the rootdir
12
+ depends on the arguments: run ``pytest`` from the repo root and all five of
13
+ these become non-top-level, aborting collection outright with "Defining
14
+ 'pytest_plugins' in a non-top-level conftest is no longer supported". A plain
15
+ import carries no such rule, does the same redirection (it happens at module
16
+ import), and re-exporting the hook below makes this conftest its own plugin.
17
+ """
18
+
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
23
+
24
+ # The import is the point: `testsupport.hermetic` redirects the environment at
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
@@ -171,10 +171,12 @@ not indexed yet (HCL/Terraform is the leading candidate).
171
171
  ## Cross-repo bridge in brief
172
172
 
173
173
  Per-repo indexing stops at the repo boundary; the bridge is a separate,
174
- shared, local-only store (`_bridge.omni`) that links repos through contracts
175
- they share — an env var one repo's infra sets and another reads, an HTTP
176
- endpoint one serves and another calls, a package one publishes and others
177
- import, a service one repo deploys. It is **zero-config**: every `index` /
174
+ shared store that links repos through contracts they share — an env var one
175
+ repo's infra sets and another reads, an HTTP endpoint one serves and another
176
+ calls, a package one publishes and others import, a service one repo deploys.
177
+ It follows the same store resolution as the per-repo graphs: `_bridge.omni`
178
+ beside them locally, and the deployment's bridge graph when one is configured
179
+ (`witan_code.store.bridge_store`). It is **zero-config**: every `index` /
178
180
  `reindex` of any repo also extracts that repo's bindings into the bridge
179
181
  store automatically — there's no registry of repos to maintain.
180
182
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "witan-code"
3
- version = "0.15.0"
3
+ version = "0.16.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"
@@ -205,7 +205,7 @@ testpaths = ["tests"]
205
205
  packages = ["witan_code"]
206
206
 
207
207
  [tool.bumpversion]
208
- current_version = "0.15.0"
208
+ current_version = "0.16.0"
209
209
  allow_dirty = true
210
210
 
211
211
  [[tool.bumpversion.files]]
@@ -41,21 +41,63 @@ def main():
41
41
  """
42
42
 
43
43
 
44
+ # Every input `identity._resolve` reads. Isolating the file paths is not
45
+ # enough on its own: each of these env vars short-circuits ahead of them.
46
+ _IDENTITY_ENV = (
47
+ "WITAN_ACTOR",
48
+ "WITAN_REMOTE_URL",
49
+ "WITAN_OIDC_ISSUER",
50
+ "WITAN_TARGET",
51
+ )
52
+
53
+
44
54
  @pytest.fixture(autouse=True)
45
- def _fresh_identity():
46
- """Forget the process-lifetime actor between tests.
55
+ def _fresh_identity(tmp_path, monkeypatch):
56
+ """Pin the identity these tests run under: nobody, unless one is asked for.
47
57
 
58
+ Two jobs. The first is to forget the process-lifetime actor between tests —
48
59
  ``identity.actor_id`` memoizes deliberately (a witan-code process writes as
49
60
  exactly one identity), which without this would let the first test that
50
61
  resolves one decide the branch-view names for every test after it.
62
+
63
+ The second is to stop that resolution reaching the machine. It reads the
64
+ real ``~/.config/witan/config.toml`` and the real OIDC token cache, so on a
65
+ developer's box — logged in, unlike a CI runner — ``actor_id()`` returns an
66
+ ``act-…`` and the branch views get namespaced under it. Four tests asserted
67
+ un-namespaced names and failed for everyone who had run ``witan login``,
68
+ green in CI the whole time. Pointing both at ``tmp_path`` makes logged-out
69
+ the deterministic default everywhere; ``logged_in_actor`` opts back in.
51
70
  """
52
71
  from witan_code import identity
53
72
 
73
+ monkeypatch.setenv("WITAN_CONFIG", str(tmp_path / "witan-config.toml"))
74
+ monkeypatch.setenv("WITAN_TOKEN_CACHE", str(tmp_path / "witan-tokens.json"))
75
+ for var in _IDENTITY_ENV:
76
+ monkeypatch.delenv(var, raising=False)
77
+
54
78
  identity.reset_cache()
55
79
  yield
56
80
  identity.reset_cache()
57
81
 
58
82
 
83
+ @pytest.fixture
84
+ def logged_in_actor(monkeypatch):
85
+ """Run the test as a specific actor — the half of the guard CI never sees.
86
+
87
+ Set through ``WITAN_ACTOR`` rather than by stubbing ``actor_id``, so the
88
+ resolution path a non-interactive writer (the CI indexer, a maintenance
89
+ job) actually takes is the one under test.
90
+ """
91
+ from witan_code import identity
92
+
93
+ def _login(actor: str = "act-alice") -> str:
94
+ monkeypatch.setenv(identity.ACTOR_ENV_VAR, actor)
95
+ identity.reset_cache()
96
+ return actor
97
+
98
+ return _login
99
+
100
+
59
101
  @pytest.fixture(autouse=True)
60
102
  def _fresh_git_context():
61
103
  """Forget the memoized git context between tests.
@@ -155,6 +155,51 @@ def test_feature_branch_indexes_to_own_branch(tmp_path, monkeypatch):
155
155
  assert not on_main, "main view must not see in-flight branch symbols"
156
156
 
157
157
 
158
+ @requires_stack
159
+ def test_a_logged_in_writer_owns_the_views_it_indexes(
160
+ tmp_path, monkeypatch, logged_in_actor
161
+ ):
162
+ """The other half of `witan_code.views`, which CI never reaches.
163
+
164
+ A GitHub runner has no witan identity, so every branch-view assertion above
165
+ is of the un-namespaced name. On a logged-in machine — every real user —
166
+ the views carry their owner, on the local store too (there is no second
167
+ naming rule for local stores). Asserting it here is what keeps the two
168
+ halves from drifting.
169
+ """
170
+ from witan_code import config as cfg_module
171
+ from witan_code import indexer
172
+ from witan_code.graph import OmnigraphClient
173
+ from witan_code.views import owner
174
+
175
+ actor = logged_in_actor("act-alice")
176
+ monkeypatch.setenv("WITAN_REPO", REPO)
177
+ monkeypatch.setenv("WITAN_CODE_DIR", str(tmp_path / "code"))
178
+ cfg = cfg_module.load()
179
+
180
+ base = _git_repo(tmp_path / "r")
181
+ (base / "svc.py").write_text(SAMPLE)
182
+ indexer.index_path(base, config=cfg)
183
+
184
+ _git(base, "checkout", "-q", "-b", "feature/new-api")
185
+ (base / "extra.py").write_text("def branch_only_symbol():\n return 2\n")
186
+ indexer.index_path(base, config=cfg)
187
+
188
+ view = f"{actor}/feature_new-api"
189
+ store = str(cfg_module.store_path(REPO, cfg.code_dir))
190
+ main_client = OmnigraphClient(store, cfg.queries_dir)
191
+ assert view in main_client.list_branches()
192
+ assert "feature_new-api" not in main_client.list_branches(), (
193
+ "the un-namespaced name is the collision this scheme replaced"
194
+ )
195
+ assert owner(view) == actor
196
+
197
+ branch_client = OmnigraphClient(store, cfg.queries_dir, branch=view)
198
+ assert branch_client.read(
199
+ "code_read.gq", "find_by_name", {"name": "branch_only_symbol"}
200
+ )
201
+
202
+
158
203
  @requires_stack
159
204
  def test_branch_index_writes_to_bridge_overlay_not_main(tmp_path, monkeypatch):
160
205
  """A non-default branch's bridge writes land on its repo-qualified
@@ -295,3 +295,63 @@ def test_branches_prune_is_refused_against_a_shared_store(monkeypatch, capsys):
295
295
  cli_module.branches(prune=True)
296
296
 
297
297
  assert "refusing to prune a shared graph" in capsys.readouterr().out
298
+
299
+
300
+ def test_rebuild_refuses_a_path_that_is_not_the_repo_root(
301
+ tmp_path, monkeypatch, capsys
302
+ ):
303
+ """A rebuild deletes the whole store, so it must reindex the whole repo.
304
+
305
+ `reindex src/ --rebuild` would empty the store and refill only `src/`,
306
+ leaving the rest of the repo unindexed with nothing afterwards reporting
307
+ it — a worse state than the unreadable store it started from.
308
+ """
309
+ from witan_code import repo as repo_module
310
+ from witan_code import store as store_module
311
+
312
+ root = tmp_path / "checkout"
313
+ sub = root / "src"
314
+ sub.mkdir(parents=True)
315
+ monkeypatch.setattr(repo_module, "git_toplevel", lambda _p: root)
316
+
317
+ def _boom(*_args, **_kwargs):
318
+ raise AssertionError("deleted a store for a partial-path rebuild")
319
+
320
+ monkeypatch.setattr(store_module, "discard_store", _boom)
321
+
322
+ with pytest.raises(SystemExit):
323
+ cli_module._rebuild_stores(sub, yes=True)
324
+
325
+ out = capsys.readouterr().out
326
+ assert "not the repo root" in out
327
+ assert str(root) in out
328
+
329
+
330
+ def test_rebuild_refuses_a_file_path_with_real_git(tmp_path, monkeypatch, capsys):
331
+ """`index`/`reindex` accept a FILE, and `git -C <file>` exits 128.
332
+
333
+ Deliberately runs the real `git_toplevel` rather than stubbing it: the
334
+ stub in the test above returns a root for anything, so it cannot see this.
335
+ Reading "Not a directory" as "no repo" skipped the guard entirely and left
336
+ `reindex some_file.py --rebuild` deleting the whole store and refilling it
337
+ with one file — the exact outcome the guard exists to prevent.
338
+ """
339
+ import subprocess
340
+
341
+ from witan_code import store as store_module
342
+
343
+ root = tmp_path / "checkout"
344
+ root.mkdir()
345
+ subprocess.run(["git", "init", "-q", str(root)], check=True)
346
+ target = root / "a.py"
347
+ target.write_text("x = 1\n")
348
+
349
+ def _boom(*_args, **_kwargs):
350
+ raise AssertionError("deleted a store for a single-file rebuild")
351
+
352
+ monkeypatch.setattr(store_module, "discard_store", _boom)
353
+
354
+ with pytest.raises(SystemExit):
355
+ cli_module._rebuild_stores(target, yes=True)
356
+
357
+ assert "not the repo root" in capsys.readouterr().out