witan-code 0.17.0__tar.gz → 0.18.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.17.0 → witan_code-0.18.0}/CHANGELOG.md +46 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/PKG-INFO +1 -1
- {witan_code-0.17.0 → witan_code-0.18.0}/pyproject.toml +2 -2
- witan_code-0.18.0/tests/test_cli_selected_target.py +86 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_config_targets.py +75 -0
- witan_code-0.18.0/tests/test_index_reports_its_store.py +122 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_indexer.py +3 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_remote_store.py +37 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/cli.py +30 -9
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/indexer.py +32 -4
- witan_code-0.18.0/witan_code/selected_target.py +28 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/server.py +9 -2
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/store.py +22 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/.gitignore +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/README.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/config/claude.json +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/config/copilot.json +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/config/pi.json +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/conftest.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/BRANCH_INDEXING.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/CLI_REFERENCE.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/EDGE_PRECISION_TIERS.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/PACKAGE_MAP.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/STAGE2_STITCHING.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/SYMBOL_FORMAT.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/SYMBOL_TABLE.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/docs/USER_GUIDE.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/install.sh +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/__init__.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/conftest.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_batched_deletes.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_branches.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_bridge.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_chunked_load.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_cli_dispatch.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_cli_hooks.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_cli_output_format.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_config.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_context.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_crossrepo.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_edges.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_elicit.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_enrichment.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_github_app.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_graph.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_hooks.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_identity.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_index_failure_diagnostics.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_indexer_scope.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_ingest.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_languages.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_maintenance.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_package_map.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_reaper.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_remote_proxy.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_repo.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_server.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_setup.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_stitch.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_store.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_symbol_table.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_tasks.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_tool_schema.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_views.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/tests/test_visualize.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/__init__.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/__main__.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/bridge.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/bridge_extractors.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/config.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/context.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/edges.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/elicit.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/extensions/pi/codegraph.ts +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/github_app.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/graph.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/hooks.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/identity.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/ingest.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/maintenance.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/output.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/package_map.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries/bridge.gq +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries/code_mutations.gq +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries/code_read.gq +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries/delete.gq +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/bash.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/hcl.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/python.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/sql.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/typescript.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/queries_ts/yaml.scm +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/reaper.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/remote/__init__.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/remote/oidc.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/remote/proxy.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/remote/store.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/repo.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/schema/bridge-schema.pg +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/schema/code-schema.pg +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/setup.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/skills/witan-code/SKILL.md +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/stitch.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/views.py +0 -0
- {witan_code-0.17.0 → witan_code-0.18.0}/witan_code/visualize.py +0 -0
|
@@ -6,6 +6,52 @@ 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
|
+
## [0.18.0] - 2026-09-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`witan --target <name> code index` reaches this package.** witan-council's
|
|
14
|
+
launcher gained an app-level `--target`, and `witan code …` mounts this
|
|
15
|
+
package's cyclopts App but *not* its meta launcher, so the value is forwarded
|
|
16
|
+
into `witan_code.selected_target` and read by `_srv()`. Before this the flag
|
|
17
|
+
was bound but never routed: `cli._srv()` called `load_remote_config()` with no
|
|
18
|
+
argument, so `witan --target ci code repos` answered from the ambient target
|
|
19
|
+
while appearing to work. Standalone `witan-code --target <name> …` works too.
|
|
20
|
+
|
|
21
|
+
- **`witan code index` prints the store it wrote to, unconditionally.** The one
|
|
22
|
+
command whose entire output is about where symbols went was silent about which
|
|
23
|
+
graph received them — and memory and code graphs are routed by separate
|
|
24
|
+
settings, so a target whose memory is deployed can still index onto the
|
|
25
|
+
laptop. Printed for local and shared alike: a line that appears only in the
|
|
26
|
+
bad case is one nobody has learned to look for. Carried on `IndexStats` rather
|
|
27
|
+
than recomputed by the printer, so a *failed* run reports the same
|
|
28
|
+
destination — the run where "which graph was this?" is actually open.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **`witan-code doctor` could not say which graph was broken under
|
|
33
|
+
`code_transport = "mcp"`.** The identifying column took `os.path.basename` of
|
|
34
|
+
the store address, which is right for a local path and wrong for an
|
|
35
|
+
MCP-routed one — those end in the endpoint URL, so all of production's
|
|
36
|
+
per-repo cluster graphs rendered as the identical string `mcp)`. `StoreRef`
|
|
37
|
+
gained a `label` that answers "which graph is this row" for all three address
|
|
38
|
+
forms, and `code_store_health` returns it alongside the full `store`.
|
|
39
|
+
|
|
40
|
+
- **A failed index did not always name its destination.** `IndexStats` was built
|
|
41
|
+
*after* `ensure_branch()` and the existing-hash read, both of which are remote
|
|
42
|
+
calls, so an unreachable endpoint or a rejected token raised before the stats
|
|
43
|
+
existed and the summary had nothing to print. Stats are now built straight
|
|
44
|
+
after the store resolves, and those two setup calls are wrapped so a failure
|
|
45
|
+
arrives as `IndexFailed` carrying the destination. `check_writable` is
|
|
46
|
+
deliberately left outside: it raises `SharedGraphWriteRefused`, a clean
|
|
47
|
+
self-explanatory refusal that callers assert on by type, not the unattributed
|
|
48
|
+
failure the phases exist to annotate.
|
|
49
|
+
|
|
50
|
+
- **`code_transport` is parsed off a target block.** A target registered with
|
|
51
|
+
`--remote-url` now carries `code_transport = "mcp"`, so branches indexed from
|
|
52
|
+
a checkout land on the cluster where another session — and another developer —
|
|
53
|
+
can see them, instead of in a directory on one laptop.
|
|
54
|
+
|
|
9
55
|
## [0.17.0] - 2026-09-01
|
|
10
56
|
|
|
11
57
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: witan-code
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "witan-code"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.18.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"
|
|
@@ -204,7 +204,7 @@ testpaths = ["tests"]
|
|
|
204
204
|
packages = ["witan_code"]
|
|
205
205
|
|
|
206
206
|
[tool.bumpversion]
|
|
207
|
-
current_version = "0.
|
|
207
|
+
current_version = "0.18.0"
|
|
208
208
|
allow_dirty = true
|
|
209
209
|
|
|
210
210
|
[[tool.bumpversion.files]]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""``witan code …`` dispatches to the target witan's launcher bound.
|
|
2
|
+
|
|
3
|
+
This package's App is mounted by witan's CLI but its meta launcher is not, so
|
|
4
|
+
witan's launcher forwards the ``--target`` it bound into
|
|
5
|
+
:mod:`witan_code.selected_target` (the same forwarding ``--output-format``
|
|
6
|
+
already gets, for the same reason). What is asserted here is the other end of
|
|
7
|
+
that wire: that the dispatch path actually consults it.
|
|
8
|
+
|
|
9
|
+
Resolving the ambient target here instead is not a cosmetic gap. ``witan code
|
|
10
|
+
index`` WRITES a code graph, so it would have written to whichever deployment
|
|
11
|
+
the checkout happened to match while the flag on the command line said
|
|
12
|
+
otherwise — and said nothing about it.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from witan_code import cli as code_cli
|
|
20
|
+
from witan_code import config as code_cfg
|
|
21
|
+
from witan_code.selected_target import selected_target, set_selected_target
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(autouse=True)
|
|
25
|
+
def _clean_selection():
|
|
26
|
+
"""Module-level state, so a leaked value would silently steer a later test."""
|
|
27
|
+
set_selected_target(None)
|
|
28
|
+
yield
|
|
29
|
+
set_selected_target(None)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_dispatch_reads_the_forwarded_target(monkeypatch):
|
|
33
|
+
seen = {}
|
|
34
|
+
|
|
35
|
+
def _record(target=None):
|
|
36
|
+
seen["target"] = target
|
|
37
|
+
return None # in-process branch; a stub RemoteConfig is not the point
|
|
38
|
+
|
|
39
|
+
monkeypatch.setattr(code_cli, "_server", None)
|
|
40
|
+
monkeypatch.setattr(code_cfg, "load_remote_config", _record)
|
|
41
|
+
set_selected_target("qa")
|
|
42
|
+
|
|
43
|
+
code_cli._srv()
|
|
44
|
+
|
|
45
|
+
assert seen["target"] == "qa"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_no_forwarded_target_leaves_resolution_where_it_was(monkeypatch):
|
|
49
|
+
"""``None`` is not "no target": the standalone ``witan-code`` launcher
|
|
50
|
+
declares no ``--target``, and resolution falls through to ``WITAN_TARGET``
|
|
51
|
+
and the checkout's ``match_*`` rules exactly as it always did."""
|
|
52
|
+
seen = {}
|
|
53
|
+
|
|
54
|
+
def _record(target=None):
|
|
55
|
+
seen["target"] = target
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(code_cli, "_server", None)
|
|
59
|
+
monkeypatch.setattr(code_cfg, "load_remote_config", _record)
|
|
60
|
+
|
|
61
|
+
code_cli._srv()
|
|
62
|
+
|
|
63
|
+
assert seen["target"] is None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_the_auth_path_reads_it_too(monkeypatch):
|
|
67
|
+
"""``witan code login`` has to reach the same deployment the read commands
|
|
68
|
+
do, or it mints a token against one target and spends it on another."""
|
|
69
|
+
seen = {}
|
|
70
|
+
|
|
71
|
+
def _record(target=None):
|
|
72
|
+
seen["target"] = target
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(code_cfg, "load_remote_config", _record)
|
|
76
|
+
set_selected_target("ci")
|
|
77
|
+
|
|
78
|
+
with pytest.raises(SystemExit):
|
|
79
|
+
code_cli._remote_or_exit() # no remote configured -> clean exit
|
|
80
|
+
|
|
81
|
+
assert seen["target"] == "ci"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_accessor_round_trips():
|
|
85
|
+
set_selected_target("production")
|
|
86
|
+
assert selected_target() == "production"
|
|
@@ -377,3 +377,78 @@ def test_unknown_index_role_is_rejected(monkeypatch, toml_file):
|
|
|
377
377
|
|
|
378
378
|
with pytest.raises(ValueError, match="Unknown index_role"):
|
|
379
379
|
load()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ── The seam with `witan target add` ─────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_the_block_witan_target_add_writes_routes_code_graphs_to_the_cluster(
|
|
386
|
+
monkeypatch, toml_file
|
|
387
|
+
):
|
|
388
|
+
"""The exact block `witan target add --remote-url …` emits, read back here.
|
|
389
|
+
|
|
390
|
+
These are two packages agreeing by convention, not by a shared type: witan
|
|
391
|
+
(witan-council) renders the TOML, witan-code parses it, and nothing fails
|
|
392
|
+
loudly if they drift. Before `target add` learned `--code-transport`, a
|
|
393
|
+
deployed target had no `code_transport` key at all and fell through to the
|
|
394
|
+
`direct` default, leaving code graphs on the developer's own machine while
|
|
395
|
+
memory went to the deployment — silently, since a local store is a working
|
|
396
|
+
store. Keep this literal in step with `witan.cli.targets._FIELD_ORDER`.
|
|
397
|
+
"""
|
|
398
|
+
monkeypatch.setenv(
|
|
399
|
+
"WITAN_CONFIG",
|
|
400
|
+
toml_file(
|
|
401
|
+
"""
|
|
402
|
+
[targets.ol]
|
|
403
|
+
remote_url = "https://witan.ci.ol.mit.edu/mcp"
|
|
404
|
+
oidc_issuer = "https://sso-ci.ol.mit.edu/realms/ol-platform-engineering"
|
|
405
|
+
oidc_audience = "witan"
|
|
406
|
+
code_transport = "mcp"
|
|
407
|
+
match_orgs = ["mitodl"]
|
|
408
|
+
"""
|
|
409
|
+
),
|
|
410
|
+
)
|
|
411
|
+
monkeypatch.setenv("WITAN_REPO", "https://github.com/mitodl/agent-kit")
|
|
412
|
+
monkeypatch.delenv("WITAN_TARGET", raising=False)
|
|
413
|
+
monkeypatch.delenv("WITAN_CODE_TRANSPORT", raising=False)
|
|
414
|
+
monkeypatch.delenv("WITAN_CODE_SERVER", raising=False)
|
|
415
|
+
|
|
416
|
+
cfg = load()
|
|
417
|
+
assert cfg.target_name == "ol"
|
|
418
|
+
assert cfg.code_transport == "mcp"
|
|
419
|
+
# `mcp` alone makes it a cluster graph — code_server stays unset, because
|
|
420
|
+
# the deployment holds the address and resolves the actor from the JWT.
|
|
421
|
+
assert cfg.code_server is None
|
|
422
|
+
assert cfg.is_cluster
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_a_deployed_target_without_code_transport_still_falls_back_to_local(
|
|
426
|
+
monkeypatch, toml_file
|
|
427
|
+
):
|
|
428
|
+
"""The pre-fix shape, pinned so the regression is legible rather than silent.
|
|
429
|
+
|
|
430
|
+
This is what every target registered before 2026-09-01 looks like. It is
|
|
431
|
+
not an error and never was — which is the whole problem, and why the
|
|
432
|
+
onboarding doc now tells people to re-run `target add --force`.
|
|
433
|
+
"""
|
|
434
|
+
monkeypatch.setenv(
|
|
435
|
+
"WITAN_CONFIG",
|
|
436
|
+
toml_file(
|
|
437
|
+
"""
|
|
438
|
+
[targets.ol]
|
|
439
|
+
remote_url = "https://witan.ci.ol.mit.edu/mcp"
|
|
440
|
+
oidc_issuer = "https://sso-ci.ol.mit.edu/realms/ol-platform-engineering"
|
|
441
|
+
match_orgs = ["mitodl"]
|
|
442
|
+
"""
|
|
443
|
+
),
|
|
444
|
+
)
|
|
445
|
+
monkeypatch.setenv("WITAN_REPO", "https://github.com/mitodl/agent-kit")
|
|
446
|
+
monkeypatch.delenv("WITAN_TARGET", raising=False)
|
|
447
|
+
monkeypatch.delenv("WITAN_CODE_TRANSPORT", raising=False)
|
|
448
|
+
monkeypatch.delenv("WITAN_CODE_SERVER", raising=False)
|
|
449
|
+
|
|
450
|
+
cfg = load()
|
|
451
|
+
assert cfg.code_transport == "direct"
|
|
452
|
+
assert not cfg.is_cluster
|
|
453
|
+
# Memory is deployed while code graphs are not — the split this task fixed.
|
|
454
|
+
assert load_remote_config() is not None
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""`witan code index` has to say which graph it wrote.
|
|
2
|
+
|
|
3
|
+
Memory and code graphs are routed by SEPARATE settings, so a target whose
|
|
4
|
+
memory is deployed can still index onto the laptop — three non-maintainers were
|
|
5
|
+
in exactly that state on production. The one command whose entire output is
|
|
6
|
+
about where symbols went was silent about which store received them.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from witan_code import cli as cli_module
|
|
16
|
+
from witan_code import indexer
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_a_successful_index_names_the_store(monkeypatch, capsys):
|
|
20
|
+
stats = indexer.IndexStats(
|
|
21
|
+
scanned=12,
|
|
22
|
+
indexed=12,
|
|
23
|
+
symbols=88,
|
|
24
|
+
store="/home/dev/.local/share/witan-code/x.omni",
|
|
25
|
+
)
|
|
26
|
+
monkeypatch.setattr(indexer, "index_path", lambda path, *, force=False, **k: stats)
|
|
27
|
+
|
|
28
|
+
cli_module.index(Path("."))
|
|
29
|
+
|
|
30
|
+
assert "/home/dev/.local/share/witan-code/x.omni" in capsys.readouterr().out
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_a_shared_store_is_reported_the_same_way(monkeypatch, capsys):
|
|
34
|
+
"""Printed for local and shared alike. A line that appears only in the bad
|
|
35
|
+
case is a line nobody has learned to look for — and reading it is how "where
|
|
36
|
+
did this go" gets answered without knowing the setting exists."""
|
|
37
|
+
stats = indexer.IndexStats(
|
|
38
|
+
scanned=12, store="github.com/mitodl/agent-kit (via https://witan.example/mcp)"
|
|
39
|
+
)
|
|
40
|
+
monkeypatch.setattr(indexer, "index_path", lambda path, *, force=False, **k: stats)
|
|
41
|
+
|
|
42
|
+
cli_module.index(Path("."))
|
|
43
|
+
|
|
44
|
+
assert "https://witan.example/mcp" in capsys.readouterr().out
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_the_branch_view_is_named_when_there_is_one(monkeypatch, capsys):
|
|
48
|
+
"""On a shared graph the view is per writer per git branch, so the store
|
|
49
|
+
alone does not identify what a reader has to look at."""
|
|
50
|
+
stats = indexer.IndexStats(store="s", branch="act-0eb-feature-x")
|
|
51
|
+
monkeypatch.setattr(indexer, "index_path", lambda path, *, force=False, **k: stats)
|
|
52
|
+
|
|
53
|
+
cli_module.index(Path("."))
|
|
54
|
+
|
|
55
|
+
assert "branch=act-0eb-feature-x" in capsys.readouterr().out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_the_default_branch_prints_no_branch_field(monkeypatch, capsys):
|
|
59
|
+
stats = indexer.IndexStats(store="s")
|
|
60
|
+
monkeypatch.setattr(indexer, "index_path", lambda path, *, force=False, **k: stats)
|
|
61
|
+
|
|
62
|
+
cli_module.index(Path("."))
|
|
63
|
+
|
|
64
|
+
assert "branch=" not in capsys.readouterr().out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_a_failed_index_still_names_the_store(monkeypatch, capsys):
|
|
68
|
+
"""The failing run is the one where "which graph was this?" is the open
|
|
69
|
+
question, so the destination is carried on the stats rather than recomputed
|
|
70
|
+
by the printer."""
|
|
71
|
+
stats = indexer.IndexStats(scanned=657, store="https://witan.example/mcp")
|
|
72
|
+
|
|
73
|
+
def failing_index(path, *, force=False, **kwargs):
|
|
74
|
+
raise indexer.IndexFailed(
|
|
75
|
+
"load of nodes and edges",
|
|
76
|
+
RuntimeError("timed out"),
|
|
77
|
+
stats=stats,
|
|
78
|
+
elapsed=181.0,
|
|
79
|
+
detail={"records": 9000},
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
monkeypatch.setattr(indexer, "index_path", failing_index)
|
|
83
|
+
|
|
84
|
+
with pytest.raises(indexer.IndexFailed):
|
|
85
|
+
cli_module.index(Path("."))
|
|
86
|
+
|
|
87
|
+
assert "https://witan.example/mcp" in capsys.readouterr().out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_a_setup_failure_names_the_store_it_was_reaching(tmp_path, monkeypatch):
|
|
91
|
+
"""Regression, PR #316 review: the guarantee has to cover setup, not just
|
|
92
|
+
the load.
|
|
93
|
+
|
|
94
|
+
`ensure_branch` and the existing-hash read are REMOTE calls, and `IndexStats`
|
|
95
|
+
used to be built after both. So an unreachable endpoint or a rejected token
|
|
96
|
+
raised before the stats object existed, `_index` had nothing to print, and
|
|
97
|
+
the run that most needed a destination reported none — the exact gap the
|
|
98
|
+
"a failed run still identifies its graph" guarantee was added to close.
|
|
99
|
+
"""
|
|
100
|
+
import subprocess
|
|
101
|
+
|
|
102
|
+
from witan_code import config as cfg_module
|
|
103
|
+
from witan_code.graph import OmnigraphClient
|
|
104
|
+
|
|
105
|
+
monkeypatch.setenv("WITAN_REPO", "https://github.com/test/cg")
|
|
106
|
+
monkeypatch.setenv("WITAN_CODE_DIR", str(tmp_path / "code"))
|
|
107
|
+
src = tmp_path / "repo"
|
|
108
|
+
src.mkdir()
|
|
109
|
+
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=src, check=True)
|
|
110
|
+
(src / "a.py").write_text("def a():\n return 1\n")
|
|
111
|
+
|
|
112
|
+
def _unreachable(self, *a, **k):
|
|
113
|
+
raise RuntimeError("connection refused")
|
|
114
|
+
|
|
115
|
+
monkeypatch.setattr(OmnigraphClient, "ensure_branch", _unreachable)
|
|
116
|
+
|
|
117
|
+
with pytest.raises(indexer.IndexFailed) as excinfo:
|
|
118
|
+
indexer.index_path(src, config=cfg_module.load())
|
|
119
|
+
|
|
120
|
+
assert excinfo.value.phase == "branch setup"
|
|
121
|
+
assert excinfo.value.stats.store # the destination, not an empty string
|
|
122
|
+
assert "cg" in excinfo.value.stats.store
|
|
@@ -21,6 +21,9 @@ def test_index_extracts_symbols_and_edges(sample_repo):
|
|
|
21
21
|
|
|
22
22
|
slug = repo_mod.detect()
|
|
23
23
|
store = store_mod.store_for_repo(slug, cfg)
|
|
24
|
+
# The destination the summary reports comes off the run itself, so this is
|
|
25
|
+
# what stops that line drifting from where the rows actually landed.
|
|
26
|
+
assert stats.store == str(store)
|
|
24
27
|
client = OmnigraphClient(str(store), cfg.queries_dir)
|
|
25
28
|
|
|
26
29
|
runs = client.read("code_read.gq", "find_by_name", {"name": "run"})
|
|
@@ -184,6 +184,43 @@ def test_the_transport_decides_the_address(monkeypatch, tmp_path):
|
|
|
184
184
|
assert bridge.via_mcp == cfg_module.BRIDGE_GRAPH_ID
|
|
185
185
|
|
|
186
186
|
|
|
187
|
+
def test_label_names_the_graph_a_row_is(monkeypatch, tmp_path):
|
|
188
|
+
"""Every MCP-routed address ends in the same endpoint.
|
|
189
|
+
|
|
190
|
+
Taking a basename of the full address — what `witan-code doctor` did —
|
|
191
|
+
therefore rendered all 14 of production's cluster graphs as `mcp)`, so the
|
|
192
|
+
table could not say which one was broken in exactly the configuration a
|
|
193
|
+
deployed target uses.
|
|
194
|
+
"""
|
|
195
|
+
monkeypatch.setenv("WITAN_CODE_DIR", str(tmp_path / "code"))
|
|
196
|
+
monkeypatch.setenv("WITAN_CODE_TRANSPORT", cfg_module.CODE_TRANSPORT_MCP)
|
|
197
|
+
monkeypatch.setenv("WITAN_REMOTE_URL", "https://witan.example.org/mcp")
|
|
198
|
+
monkeypatch.setenv("WITAN_OIDC_ISSUER", "https://sso.example.org/realms/ol")
|
|
199
|
+
|
|
200
|
+
ref = store_module.store_for_repo(REPO, cfg_module.load())
|
|
201
|
+
assert ref.label == "test/cg"
|
|
202
|
+
assert str(ref).endswith("(via https://witan.example.org/mcp)")
|
|
203
|
+
assert store_module.bridge_store(cfg_module.load()).label == (
|
|
204
|
+
cfg_module.BRIDGE_GRAPH_ID
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_label_falls_back_to_the_store_name_off_the_mcp_path(monkeypatch, tmp_path):
|
|
209
|
+
"""A local store and a directly-addressed cluster graph keep their own names."""
|
|
210
|
+
monkeypatch.setenv("WITAN_CODE_DIR", str(tmp_path / "code"))
|
|
211
|
+
monkeypatch.delenv("WITAN_CODE_TRANSPORT", raising=False)
|
|
212
|
+
monkeypatch.delenv("WITAN_CODE_SERVER", raising=False)
|
|
213
|
+
monkeypatch.delenv("WITAN_REMOTE_URL", raising=False)
|
|
214
|
+
|
|
215
|
+
local = store_module.store_for_repo(REPO, cfg_module.load())
|
|
216
|
+
assert local.label.endswith(".omni")
|
|
217
|
+
assert "/" not in local.label
|
|
218
|
+
|
|
219
|
+
monkeypatch.setenv("WITAN_CODE_SERVER", "http://omnigraph:8080")
|
|
220
|
+
direct = store_module.store_for_repo(REPO, cfg_module.load())
|
|
221
|
+
assert direct.label == direct.graph_id
|
|
222
|
+
|
|
223
|
+
|
|
187
224
|
def test_the_endpoint_comes_from_the_config_that_asked_for_it(monkeypatch, tmp_path):
|
|
188
225
|
"""A `Config` from an explicit `load(target=…)` must resolve *that*
|
|
189
226
|
target's endpoint. Re-running target selection would fall back to
|
|
@@ -15,7 +15,6 @@ unconditionally.
|
|
|
15
15
|
|
|
16
16
|
import asyncio
|
|
17
17
|
import inspect
|
|
18
|
-
import os
|
|
19
18
|
import sys
|
|
20
19
|
from pathlib import Path
|
|
21
20
|
from typing import Annotated, Literal
|
|
@@ -50,16 +49,23 @@ def _srv():
|
|
|
50
49
|
``RemoteServerProxy`` when ``WITAN_REMOTE_URL`` (or a matched target's
|
|
51
50
|
``remote_url``) is set. The proxy mirrors the server module's tool surface,
|
|
52
51
|
so every call site is identical either way.
|
|
52
|
+
|
|
53
|
+
Under ``witan code …`` the target comes from witan's launcher, which mounts
|
|
54
|
+
this App without this package's launcher and forwards what it bound into
|
|
55
|
+
:mod:`witan_code.selected_target`. ``witan code index`` writing a code graph
|
|
56
|
+
to whichever deployment the checkout happened to match, while the flag said
|
|
57
|
+
otherwise, is the failure this reaches back to prevent.
|
|
53
58
|
"""
|
|
54
59
|
global _server
|
|
55
60
|
if _server is None:
|
|
56
61
|
from . import config as cfg_module
|
|
62
|
+
from .selected_target import selected_target
|
|
57
63
|
|
|
58
64
|
# A misconfigured remote (e.g. WITAN_REMOTE_URL without
|
|
59
65
|
# WITAN_OIDC_ISSUER) raises ValueError here; surface it as a clean CLI
|
|
60
66
|
# error rather than letting a traceback escape every read command.
|
|
61
67
|
try:
|
|
62
|
-
remote = cfg_module.load_remote_config()
|
|
68
|
+
remote = cfg_module.load_remote_config(selected_target())
|
|
63
69
|
except ValueError as exc:
|
|
64
70
|
print(exc)
|
|
65
71
|
raise SystemExit(1) from None
|
|
@@ -272,12 +278,15 @@ def doctor() -> None:
|
|
|
272
278
|
report = _fn(server_module.code_store_health)()
|
|
273
279
|
rows = [
|
|
274
280
|
{
|
|
275
|
-
#
|
|
276
|
-
# repo>.omni` and
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
|
|
281
|
+
# The short form, not the address: a local store path is
|
|
282
|
+
# `<code_dir>/<sanitized repo>.omni` and a cluster graph's address
|
|
283
|
+
# ends in the endpoint, so in both cases the shared part is
|
|
284
|
+
# identical on every row — printed in full it wraps each store onto
|
|
285
|
+
# three lines and buries the status column this table exists for.
|
|
286
|
+
# Computed by StoreRef.label rather than here, because the three
|
|
287
|
+
# address forms are that type's business and taking a basename of
|
|
288
|
+
# the MCP one silently collapsed all 14 rows to `mcp)`.
|
|
289
|
+
"store": s["label"],
|
|
281
290
|
"kind": s["kind"],
|
|
282
291
|
"status": "ok"
|
|
283
292
|
if s["ok"]
|
|
@@ -927,6 +936,17 @@ def setup(
|
|
|
927
936
|
|
|
928
937
|
|
|
929
938
|
def _print_summary(action: str, path: Path, stats: indexer.IndexStats) -> None:
|
|
939
|
+
# ★ WHERE THE SYMBOLS WENT, UNCONDITIONALLY. Memory and code graphs are
|
|
940
|
+
# routed by separate settings, so a target whose memory is deployed can
|
|
941
|
+
# still index onto this laptop — and this command, whose entire output is
|
|
942
|
+
# about where symbols went, used to be silent about which graph it wrote.
|
|
943
|
+
# Printed for local and shared alike: a line that appears only in the bad
|
|
944
|
+
# case is a line nobody has learned to look for.
|
|
945
|
+
if stats.store:
|
|
946
|
+
print(
|
|
947
|
+
f"store: {stats.store}"
|
|
948
|
+
+ (f" branch={stats.branch}" if stats.branch else "")
|
|
949
|
+
)
|
|
930
950
|
print(
|
|
931
951
|
f"{action} {path}: "
|
|
932
952
|
f"scanned={stats.scanned} indexed={stats.indexed} "
|
|
@@ -1078,9 +1098,10 @@ def repos() -> None:
|
|
|
1078
1098
|
|
|
1079
1099
|
def _remote_or_exit():
|
|
1080
1100
|
from . import config as cfg_module
|
|
1101
|
+
from .selected_target import selected_target
|
|
1081
1102
|
|
|
1082
1103
|
try:
|
|
1083
|
-
remote = cfg_module.load_remote_config()
|
|
1104
|
+
remote = cfg_module.load_remote_config(selected_target())
|
|
1084
1105
|
except ValueError as exc:
|
|
1085
1106
|
print(exc)
|
|
1086
1107
|
raise SystemExit(1) from None
|
|
@@ -303,6 +303,20 @@ class IndexStats:
|
|
|
303
303
|
directory). Full-repo runs only; reported so a mass cleanup is visible
|
|
304
304
|
rather than silent."""
|
|
305
305
|
|
|
306
|
+
store: str = ""
|
|
307
|
+
"""Where the run wrote — a local directory, or the shared graph's address.
|
|
308
|
+
|
|
309
|
+
``witan code index`` is the command whose whole output is about where
|
|
310
|
+
symbols went, and it happily wrote to a laptop under a deployed target
|
|
311
|
+
without comment. Carried on the stats rather than recomputed by the
|
|
312
|
+
printer so a FAILED run reports the same destination a successful one
|
|
313
|
+
does — that is precisely the run where "which graph was this?" is the
|
|
314
|
+
open question.
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
branch: str = ""
|
|
318
|
+
"""The branch view written, or empty on the repo's default branch."""
|
|
319
|
+
|
|
306
320
|
|
|
307
321
|
class IndexFailed(RuntimeError):
|
|
308
322
|
"""A write phase of :func:`index_path` failed, carrying what it knows.
|
|
@@ -397,22 +411,36 @@ def index_path(
|
|
|
397
411
|
branch = views.repo_view(git_branch, actor=actor) if git_branch else None
|
|
398
412
|
|
|
399
413
|
store = ensure_store(slug, cfg)
|
|
414
|
+
# Built HERE, before the first remote call, not after the setup reads
|
|
415
|
+
# below. The guarantee is that a failed run still says which graph it was
|
|
416
|
+
# writing to, and the setup calls are remote — an unreachable endpoint or a
|
|
417
|
+
# rejected token raised before `stats` existed, so `_index` had nothing to
|
|
418
|
+
# print and the run that most needed a destination reported none.
|
|
419
|
+
stats = IndexStats(store=str(store), branch=branch or "")
|
|
420
|
+
|
|
400
421
|
client = store.client(cfg, branch=branch)
|
|
422
|
+
# Deliberately OUTSIDE the phases below. This raises
|
|
423
|
+
# SharedGraphWriteRefused, which is a clean, self-explanatory refusal with
|
|
424
|
+
# its own message — not the unattributed failure the phases exist to
|
|
425
|
+
# annotate — and callers assert on that type directly
|
|
426
|
+
# (tests/test_indexer_scope.py). Wrapping it would rename the exception for
|
|
427
|
+
# no diagnostic gain.
|
|
401
428
|
check_writable(
|
|
402
429
|
is_remote=client.is_remote, branch=branch, cfg=cfg, slug=slug, actor=actor
|
|
403
430
|
)
|
|
404
431
|
# The hash read below never forks; create the branch before reading.
|
|
405
|
-
|
|
432
|
+
with _write_phase("branch setup", stats, branch=branch or ""):
|
|
433
|
+
client.ensure_branch()
|
|
406
434
|
|
|
407
435
|
# One query for all existing file hashes → the incremental skip check is
|
|
408
436
|
# in-memory, not a query per file. Read even under `force` (where the
|
|
409
437
|
# hashes go unused): a full-repo run also needs the stored file set to
|
|
410
438
|
# find rows for files that are no longer part of the repo.
|
|
411
439
|
existing: dict[str, str] = {}
|
|
412
|
-
|
|
413
|
-
|
|
440
|
+
with _write_phase("existing-hash read", stats):
|
|
441
|
+
for row in client.read("code_read.gq", "all_file_hashes", {}):
|
|
442
|
+
existing[row["slug"]] = row.get("content_hash")
|
|
414
443
|
|
|
415
|
-
stats = IndexStats()
|
|
416
444
|
records: list[dict] = []
|
|
417
445
|
reindexed_file_ids: list[str] = []
|
|
418
446
|
bindings: list[ParsedBinding] = []
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The ``--target`` in effect for this process.
|
|
2
|
+
|
|
3
|
+
The twin of :mod:`witan.cli.selected_target`, and duplicated for the same
|
|
4
|
+
reason :mod:`witan_code.output` is: ``witan code …`` mounts this package's
|
|
5
|
+
cyclopts App but **not** its meta launcher, so witan's launcher is the one that
|
|
6
|
+
runs and it forwards what it bound into here. Two modules rather than one
|
|
7
|
+
because witan depends on witan-code, not the other way round.
|
|
8
|
+
|
|
9
|
+
Left ``None`` when this package's own binary is driven directly
|
|
10
|
+
(``witan-code index``): that launcher declares no ``--target``, so resolution
|
|
11
|
+
falls through to ``WITAN_TARGET`` and the checkout's ``match_*`` rules exactly
|
|
12
|
+
as it did before. Adding the option there is a separate change.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
_selected: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def set_selected_target(name: str | None) -> None:
|
|
21
|
+
"""Record the ``--target`` the calling launcher bound, if any."""
|
|
22
|
+
global _selected
|
|
23
|
+
_selected = name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def selected_target() -> str | None:
|
|
27
|
+
"""The ``--target`` given on this command line, else ``None``."""
|
|
28
|
+
return _selected
|
|
@@ -1059,8 +1059,11 @@ def code_store_health() -> dict:
|
|
|
1059
1059
|
fail while every per-repo listing still looks healthy — which is how it
|
|
1060
1060
|
stayed broken for six weeks.
|
|
1061
1061
|
|
|
1062
|
-
Returns ``{"stores": [{store, kind, ok, files, error,
|
|
1063
|
-
"ok": bool, "stale_schema": [store, ...]}``. ``
|
|
1062
|
+
Returns ``{"stores": [{store, label, kind, ok, files, error,
|
|
1063
|
+
stale_schema}], "ok": bool, "stale_schema": [store, ...]}``. ``store`` is
|
|
1064
|
+
the full address; ``label`` is the short form that identifies WHICH graph a
|
|
1065
|
+
row is, which for a cluster graph the address does not (it ends in the
|
|
1066
|
+
endpoint, identical on every row). ``stale_schema`` names the
|
|
1064
1067
|
stores written by an omnigraph whose on-disk format the installed binary
|
|
1065
1068
|
no longer reads — the one failure with a known remedy, since a code graph
|
|
1066
1069
|
is derived from its checkout and is rebuilt by reindexing
|
|
@@ -1071,6 +1074,10 @@ def code_store_health() -> dict:
|
|
|
1071
1074
|
"stores": [
|
|
1072
1075
|
{
|
|
1073
1076
|
"store": str(h.ref),
|
|
1077
|
+
# The full `store` address is the same endpoint on every row
|
|
1078
|
+
# under a deployed target, so it cannot be used as the column
|
|
1079
|
+
# that tells the rows apart. See StoreRef.label.
|
|
1080
|
+
"label": h.ref.label,
|
|
1074
1081
|
"kind": "bridge" if h.is_bridge else "repo",
|
|
1075
1082
|
"ok": h.ok,
|
|
1076
1083
|
"files": h.files,
|
|
@@ -41,6 +41,7 @@ from witan_core.omnigraph import (
|
|
|
41
41
|
|
|
42
42
|
from . import config as cfg_module
|
|
43
43
|
from .graph import OmnigraphClient, is_stale_schema
|
|
44
|
+
from .visualize import short_repo
|
|
44
45
|
|
|
45
46
|
# Graph registration changes only when provisioning does (a Pulumi deploy), but
|
|
46
47
|
# `_client_for_repo` asks "does this store exist" on every MCP tool call. One
|
|
@@ -143,6 +144,27 @@ class StoreRef:
|
|
|
143
144
|
return f"{self.uri} (graph {self.graph_id})"
|
|
144
145
|
return self.uri
|
|
145
146
|
|
|
147
|
+
@property
|
|
148
|
+
def label(self) -> str:
|
|
149
|
+
"""Short identifier for a table column — which graph, not where it is.
|
|
150
|
+
|
|
151
|
+
:meth:`__str__` is the full address and is what a refusal should print.
|
|
152
|
+
This is the version that fits in a column: the part that DIFFERS
|
|
153
|
+
between rows, with the endpoint or directory prefix that is identical
|
|
154
|
+
on every row dropped.
|
|
155
|
+
|
|
156
|
+
Taking ``os.path.basename`` of the full address instead is right only
|
|
157
|
+
for a local store. An MCP-routed address ends in the endpoint URL, so
|
|
158
|
+
every cluster graph in a listing came out as the same ``mcp)`` — which
|
|
159
|
+
made ``witan-code doctor`` unable to name which graph was broken in
|
|
160
|
+
exactly the configuration a deployed target now uses.
|
|
161
|
+
"""
|
|
162
|
+
if self.via_mcp:
|
|
163
|
+
return short_repo(self.via_mcp)
|
|
164
|
+
if self.is_remote:
|
|
165
|
+
return self.graph_id or self.uri
|
|
166
|
+
return Path(self.uri).name
|
|
167
|
+
|
|
146
168
|
def client(
|
|
147
169
|
self,
|
|
148
170
|
config: cfg_module.Config | None = None,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|