ph-code-graph 0.4.0__tar.gz → 0.6.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ph-code-graph
3
- Version: 0.4.0
3
+ Version: 0.6.0
4
4
  Summary: pH plugin: `code_index` and `code_graph`, a Python-native tree-sitter code graph with line-accurate answers.
5
5
  Project-URL: Homepage, https://github.com/chastabor/pH
6
6
  Project-URL: Repository, https://github.com/chastabor/pH
@@ -21,7 +21,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
21
  Classifier: Topic :: Software Development
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.12
24
- Requires-Dist: ph-core==0.4.0
24
+ Requires-Dist: ph-core==0.6.0
25
25
  Requires-Dist: tree-sitter-language-pack>=1.16
26
26
  Requires-Dist: tree-sitter>=0.25
27
27
  Description-Content-Type: text/markdown
@@ -206,10 +206,12 @@ would make `callers` return every place that merely *mentions* a type.
206
206
 
207
207
  ## Where things live
208
208
 
209
- - **the index**: `$PH_CACHE/code-graph/<digest of the workspace root>.db` — the
210
- cache root because it is rebuildable from the source, and keyed by root so two
211
- checkouts do not answer each other's questions. `path:` in the row's config
212
- overrides it.
209
+ - **the index**: `$PH_CACHE/code-graph/<digest of the workspace root>/graph.db` —
210
+ the cache root because it is rebuildable from the source, and keyed by root so
211
+ two checkouts do not answer each other's questions. The directory records its
212
+ workspace, and one whose workspace has been gone a week is removed on the next
213
+ index, so a profile that runs children in worktrees does not leave a database
214
+ per tree forever. `path:` in the row's config names one database instead.
213
215
  - **the grammars**: `$PH_CACHE/tree-sitter`, set by the row at mount.
214
216
 
215
217
  That second one is not housekeeping. The pack materializes even its *bundled*
@@ -178,10 +178,12 @@ would make `callers` return every place that merely *mentions* a type.
178
178
 
179
179
  ## Where things live
180
180
 
181
- - **the index**: `$PH_CACHE/code-graph/<digest of the workspace root>.db` — the
182
- cache root because it is rebuildable from the source, and keyed by root so two
183
- checkouts do not answer each other's questions. `path:` in the row's config
184
- overrides it.
181
+ - **the index**: `$PH_CACHE/code-graph/<digest of the workspace root>/graph.db` —
182
+ the cache root because it is rebuildable from the source, and keyed by root so
183
+ two checkouts do not answer each other's questions. The directory records its
184
+ workspace, and one whose workspace has been gone a week is removed on the next
185
+ index, so a profile that runs children in worktrees does not leave a database
186
+ per tree forever. `path:` in the row's config names one database instead.
185
187
  - **the grammars**: `$PH_CACHE/tree-sitter`, set by the row at mount.
186
188
 
