ph-code-graph 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- ph_code_graph/__init__.py +48 -3
- ph_code_graph/_extract.py +7 -5
- ph_code_graph/_store.py +40 -8
- {ph_code_graph-0.2.0.dist-info → ph_code_graph-0.4.0.dist-info}/METADATA +2 -2
- ph_code_graph-0.4.0.dist-info/RECORD +11 -0
- {ph_code_graph-0.2.0.dist-info → ph_code_graph-0.4.0.dist-info}/WHEEL +1 -1
- ph_code_graph-0.2.0.dist-info/RECORD +0 -11
- {ph_code_graph-0.2.0.dist-info → ph_code_graph-0.4.0.dist-info}/entry_points.txt +0 -0
- {ph_code_graph-0.2.0.dist-info → ph_code_graph-0.4.0.dist-info}/licenses/LICENSE +0 -0
ph_code_graph/__init__.py
CHANGED
|
@@ -42,7 +42,9 @@ answers most of what an agent actually asks and reports where it cannot.
|
|
|
42
42
|
from __future__ import annotations
|
|
43
43
|
|
|
44
44
|
import hashlib
|
|
45
|
+
import importlib.util
|
|
45
46
|
import logging
|
|
47
|
+
from collections.abc import Mapping
|
|
46
48
|
from dataclasses import dataclass, field
|
|
47
49
|
from pathlib import Path
|
|
48
50
|
from typing import Any, Literal
|
|
@@ -59,6 +61,7 @@ from ph.seams._registry import contribute_item
|
|
|
59
61
|
from ph.seams.changes import tree_state
|
|
60
62
|
from ph.seams.commands import CommandContext, CommandDefinition
|
|
61
63
|
from ph.seams.diagnostics import Diagnostic, contribute
|
|
64
|
+
from ph.seams.fs import FsService
|
|
62
65
|
from ph.seams.skills import discover_skills
|
|
63
66
|
from ph.text import count_of
|
|
64
67
|
from ph.tools.definition import ToolModel, ToolOutput, ToolRunContext, define_tool, text_content
|
|
@@ -196,7 +199,8 @@ class CodeGraphSeam:
|
|
|
196
199
|
|
|
197
200
|
def report(self) -> list[tuple[str, str]]:
|
|
198
201
|
"""`phern doctor`'s section."""
|
|
199
|
-
|
|
202
|
+
# Not a declared dependency: the row checks for it before this runs.
|
|
203
|
+
import tree_sitter # noqa: PLC0415
|
|
200
204
|
|
|
201
205
|
# No `hasattr` guard: the row declares `inject=[FS]`, so `ctx.fs` is
|
|
202
206
|
# present for as long as it is active — and a `Path.cwd()` fallback
|
|
@@ -492,8 +496,6 @@ def offer_skills(ctx: Context) -> None:
|
|
|
492
496
|
@plugin("code-graph", inject=[TOOLS, FS], config=Config)
|
|
493
497
|
async def apply(ctx: Context, config: Config) -> None:
|
|
494
498
|
"""Mount the seam and register both tools."""
|
|
495
|
-
import importlib.util
|
|
496
|
-
|
|
497
499
|
for module in ("tree_sitter", "tree_sitter_language_pack"):
|
|
498
500
|
if importlib.util.find_spec(module) is None:
|
|
499
501
|
raise MountRefusal(MISSING)
|
|
@@ -535,6 +537,16 @@ async def apply(ctx: Context, config: Config) -> None:
|
|
|
535
537
|
def store(run: ToolRunContext) -> CodeGraphStore:
|
|
536
538
|
return seam.store_for(ctx.require(FS).root_for(run.agent))
|
|
537
539
|
|
|
540
|
+
def _forget_missing(
|
|
541
|
+
fs: FsService,
|
|
542
|
+
book: CodeGraphStore,
|
|
543
|
+
known: Mapping[str, tuple[str, str]],
|
|
544
|
+
run: ToolRunContext,
|
|
545
|
+
) -> int:
|
|
546
|
+
"""Drop every indexed path that is no longer on disk. One thread hop."""
|
|
547
|
+
gone = [path for path in known if not fs.resolve(path, agent=run.agent).exists()]
|
|
548
|
+
return book.forget(gone) if gone else 0
|
|
549
|
+
|
|
538
550
|
async def index_tool(args: IndexArgs, run: ToolRunContext) -> dict[str, Any]:
|
|
539
551
|
fs = ctx.require(FS)
|
|
540
552
|
book = store(run)
|
|
@@ -616,10 +628,24 @@ async def apply(ctx: Context, config: Config) -> None:
|
|
|
616
628
|
slice_ = await fs.read(
|
|
617
629
|
path,
|
|
618
630
|
limit=None,
|
|
631
|
+
# `skip_reason` above is this walk's bound, and it is a skip
|
|
632
|
+
# rather than a raise on purpose: an indexing pass must not
|
|
633
|
+
# fail because it found a minified bundle. `read`'s own
|
|
634
|
+
# default would be a second, larger bound that raises.
|
|
635
|
+
max_bytes=None,
|
|
619
636
|
scope=run.scope,
|
|
620
637
|
agent=run.agent,
|
|
621
638
|
session=run.session,
|
|
622
639
|
)
|
|
640
|
+
# **No prose test here, deliberately** (X6 review). The text
|
|
641
|
+
# index refuses documents that hold no prose; source code is not
|
|
642
|
+
# prose, and the same predicate refused a module whose only
|
|
643
|
+
# statement is a long `__all__`, and a `TABLE = {...}` literal
|
|
644
|
+
# beside `def lookup()` — so `lookup` would have vanished from
|
|
645
|
+
# this index with nothing but a skip line to say so. Telling a
|
|
646
|
+
# minified bundle from a legitimate data module by shape is not
|
|
647
|
+
# reliable for code, and a false positive here loses symbols
|
|
648
|
+
# silently. `skip_reason`'s size bound stays this walk's guard.
|
|
623
649
|
# The content hash stays the authority on *whether* a file
|
|
624
650
|
# changed — the filter above only decides whether to open it, so
|
|
625
651
|
# the "content, not clock" guarantee is untouched.
|
|
@@ -642,6 +668,25 @@ async def apply(ctx: Context, config: Config) -> None:
|
|
|
642
668
|
book.put, path, digest, extraction, state.id_for(path)
|
|
643
669
|
)
|
|
644
670
|
indexed += 1
|
|
671
|
+
if not args.forget:
|
|
672
|
+
# **Gone from disk is gone from the index** (X3). A deleted file
|
|
673
|
+
# kept its symbols until somebody ran `forget` by hand, so
|
|
674
|
+
# `code_graph` went on naming definitions at paths that are not
|
|
675
|
+
# there — and a rename showed the symbol twice, once under each
|
|
676
|
+
# name, with nothing saying which was real. A pointer into a file
|
|
677
|
+
# that does not exist is the one answer this tool must not give,
|
|
678
|
+
# because the model's next move is to open it.
|
|
679
|
+
#
|
|
680
|
+
# **Asked of the filesystem, not of the walk.** "Absent from
|
|
681
|
+
# `paths`" looks like the same question and is not: `collect`
|
|
682
|
+
# stops at `max_files` and reports nothing, and the policy screen
|
|
683
|
+
# prunes whole directories — so on a tree past the cap, or with a
|
|
684
|
+
# `permissions-fs` rule hiding a subtree, the walk is a *subset*
|
|
685
|
+
# of the scope and differencing against it would forget
|
|
686
|
+
# everything it did not reach. It also settles the narrowed walk
|
|
687
|
+
# for free, where the question is not "did we cover the scope"
|
|
688
|
+
# but "is this file still there".
|
|
689
|
+
removed = await anyio.to_thread.run_sync(_forget_missing, fs, book, known, run)
|
|
645
690
|
# Stored last, and only after the loop: a token recorded before the
|
|
646
691
|
# writes would, on a crash between the two, vouch for files this run
|
|
647
692
|
# never actually indexed.
|
ph_code_graph/_extract.py
CHANGED
|
@@ -40,6 +40,7 @@ query alone yields two tags and no calls — measured. `INHERITS` is that fact.
|
|
|
40
40
|
from __future__ import annotations
|
|
41
41
|
|
|
42
42
|
import logging
|
|
43
|
+
import os
|
|
43
44
|
from collections.abc import Callable, Sequence
|
|
44
45
|
from dataclasses import dataclass, replace
|
|
45
46
|
from functools import cache
|
|
@@ -129,7 +130,8 @@ class Extraction:
|
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
def _pack() -> Any: # noqa: ANN401
|
|
132
|
-
|
|
133
|
+
# Not a declared dependency: the row checks for it before this runs.
|
|
134
|
+
import tree_sitter_language_pack # noqa: PLC0415
|
|
133
135
|
|
|
134
136
|
return tree_sitter_language_pack
|
|
135
137
|
|
|
@@ -196,8 +198,6 @@ def use_cache(directory: Path) -> Path:
|
|
|
196
198
|
An operator who set the environment variable deliberately keeps it — their
|
|
197
199
|
spelling wins, which is what makes the variable still mean something.
|
|
198
200
|
"""
|
|
199
|
-
import os
|
|
200
|
-
|
|
201
201
|
global _configured
|
|
202
202
|
|
|
203
203
|
override = os.environ.get("TREE_SITTER_LANGUAGE_PACK_CACHE_DIR")
|
|
@@ -343,7 +343,8 @@ def _tags_query(language: str) -> Any: # noqa: ANN401
|
|
|
343
343
|
for the same reason: a `Query` is read-only once built, and `extract` makes
|
|
344
344
|
a fresh `QueryCursor` per call — which is the mutable half.
|
|
345
345
|
"""
|
|
346
|
-
|
|
346
|
+
# Not a declared dependency: the row checks for it before this runs.
|
|
347
|
+
from tree_sitter import Query # noqa: PLC0415
|
|
347
348
|
|
|
348
349
|
pack = _pack()
|
|
349
350
|
sources = [pack.get_tags_query(one) for one in INHERITS.get(language, ())]
|
|
@@ -359,7 +360,8 @@ def extract(path: str, text: str, language: str) -> Extraction:
|
|
|
359
360
|
should stop the batch or be reported and skipped is its decision, not
|
|
360
361
|
this function's.
|
|
361
362
|
"""
|
|
362
|
-
|
|
363
|
+
# Not a declared dependency: the row checks for it before this runs.
|
|
364
|
+
from tree_sitter import QueryCursor # noqa: PLC0415
|
|
363
365
|
|
|
364
366
|
pack = _pack()
|
|
365
367
|
config = pack.ProcessConfig(
|
ph_code_graph/_store.py
CHANGED
|
@@ -213,6 +213,22 @@ class Hit:
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
|
|
216
|
+
def _fts_values(rowid: int, name: str, doc: str | None, path: str) -> tuple[int, str, str, str]:
|
|
217
|
+
"""One `symbols_fts` row, for the insert **and** the delete that undoes it.
|
|
218
|
+
|
|
219
|
+
FTS5's `'delete'` command re-derives the postings to remove from the values
|
|
220
|
+
it is given, so a delete whose tuple differs from the insert's by so much as
|
|
221
|
+
one NULL removes nothing — and SQLite reports no error for it. `_forget`
|
|
222
|
+
carried a copy of this expression with a comment saying it mirrored the
|
|
223
|
+
insert, which is a reader being asked to check two expressions in two
|
|
224
|
+
methods rather than a compiler being asked to.
|
|
225
|
+
|
|
226
|
+
`doc or ""` is the whole substance: `symbols.doc` is nullable and the FTS
|
|
227
|
+
column is not.
|
|
228
|
+
"""
|
|
229
|
+
return (rowid, name, doc or "", path)
|
|
230
|
+
|
|
231
|
+
|
|
216
232
|
@dataclass(slots=True)
|
|
217
233
|
class CodeGraphStore:
|
|
218
234
|
"""The index. **Blocking**; the seam calls it in a worker thread."""
|
|
@@ -361,17 +377,33 @@ class CodeGraphStore:
|
|
|
361
377
|
def _forget(self, connection: sqlite3.Connection, paths: Sequence[str]) -> int:
|
|
362
378
|
gone = 0
|
|
363
379
|
for path in paths:
|
|
364
|
-
# The FTS rows first,
|
|
380
|
+
# The FTS rows first, **with the values they were inserted under**: a
|
|
365
381
|
# contentless table cannot be asked which rows belong to a path, so
|
|
366
|
-
#
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
382
|
+
# they come from `symbols` while it still has them (X1).
|
|
383
|
+
#
|
|
384
|
+
# The columns and not just the rowid, because that is what FTS5's
|
|
385
|
+
# `'delete'` command needs: it re-derives the postings to remove from
|
|
386
|
+
# the values it is given, and three empty strings therefore remove
|
|
387
|
+
# *nothing*. SQLite reports no error for it. Every posting of every
|
|
388
|
+
# re-indexed symbol stayed, and `symbols.id` has no `AUTOINCREMENT` —
|
|
389
|
+
# so the next insert took the dead row's id and the stale posting
|
|
390
|
+
# started pointing at a different symbol. `search` joins the rowid
|
|
391
|
+
# back to `symbols`, so the old name returned the new symbol: worse
|
|
392
|
+
# than a miss, because it reads like an answer.
|
|
393
|
+
#
|
|
394
|
+
# Both tuples come from `_fts_values`, so the agreement the paragraph
|
|
395
|
+
# above depends on is structural rather than a comment asking a
|
|
396
|
+
# reader to check two expressions in two methods.
|
|
397
|
+
stale = [
|
|
398
|
+
_fts_values(row["id"], row["name"], row["doc"], row["path"])
|
|
399
|
+
for row in connection.execute(
|
|
400
|
+
"SELECT id, name, doc, path FROM symbols WHERE path = ?", (path,)
|
|
401
|
+
)
|
|
370
402
|
]
|
|
371
403
|
connection.executemany(
|
|
372
404
|
"INSERT INTO symbols_fts(symbols_fts, rowid, name, doc, path) "
|
|
373
|
-
"VALUES ('delete', ?,
|
|
374
|
-
|
|
405
|
+
"VALUES ('delete', ?, ?, ?, ?)",
|
|
406
|
+
stale,
|
|
375
407
|
)
|
|
376
408
|
connection.execute("DELETE FROM symbols WHERE path = ?", (path,))
|
|
377
409
|
connection.execute("DELETE FROM refs WHERE path = ?", (path,))
|
|
@@ -420,7 +452,7 @@ class CodeGraphStore:
|
|
|
420
452
|
by_line[definition.start_line] = identifier
|
|
421
453
|
connection.execute(
|
|
422
454
|
"INSERT INTO symbols_fts(rowid, name, doc, path) VALUES (?, ?, ?, ?)",
|
|
423
|
-
(identifier, definition.name, definition.doc
|
|
455
|
+
_fts_values(identifier, definition.name, definition.doc, path),
|
|
424
456
|
)
|
|
425
457
|
# One `{line: tightest definition}` table for the whole file, rather
|
|
426
458
|
# than a scan of every definition per reference: that was
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ph-code-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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.
|
|
24
|
+
Requires-Dist: ph-core==0.4.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
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ph_code_graph/__init__.py,sha256=Cw9l34LPyR7LjinpIIZfjrWn0i8cw8IXwdSpGTJHgVE,38079
|
|
2
|
+
ph_code_graph/_extract.py,sha256=9AprYeNCqIQRZOv5RWtxTQFbU6jlSlPo7ot2fkAcavg,24020
|
|
3
|
+
ph_code_graph/_store.py,sha256=zcxMXE6_DM5DWggQ2iMD_-ds3DfPq0oz4t0O55hyhUM,28785
|
|
4
|
+
ph_code_graph/bundle.yaml,sha256=x3UoJbgQ6QUootM-M6rWZpDQT1sRGSCrtiBh2LFq1ms,1190
|
|
5
|
+
ph_code_graph/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
ph_code_graph/skills/code-graph/SKILL.md,sha256=VNIs0a0U2iveu60E1tQHxz_nRRHOUUHmg9b5_saOMNs,2950
|
|
7
|
+
ph_code_graph-0.4.0.dist-info/METADATA,sha256=Y7RyUptibqyGGbMByzIU4NEhpsMGSg2XS5jxigREeP8,13501
|
|
8
|
+
ph_code_graph-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
ph_code_graph-0.4.0.dist-info/entry_points.txt,sha256=4DPMS_RTRniZPXb59juSl5mpTpi0fHpwViZPCvffAgo,94
|
|
10
|
+
ph_code_graph-0.4.0.dist-info/licenses/LICENSE,sha256=sN3yhtK25eQpVcewQbaq1k4A-_fWl3RkTf3QfGDh3ko,1070
|
|
11
|
+
ph_code_graph-0.4.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
ph_code_graph/__init__.py,sha256=SbIRwgUpurM8tGGYS7VYK2HQo4LcDGGaNR5otTXMD-s,35101
|
|
2
|
-
ph_code_graph/_extract.py,sha256=3zBEy3EibEMy4636w3o68lcwNb_wnXXWxXemuhO5WiY,23755
|
|
3
|
-
ph_code_graph/_store.py,sha256=5ZPXvlW_Vt1U-c0ag6fgYBaDp2PVrzPGTI_aPcOFOuU,26964
|
|
4
|
-
ph_code_graph/bundle.yaml,sha256=x3UoJbgQ6QUootM-M6rWZpDQT1sRGSCrtiBh2LFq1ms,1190
|
|
5
|
-
ph_code_graph/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
-
ph_code_graph/skills/code-graph/SKILL.md,sha256=VNIs0a0U2iveu60E1tQHxz_nRRHOUUHmg9b5_saOMNs,2950
|
|
7
|
-
ph_code_graph-0.2.0.dist-info/METADATA,sha256=xfEy_jtMardrhV8IRKIzo5jz6t9P9_Ua86sFoFZPhi0,13501
|
|
8
|
-
ph_code_graph-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
9
|
-
ph_code_graph-0.2.0.dist-info/entry_points.txt,sha256=4DPMS_RTRniZPXb59juSl5mpTpi0fHpwViZPCvffAgo,94
|
|
10
|
-
ph_code_graph-0.2.0.dist-info/licenses/LICENSE,sha256=sN3yhtK25eQpVcewQbaq1k4A-_fWl3RkTf3QfGDh3ko,1070
|
|
11
|
-
ph_code_graph-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|