187
189
  That second one is not housekeeping. The pack materializes even its *bundled*
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ph-code-graph"
7
- version = "0.4.0"
7
+ version = "0.6.0"
8
8
  description = "pH plugin: `code_index` and `code_graph`, a Python-native tree-sitter code graph with line-accurate answers."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -29,7 +29,7 @@ classifiers = [
29
29
  # in (371 available, the rest fetched on demand — which this row never does
30
30
  # implicitly; see `_extract.indexable`). No Rust toolchain, no Node, no
31
31
  # submodule: the graph itself is stdlib `sqlite3`.
32
- dependencies = ["ph-core==0.4.0", "tree-sitter>=0.25", "tree-sitter-language-pack>=1.16"]
32
+ dependencies = ["ph-core==0.6.0", "tree-sitter>=0.25", "tree-sitter-language-pack>=1.16"]
33
33
 
34
34
  [project.urls]
35
35
  Homepage = "https://github.com/chastabor/pH"
@@ -41,7 +41,6 @@ answers most of what an agent actually asks and reports where it cannot.
41
41
 
42
42
  from __future__ import annotations
43
43
 
44
- import hashlib
45
44
  import importlib.util
46
45
  import logging
47
46
  from collections.abc import Mapping
@@ -59,7 +58,7 @@ from ph.llm.types import ContentBlock
59
58
  from ph.paths import default_cache_path, resolve_roots
60
59
  from ph.seams._registry import contribute_item
61
60
  from ph.seams.changes import tree_state
62
- from ph.seams.commands import CommandContext, CommandDefinition
61
+ from ph.seams.commands import Provisioning, install_or_status
63
62
  from ph.seams.diagnostics import Diagnostic, contribute
64
63
  from ph.seams.fs import FsService
65
64
  from ph.seams.skills import discover_skills
@@ -68,6 +67,7 @@ from ph.tools.definition import ToolModel, ToolOutput, ToolRunContext, define_to
68
67
  from ph.tools.errors import HarnessError
69
68
  from ph.tools.presentation import simple_views
70
69
  from ph.wire import WireModel
70
+ from ph.workspace_caches import digest, use
71
71
 
72
72
  from ._extract import (
73
73
  cache_release,
@@ -127,7 +127,8 @@ class Config(WireModel):
127
127
  """Row config for `code-graph`."""
128
128
 
129
129
  path: str = ""
130
- """Where the index lives. Defaults to `$PH_CACHE/code-graph/<root digest>.db`.
130
+ """Where the index lives. Defaults to `$PH_CACHE/code-graph/<root digest>/graph.db`;
131
+ a path set here is one database, used as given.
131
132
 
132
133
  Under the cache root because the index is **rebuildable** — the source is the
133
134
  truth and this is derived, which is the lifecycle `$PH_CACHE` names (Q1).
@@ -178,21 +179,22 @@ class CodeGraphSeam:
178
179
  _lock: anyio.Lock = field(default_factory=anyio.Lock)
179
180
 
180
181
  def store_for(self, root: Path) -> CodeGraphStore:
181
- """This workspace's index. One file per root, and **nothing prunes them.**
182
+ """This workspace's index: a directory per root, or the row's `path` as given.
182
183
 
183
184
  Keyed per root on purpose — a worktree at another revision holds
184
185
  different line numbers, so sharing one index would hand out pointers
185
186
  that are quietly stale. The cost is that under a profile whose children
186
- run in worktrees, every throwaway tree leaves a database behind, and
187
- `phern doctor` names only the current one. Stated here rather than left to
188
- be discovered, which is this codebase's rule for a cache nothing
189
- collects (`ph.seams.uploads` says the same about attachments): deleting
190
- `$PH_CACHE/code-graph` reclaims all of them and costs a re-index.
187
+ run in worktrees, every throwaway tree leaves a database behind, so the
188
+ directory records its workspace and one whose workspace is gone is swept
189
+ on a later index (`ph.workspace_caches`).
191
190
  """
192
- digest = hashlib.sha256(str(root).encode("utf-8")).hexdigest()[:16]
193
- return CodeGraphStore(
194
- path=default_cache_path(self.config.path, "code-graph", f"{digest}.db")
195
- )
191
+ if self.config.path:
192
+ return CodeGraphStore(path=default_cache_path(self.config.path, "code-graph"))
193
+ return CodeGraphStore(path=self.base() / digest(str(root)) / "graph.db")
194
+
195
+ def base(self) -> Path:
196
+ """Where the per-workspace indexes live, when no `path` names one database."""
197
+ return default_cache_path(None, "code-graph")
196
198
 
197
199
  def locked(self) -> anyio.Lock:
198
200
  return self._lock
@@ -493,7 +495,7 @@ def offer_skills(ctx: Context) -> None:
493
495
  contribute_item(ctx, SKILLS, skill, label=f"skill({skill.name})")
494
496
 
495
497
 
496
- @plugin("code-graph", inject=[TOOLS, FS], config=Config)
498
+ @plugin("code-graph", affects="environment", inject=[TOOLS, FS], config=Config)
497
499
  async def apply(ctx: Context, config: Config) -> None:
498
500
  """Mount the seam and register both tools."""
499
501
  for module in ("tree_sitter", "tree_sitter_language_pack"):
@@ -560,6 +562,10 @@ async def apply(ctx: Context, config: Config) -> None:
560
562
  skipped: list[dict[str, str]] = []
561
563
  indexed = unchanged = symbols = removed = 0
562
564
 
565
+ if not config.path:
566
+ # Before the lock: the record and the sweep touch nothing an index
567
+ # holds. Not for a configured `path`, one database the row was given.
568
+ await anyio.to_thread.run_sync(use, seam.base(), fs.root_for(run.agent))
563
569
  async with seam.locked():
564
570
  await anyio.to_thread.run_sync(book.prepare)
565
571
  if args.forget:
@@ -808,7 +814,7 @@ async def apply(ctx: Context, config: Config) -> None:
808
814
  )
809
815
  )
810
816
 
811
- async def install(argument: str, _invocation: CommandContext) -> str:
817
+ async def grammar_command(verb: Provisioning) -> str:
812
818
  """`/code-graph install|status` — make the grammars ready, on purpose.
813
819
 
814
820
  A **command** rather than a tool, per the seam's own rule: a person asks
@@ -817,9 +823,6 @@ async def apply(ctx: Context, config: Config) -> None:
817
823
  the long tail fetches — but "is this ready" should have one answer per
818
824
  plugin, asked the same way.
819
825
  """
820
- verb = argument.strip().lower() or "status"
821
- if verb not in ("install", "status"):
822
- return f"/code-graph takes `install` or `status`, not {argument.strip()!r}."
823
826
  wanted = [one for one in (config.languages or DEFAULT_LANGUAGES) if indexable(one)]
824
827
  if verb == "status":
825
828
  ready, missing = await anyio.to_thread.run_sync(readiness, wanted)
@@ -834,11 +837,10 @@ async def apply(ctx: Context, config: Config) -> None:
834
837
  )
835
838
  return f"all {count_of(len(ready), 'grammar')} ready under {seam.grammars}."
836
839
 
837
- command = CommandDefinition(
838
- name="code-graph",
839
- summary="Make the tree-sitter grammars ready, or report whether they are.",
840
- argument_hint="[install|status]",
841
- run=install,
840
+ command = install_or_status(
841
+ "code-graph",
842
+ "Make the tree-sitter grammars ready, or report whether they are.",
843
+ grammar_command,
842
844
  )
843
845
  contribute_item(
844
846
  ctx,
@@ -47,6 +47,7 @@ from ph.llm.types import text_of
47
47
  from ph.testing import FAKE_OPTIONS, MountProfile, report_section, run_tool
48
48
  from ph.testing.git import git, git_repo
49
49
  from ph.testing.jj import jj_repo
50
+ from ph.workspace_caches import WORKSPACE_FILE
50
51
  from ph_code_graph import CODE_GRAPH
51
52
  from ph_code_graph._extract import (
52
53
  INHERITS,
@@ -309,6 +310,25 @@ async def test_indexing_then_asking_answers_with_a_readable_pointer(
309
310
  assert f"{hit['path']}:{hit['start_line']}" in text_of(found.content)
310
311
 
311
312
 
313
+ async def test_an_index_records_the_workspace_its_graph_serves(
314
+ mount: MountProfile, tmp_path: Path
315
+ ) -> None:
316
+ """What lets a sweep remove the graph of a worktree that is gone: its directory
317
+ names the workspace, and a digest cannot (`ph.workspace_caches`).
318
+
319
+ Sabotage: drop the `use` call from `code_index`, and nothing is recorded.
320
+ """
321
+ _tree(tmp_path)
322
+ ctx = await mount(ROW)
323
+ root = ctx.require(FS).root_for(None)
324
+
325
+ built = await run_tool(ctx, "code_index", {"paths": ["pkg"]}, agent=_agent(ctx))
326
+
327
+ assert not built.is_error, text_of(built.content)
328
+ recorded = ctx.require(CODE_GRAPH).store_for(root).path.parent / WORKSPACE_FILE
329
+ assert recorded.read_text(encoding="utf-8") == str(root)
330
+
331
+
312
332
  async def test_callers_names_the_calling_symbol_and_the_calling_line(
313
333
  mount: MountProfile, tmp_path: Path
314
334
  ) -> None:
@@ -903,7 +923,7 @@ async def test_every_row_in_the_rlm_indexed_profile_activates(
903
923
  the `PROFILES` entry and cannot test it — nothing there depends on the two
904
924
  indexing distributions, which is the point of discovering them as bundles.
905
925
  """
906
- from ph.cordis import Context, Profile, load_profile_documents
926
+ from ph.cordis import Context, Profile, ProfileDocument, load_profile_documents
907
927
  from ph_app.profiles import resolve_profile
908
928
 
909
929
  # `PH_HOME`/`PH_CACHE` are not pinned here: the suite's autouse
@@ -911,7 +931,7 @@ async def test_every_row_in_the_rlm_indexed_profile_activates(
911
931
  # the first comes to be believed and bypassed.
912
932
  documents = load_profile_documents(resolve_profile("rlm-indexed"))
913
933
  documents.append(
914
- (
934
+ ProfileDocument(
915
935
  "test-overlay",
916
936
  [
917
937
  {"id": "fs", "config": {"root": str(tmp_path)}},
File without changes
File without changes