witan-code 0.2.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.
- witan_code/__init__.py +0 -0
- witan_code/__main__.py +4 -0
- witan_code/_detach.py +27 -0
- witan_code/bridge.py +392 -0
- witan_code/bridge_extractors.py +642 -0
- witan_code/cli.py +722 -0
- witan_code/config.py +65 -0
- witan_code/context.py +98 -0
- witan_code/edges.py +171 -0
- witan_code/elicit.py +92 -0
- witan_code/extensions/pi/codegraph.ts +121 -0
- witan_code/graph.py +271 -0
- witan_code/hooks.py +116 -0
- witan_code/indexer.py +1042 -0
- witan_code/maintenance.py +130 -0
- witan_code/package_map.py +74 -0
- witan_code/queries/bridge.gq +193 -0
- witan_code/queries/code_mutations.gq +81 -0
- witan_code/queries/code_read.gq +154 -0
- witan_code/queries/delete.gq +16 -0
- witan_code/queries_ts/bash.scm +9 -0
- witan_code/queries_ts/hcl.scm +16 -0
- witan_code/queries_ts/python.scm +29 -0
- witan_code/queries_ts/sql.scm +20 -0
- witan_code/queries_ts/typescript.scm +51 -0
- witan_code/queries_ts/yaml.scm +5 -0
- witan_code/repo.py +282 -0
- witan_code/schema/bridge-schema.pg +82 -0
- witan_code/schema/code-schema.pg +51 -0
- witan_code/server.py +859 -0
- witan_code/setup.py +230 -0
- witan_code/skills/witan-code/SKILL.md +97 -0
- witan_code/stitch.py +177 -0
- witan_code/store.py +116 -0
- witan_code/visualize.py +347 -0
- witan_code-0.2.0.dist-info/METADATA +476 -0
- witan_code-0.2.0.dist-info/RECORD +39 -0
- witan_code-0.2.0.dist-info/WHEEL +4 -0
- witan_code-0.2.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Store compaction (omnigraph optimize/cleanup) with throttling.
|
|
2
|
+
|
|
3
|
+
Mirrors witan's own ``witan.maintenance`` (deliberately duplicated — no
|
|
4
|
+
cross-package import, see ``graph.py``'s docstring), adapted for witan-code's
|
|
5
|
+
per-repo, multi-store layout: there is no single configured store to
|
|
6
|
+
throttle. The current repo's per-repo store and the shared cross-repo bridge
|
|
7
|
+
store are each compacted and throttled independently, keyed by the store's
|
|
8
|
+
own path, so a bloated bridge store doesn't gate (or get gated by) a repo
|
|
9
|
+
store's compaction.
|
|
10
|
+
|
|
11
|
+
Every witan-code write — the ``PostToolUse`` single-file reindex, the
|
|
12
|
+
``SessionStart`` full index — appends a new tiny Lance fragment + manifest
|
|
13
|
+
version to a store, and left uncompacted it bloats until *opening* the store
|
|
14
|
+
dominates query latency, the same failure mode witan's own store hit (#98).
|
|
15
|
+
``omnigraph optimize`` collapses the fragments (non-destructive); ``cleanup``
|
|
16
|
+
GCs old versions to reclaim disk (destructive).
|
|
17
|
+
|
|
18
|
+
The ``Stop`` hook (``witan-code checkpoint``) calls
|
|
19
|
+
:func:`spawn_background_optimize` for whichever stores exist in the current
|
|
20
|
+
repo — at most once per interval each, detached so the hook returns
|
|
21
|
+
immediately. There is also a ``witan-code optimize`` / ``witan-code cleanup``
|
|
22
|
+
CLI for cron/systemd-timer driven maintenance.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
import time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from ._detach import popen_detached
|
|
37
|
+
|
|
38
|
+
# Opportunistic optimize runs at most once per this window per store. Optimize
|
|
39
|
+
# takes the store's write lock and is ~tens of seconds on a bloated store, so
|
|
40
|
+
# daily is a safe default; override with WITAN_CODE_OPTIMIZE_INTERVAL (seconds;
|
|
41
|
+
# 0 disables).
|
|
42
|
+
_OPTIMIZE_INTERVAL = 24 * 3600.0
|
|
43
|
+
|
|
44
|
+
_REMOTE_PREFIXES = ("http://", "https://", "s3://")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def optimize_interval() -> float:
|
|
48
|
+
"""Throttle window in seconds; ``0`` (or negative) disables auto-optimize."""
|
|
49
|
+
raw = os.environ.get("WITAN_CODE_OPTIMIZE_INTERVAL")
|
|
50
|
+
if raw is None:
|
|
51
|
+
return _OPTIMIZE_INTERVAL
|
|
52
|
+
try:
|
|
53
|
+
return max(0.0, float(raw))
|
|
54
|
+
except ValueError:
|
|
55
|
+
return _OPTIMIZE_INTERVAL
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _stamp_file(store: str | Path) -> Path:
|
|
59
|
+
digest = hashlib.sha256(str(store).encode()).hexdigest()[:16]
|
|
60
|
+
return Path(tempfile.gettempdir()) / f"witan-code-optimize-{digest}.json"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _last_run(store: str | Path) -> float:
|
|
64
|
+
try:
|
|
65
|
+
return float(json.loads(_stamp_file(store).read_text()).get("stamp", 0.0))
|
|
66
|
+
except Exception: # noqa: BLE001 — missing/corrupt stamp → treat as never run
|
|
67
|
+
return 0.0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _mark_run(store: str | Path, when: float) -> None:
|
|
71
|
+
"""Record the last-optimize time atomically.
|
|
72
|
+
|
|
73
|
+
Concurrent Stop hooks (or an interrupted write) could otherwise leave a
|
|
74
|
+
half-written stamp that ``_last_run`` reads as "never run", defeating the
|
|
75
|
+
throttle and letting optimize spawn repeatedly. Write a process-unique temp
|
|
76
|
+
file and ``os.replace`` it in, so a reader always sees a complete file.
|
|
77
|
+
"""
|
|
78
|
+
path = _stamp_file(store)
|
|
79
|
+
tmp = path.with_name(f"{path.name}.{os.getpid()}.tmp")
|
|
80
|
+
try:
|
|
81
|
+
tmp.write_text(json.dumps({"stamp": when}))
|
|
82
|
+
os.replace(tmp, path)
|
|
83
|
+
except OSError:
|
|
84
|
+
try:
|
|
85
|
+
tmp.unlink(missing_ok=True)
|
|
86
|
+
except OSError:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def due(store: str | Path, now: float | None = None) -> bool:
|
|
91
|
+
"""Whether an opportunistic optimize is due for ``store``.
|
|
92
|
+
|
|
93
|
+
False when auto-optimize is disabled, the store is remote (maintained
|
|
94
|
+
server-side, not by a client hook), the store doesn't exist yet, or the
|
|
95
|
+
throttle window hasn't elapsed.
|
|
96
|
+
"""
|
|
97
|
+
interval = optimize_interval()
|
|
98
|
+
if interval <= 0:
|
|
99
|
+
return False
|
|
100
|
+
if str(store).startswith(_REMOTE_PREFIXES):
|
|
101
|
+
return False
|
|
102
|
+
if not Path(store).exists():
|
|
103
|
+
return False
|
|
104
|
+
now = time.time() if now is None else now
|
|
105
|
+
return now - _last_run(store) >= interval
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def spawn_background_optimize(store: str | Path, now: float | None = None) -> bool:
|
|
109
|
+
"""If an optimize is due for ``store``, detach one and return ``True``.
|
|
110
|
+
|
|
111
|
+
Best-effort and non-blocking: the throttle stamp is written *before*
|
|
112
|
+
spawning (so a failing optimize can't hot-loop every session), and the
|
|
113
|
+
child is fully detached (its own session, no inherited stdio) so it
|
|
114
|
+
outlives the Stop hook. Never raises — a maintenance failure must not fail
|
|
115
|
+
the hook.
|
|
116
|
+
"""
|
|
117
|
+
if not due(store, now):
|
|
118
|
+
return False
|
|
119
|
+
_mark_run(store, time.time() if now is None else now)
|
|
120
|
+
try:
|
|
121
|
+
popen_detached(
|
|
122
|
+
[sys.executable, "-m", "witan_code", "optimize", "--store", str(store)],
|
|
123
|
+
stdin=subprocess.DEVNULL,
|
|
124
|
+
stdout=subprocess.DEVNULL,
|
|
125
|
+
stderr=subprocess.DEVNULL,
|
|
126
|
+
env=dict(os.environ),
|
|
127
|
+
)
|
|
128
|
+
return True
|
|
129
|
+
except OSError:
|
|
130
|
+
return False
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Package map: a repo's declared canonical package identity.
|
|
2
|
+
|
|
3
|
+
Read from ``witan-code.toml`` at the repo root (see docs/PACKAGE_MAP.md).
|
|
4
|
+
The identity qualifies provider symbol strings and feeds the
|
|
5
|
+
``known_provider_package`` confidence heuristic. Repos without the file get a
|
|
6
|
+
fallback identity derived from the repo URI so provider symbols always carry a
|
|
7
|
+
package qualifier.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import tomllib
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
PACKAGE_MAP_FILENAME = "witan-code.toml"
|
|
15
|
+
|
|
16
|
+
# SCIP empty-field convention (see docs/SYMBOL_FORMAT.md).
|
|
17
|
+
EMPTY = "."
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class PackageIdentity:
|
|
22
|
+
name: str
|
|
23
|
+
manager: str = EMPTY
|
|
24
|
+
version: str = "main"
|
|
25
|
+
provides: tuple[str, ...] = field(default_factory=tuple)
|
|
26
|
+
"""Extra published identities as ``"manager:name"`` strings."""
|
|
27
|
+
|
|
28
|
+
declared: bool = False
|
|
29
|
+
"""True when read from witan-code.toml, False for a URI-derived fallback."""
|
|
30
|
+
|
|
31
|
+
def provided_names(self) -> frozenset[str]:
|
|
32
|
+
"""Bare package names this repo provides (primary name + provides)."""
|
|
33
|
+
names = {self.name}
|
|
34
|
+
for entry in self.provides:
|
|
35
|
+
_, _, name = entry.partition(":")
|
|
36
|
+
names.add(name or entry)
|
|
37
|
+
return frozenset(names)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fallback_identity(repo: str) -> PackageIdentity:
|
|
41
|
+
"""Identity derived from the canonical repo URI when no TOML exists."""
|
|
42
|
+
name = repo.rstrip("/").rsplit("/", 1)[-1] or repo
|
|
43
|
+
return PackageIdentity(name=name)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load(repo_root: Path, repo: str) -> PackageIdentity:
|
|
47
|
+
"""Load ``witan-code.toml`` from ``repo_root``, else the fallback identity.
|
|
48
|
+
|
|
49
|
+
Malformed TOML or a missing ``[package].name`` degrades to the fallback —
|
|
50
|
+
the package map must never make indexing fail.
|
|
51
|
+
"""
|
|
52
|
+
path = repo_root / PACKAGE_MAP_FILENAME
|
|
53
|
+
if not path.is_file():
|
|
54
|
+
return fallback_identity(repo)
|
|
55
|
+
try:
|
|
56
|
+
data = tomllib.loads(path.read_text())
|
|
57
|
+
except (tomllib.TOMLDecodeError, OSError, UnicodeDecodeError):
|
|
58
|
+
return fallback_identity(repo)
|
|
59
|
+
|
|
60
|
+
pkg = data.get("package")
|
|
61
|
+
if not isinstance(pkg, dict) or not isinstance(pkg.get("name"), str):
|
|
62
|
+
return fallback_identity(repo)
|
|
63
|
+
|
|
64
|
+
provides_raw = pkg.get("provides", ())
|
|
65
|
+
if not isinstance(provides_raw, (list, tuple)):
|
|
66
|
+
provides_raw = ()
|
|
67
|
+
provides = tuple(entry for entry in provides_raw if isinstance(entry, str))
|
|
68
|
+
return PackageIdentity(
|
|
69
|
+
name=pkg["name"],
|
|
70
|
+
manager=str(pkg.get("manager", EMPTY)),
|
|
71
|
+
version=str(pkg.get("version", "main")),
|
|
72
|
+
provides=provides,
|
|
73
|
+
declared=True,
|
|
74
|
+
)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Cross-Repo Context Bridge — read / delete / search queries (_bridge.omni).
|
|
2
|
+
//
|
|
3
|
+
// Linkages are NOT edges: they are computed by grouping InterfaceBinding rows on
|
|
4
|
+
// (kind, key_norm) in Python. These queries just filter + search the flat node.
|
|
5
|
+
// Read filters use the match-shorthand `{ field: $param }` only (standalone
|
|
6
|
+
// `where`/quoted-key parse-errors in reads); deletes use `where`.
|
|
7
|
+
|
|
8
|
+
// ── Deletes (reindex cleanup) ─────────────────────────────────────
|
|
9
|
+
// Per-file purge: drop one source file's bindings so a reindex only replaces
|
|
10
|
+
// what it re-extracted (an incremental full-repo index skips unchanged files,
|
|
11
|
+
// so a repo-wide purge would orphan their bindings). Keyed on the composite
|
|
12
|
+
// repo_file ("repo|file") because delete `where` takes a single condition
|
|
13
|
+
// (no compound `and`).
|
|
14
|
+
|
|
15
|
+
query delete_bindings_in_file($repo_file: String) {
|
|
16
|
+
delete InterfaceBinding where repo_file = $repo_file
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// All bindings — read once to build the cross-repo dependency visualization.
|
|
20
|
+
|
|
21
|
+
query all_bindings() {
|
|
22
|
+
match {
|
|
23
|
+
$b: InterfaceBinding
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
$b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
27
|
+
$b.file, $b.symbol_id, $b.framework, $b.generic, $b.confidence,
|
|
28
|
+
$b.symbol, $b.line
|
|
29
|
+
}
|
|
30
|
+
limit 1000000
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Per-repo symbol table (Stage 1 — docs/SYMBOL_TABLE.md) ────────
|
|
34
|
+
// The table is rebuilt exactly on every bridge write: delete the repo's rows,
|
|
35
|
+
// re-aggregate from surviving + fresh bindings. Delete is keyed on the single
|
|
36
|
+
// repo field (delete `where` takes one condition).
|
|
37
|
+
|
|
38
|
+
query delete_repo_symbols($repo: String) {
|
|
39
|
+
delete RepoSymbol where repo = $repo
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
query repo_symbols($repo: String) {
|
|
43
|
+
match {
|
|
44
|
+
$s: RepoSymbol { repo: $repo }
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
$s.slug, $s.repo, $s.role, $s.symbol, $s.scheme, $s.descriptor,
|
|
48
|
+
$s.key_norm, $s.manager, $s.package, $s.version, $s.kind,
|
|
49
|
+
$s.n_refs, $s.confidence, $s.file, $s.line
|
|
50
|
+
}
|
|
51
|
+
order { $s.symbol asc }
|
|
52
|
+
limit 100000
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Every RepoSymbol row across all repos — Stage 2 (witan_code.stitch) reads
|
|
56
|
+
// this once and joins external/exported rows in Python, the same "bulk read,
|
|
57
|
+
// group in Python" pattern all_bindings/build_graph already use for the
|
|
58
|
+
// heuristic tier.
|
|
59
|
+
|
|
60
|
+
query all_repo_symbols() {
|
|
61
|
+
match {
|
|
62
|
+
$s: RepoSymbol
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
$s.repo, $s.role, $s.symbol, $s.scheme, $s.descriptor,
|
|
66
|
+
$s.key_norm, $s.manager, $s.package, $s.version, $s.kind,
|
|
67
|
+
$s.n_refs, $s.confidence, $s.file, $s.line
|
|
68
|
+
}
|
|
69
|
+
limit 1000000
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Stage-2 join primitives: exact descriptor match (env/svc), and the key_norm
|
|
73
|
+
// match (http/pkg). pkg canonical descriptors are always "." (SYMBOL_FORMAT.md
|
|
74
|
+
// — package identity lives in the manager/package fields, not the
|
|
75
|
+
// descriptor), so symbols_by_descriptor cannot distinguish packages;
|
|
76
|
+
// key_norm carries the package name for both exported and external rows
|
|
77
|
+
// instead. http also joins on key_norm rather than descriptor because the
|
|
78
|
+
// descriptor embeds the method, which consumers usually can't determine
|
|
79
|
+
// statically ("*").
|
|
80
|
+
|
|
81
|
+
query symbols_by_descriptor($scheme: String, $descriptor: String) {
|
|
82
|
+
match {
|
|
83
|
+
$s: RepoSymbol { scheme: $scheme, descriptor: $descriptor }
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
$s.slug, $s.repo, $s.role, $s.symbol, $s.scheme, $s.descriptor,
|
|
87
|
+
$s.key_norm, $s.manager, $s.package, $s.version, $s.kind,
|
|
88
|
+
$s.n_refs, $s.confidence, $s.file, $s.line
|
|
89
|
+
}
|
|
90
|
+
order { $s.repo asc }
|
|
91
|
+
limit 1000
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
query symbols_by_key($scheme: String, $key_norm: String) {
|
|
95
|
+
match {
|
|
96
|
+
$s: RepoSymbol { scheme: $scheme, key_norm: $key_norm }
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
$s.slug, $s.repo, $s.role, $s.symbol, $s.scheme, $s.descriptor,
|
|
100
|
+
$s.key_norm, $s.manager, $s.package, $s.version, $s.kind,
|
|
101
|
+
$s.n_refs, $s.confidence, $s.file, $s.line
|
|
102
|
+
}
|
|
103
|
+
order { $s.repo asc }
|
|
104
|
+
limit 1000
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Package maps ──────────────────────────────────────────────────
|
|
108
|
+
// Every repo's declared/fallback package identity (docs/PACKAGE_MAP.md).
|
|
109
|
+
// Read at bridge-write time to qualify symbols and feed the
|
|
110
|
+
// known_provider_package heuristic.
|
|
111
|
+
|
|
112
|
+
query all_package_maps() {
|
|
113
|
+
match {
|
|
114
|
+
$p: PackageMap
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
$p.repo, $p.name, $p.manager, $p.version, $p.provides, $p.declared
|
|
118
|
+
}
|
|
119
|
+
limit 10000
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Grouping reads ────────────────────────────────────────────────
|
|
123
|
+
// All bindings for a contract key (the workhorse behind providers / consumers /
|
|
124
|
+
// impact grouping).
|
|
125
|
+
|
|
126
|
+
query bindings_by_key($kind: String, $key_norm: String) {
|
|
127
|
+
match {
|
|
128
|
+
$b: InterfaceBinding { kind: $kind, key_norm: $key_norm }
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
$b.slug, $b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
132
|
+
$b.file, $b.symbol_id, $b.line, $b.language, $b.framework, $b.generic,
|
|
133
|
+
$b.symbol
|
|
134
|
+
}
|
|
135
|
+
order { $b.repo asc }
|
|
136
|
+
limit 500
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Same filtered to one role — backs code_interface_providers / _consumers.
|
|
140
|
+
|
|
141
|
+
query bindings_by_key_role($kind: String, $key_norm: String, $role: String) {
|
|
142
|
+
match {
|
|
143
|
+
$b: InterfaceBinding { kind: $kind, key_norm: $key_norm, role: $role }
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
$b.slug, $b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
147
|
+
$b.file, $b.symbol_id, $b.line, $b.language, $b.framework, $b.generic,
|
|
148
|
+
$b.symbol
|
|
149
|
+
}
|
|
150
|
+
order { $b.repo asc }
|
|
151
|
+
limit 500
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Bindings attributed to a symbol — first half of code_cross_repo_impact.
|
|
155
|
+
|
|
156
|
+
query bindings_for_symbol($symbol_id: String) {
|
|
157
|
+
match {
|
|
158
|
+
$b: InterfaceBinding { symbol_id: $symbol_id }
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
$b.slug, $b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
162
|
+
$b.file, $b.symbol_id, $b.generic
|
|
163
|
+
}
|
|
164
|
+
limit 500
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Full-text search (BM25 on key_norm) ───────────────────────────
|
|
168
|
+
|
|
169
|
+
query search_bindings($query: String) {
|
|
170
|
+
match {
|
|
171
|
+
$b: InterfaceBinding
|
|
172
|
+
search($b.key_norm, $query)
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
$b.slug, $b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
176
|
+
$b.file, $b.symbol_id, $b.framework, $b.generic
|
|
177
|
+
}
|
|
178
|
+
order { bm25($b.key_norm, $query) desc }
|
|
179
|
+
limit 100
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
query search_bindings_by_kind($query: String, $kind: String) {
|
|
183
|
+
match {
|
|
184
|
+
$b: InterfaceBinding { kind: $kind }
|
|
185
|
+
search($b.key_norm, $query)
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
$b.slug, $b.kind, $b.key, $b.key_norm, $b.role, $b.repo,
|
|
189
|
+
$b.file, $b.symbol_id, $b.framework, $b.generic
|
|
190
|
+
}
|
|
191
|
+
order { bm25($b.key_norm, $query) desc }
|
|
192
|
+
limit 100
|
|
193
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Code Graph — insert/update mutations.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: Omnigraph cannot mix deletes with inserts/updates in one query.
|
|
4
|
+
// Deletes for reindexing live in delete.gq and are run as separate change()
|
|
5
|
+
// calls BEFORE these inserts.
|
|
6
|
+
|
|
7
|
+
// ── CodeFile ──────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
query insert_file(
|
|
10
|
+
$id: String,
|
|
11
|
+
$repo: String,
|
|
12
|
+
$path: String,
|
|
13
|
+
$language: String,
|
|
14
|
+
$content_hash: String,
|
|
15
|
+
$indexed_at: DateTime
|
|
16
|
+
) {
|
|
17
|
+
insert CodeFile {
|
|
18
|
+
slug: $id,
|
|
19
|
+
repo: $repo,
|
|
20
|
+
path: $path,
|
|
21
|
+
language: $language,
|
|
22
|
+
content_hash: $content_hash,
|
|
23
|
+
indexed_at: $indexed_at
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Symbol ────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
query insert_symbol(
|
|
30
|
+
$id: String,
|
|
31
|
+
$repo: String,
|
|
32
|
+
$file_id: String,
|
|
33
|
+
$name: String,
|
|
34
|
+
$qualified_name: String,
|
|
35
|
+
$kind: String,
|
|
36
|
+
$start_line: I32,
|
|
37
|
+
$end_line: I32,
|
|
38
|
+
$signature: String?,
|
|
39
|
+
$docstring: String?,
|
|
40
|
+
$indexed_at: DateTime
|
|
41
|
+
) {
|
|
42
|
+
insert Symbol {
|
|
43
|
+
slug: $id,
|
|
44
|
+
repo: $repo,
|
|
45
|
+
file_id: $file_id,
|
|
46
|
+
name: $name,
|
|
47
|
+
qualified_name: $qualified_name,
|
|
48
|
+
kind: $kind,
|
|
49
|
+
start_line: $start_line,
|
|
50
|
+
end_line: $end_line,
|
|
51
|
+
signature: $signature,
|
|
52
|
+
docstring: $docstring,
|
|
53
|
+
indexed_at: $indexed_at
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Edges ─────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
query link_defines($from: String, $to: String) {
|
|
60
|
+
insert Defines { from: $from, to: $to }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
query link_contains($from: String, $to: String) {
|
|
64
|
+
insert Contains { from: $from, to: $to }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
query link_calls($from: String, $to: String) {
|
|
68
|
+
insert Calls { from: $from, to: $to }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
query link_references($from: String, $to: String) {
|
|
72
|
+
insert References { from: $from, to: $to }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
query link_imports($from: String, $to: String) {
|
|
76
|
+
insert Imports { from: $from, to: $to }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
query link_inherits($from: String, $to: String) {
|
|
80
|
+
insert Inherits { from: $from, to: $to }
|
|
81
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Code Graph — read queries.
|
|
2
|
+
//
|
|
3
|
+
// Dispatch strategy mirrors witan: the MCP server picks the named
|
|
4
|
+
// query based on the call. Symbol ids are `<repo>#<path/to/file.py>::<Name>`.
|
|
5
|
+
// Queries project the id as `slug`; the server renames it to `symbol_id`
|
|
6
|
+
// (see server._as_symbol_id) so it round-trips under the tools' param name.
|
|
7
|
+
|
|
8
|
+
// ── Definition lookup ─────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
query find_by_name($name: String) {
|
|
11
|
+
match {
|
|
12
|
+
$s: Symbol { name: $name }
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
$s.slug, $s.repo, $s.file_id, $s.name, $s.qualified_name,
|
|
16
|
+
$s.kind, $s.start_line, $s.end_line, $s.signature, $s.docstring,
|
|
17
|
+
$s.decorators
|
|
18
|
+
}
|
|
19
|
+
order { $s.qualified_name asc }
|
|
20
|
+
limit 50
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
query find_by_qualified_name($qualified_name: String) {
|
|
24
|
+
match {
|
|
25
|
+
$s: Symbol { qualified_name: $qualified_name }
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
$s.slug, $s.repo, $s.file_id, $s.name, $s.qualified_name,
|
|
29
|
+
$s.kind, $s.start_line, $s.end_line, $s.signature, $s.docstring,
|
|
30
|
+
$s.decorators
|
|
31
|
+
}
|
|
32
|
+
order { $s.qualified_name asc }
|
|
33
|
+
limit 50
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
query get_symbol($id: String) {
|
|
37
|
+
match {
|
|
38
|
+
$s: Symbol { slug: $id }
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
$s.slug, $s.repo, $s.file_id, $s.name, $s.qualified_name,
|
|
42
|
+
$s.kind, $s.start_line, $s.end_line, $s.signature, $s.docstring,
|
|
43
|
+
$s.decorators
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Full-text symbol search (BM25 on name / qualified_name) ────────
|
|
48
|
+
|
|
49
|
+
query search_symbols($query: String) {
|
|
50
|
+
match {
|
|
51
|
+
$s: Symbol
|
|
52
|
+
search($s.qualified_name, $query)
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
$s.slug, $s.repo, $s.file_id, $s.name, $s.qualified_name,
|
|
56
|
+
$s.kind, $s.start_line, $s.end_line, $s.signature
|
|
57
|
+
}
|
|
58
|
+
order { bm25($s.qualified_name, $query) desc }
|
|
59
|
+
limit 50
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
query search_symbols_by_kind($query: String, $kind: String) {
|
|
63
|
+
match {
|
|
64
|
+
$s: Symbol { kind: $kind }
|
|
65
|
+
search($s.qualified_name, $query)
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
$s.slug, $s.repo, $s.file_id, $s.name, $s.qualified_name,
|
|
69
|
+
$s.kind, $s.start_line, $s.end_line, $s.signature
|
|
70
|
+
}
|
|
71
|
+
order { bm25($s.qualified_name, $query) desc }
|
|
72
|
+
limit 50
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── File symbols ──────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
query symbols_in_file($file_id: String) {
|
|
78
|
+
match {
|
|
79
|
+
$s: Symbol { file_id: $file_id }
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
$s.slug, $s.name, $s.qualified_name, $s.kind,
|
|
83
|
+
$s.start_line, $s.end_line, $s.signature, $s.docstring, $s.decorators
|
|
84
|
+
}
|
|
85
|
+
order { $s.start_line asc }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Edge traversal ────────────────────────────────────────────────
|
|
89
|
+
// Incoming Calls: symbols that call $id.
|
|
90
|
+
|
|
91
|
+
query callers($id: String) {
|
|
92
|
+
match {
|
|
93
|
+
$target: Symbol { slug: $id }
|
|
94
|
+
$caller: Symbol
|
|
95
|
+
$caller calls $target
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
$caller.slug, $caller.repo, $caller.file_id, $caller.name,
|
|
99
|
+
$caller.qualified_name, $caller.kind,
|
|
100
|
+
$caller.start_line, $caller.end_line
|
|
101
|
+
}
|
|
102
|
+
order { $caller.qualified_name asc }
|
|
103
|
+
limit 200
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Incoming Calls + References: anything that uses $id.
|
|
107
|
+
|
|
108
|
+
query referencers($id: String) {
|
|
109
|
+
match {
|
|
110
|
+
$target: Symbol { slug: $id }
|
|
111
|
+
$src: Symbol
|
|
112
|
+
$src references $target
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
$src.slug, $src.repo, $src.file_id, $src.name,
|
|
116
|
+
$src.qualified_name, $src.kind,
|
|
117
|
+
$src.start_line, $src.end_line
|
|
118
|
+
}
|
|
119
|
+
order { $src.qualified_name asc }
|
|
120
|
+
limit 200
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── CodeFile fetch (for incremental reindex) ──────────────────────
|
|
124
|
+
|
|
125
|
+
query get_file($id: String) {
|
|
126
|
+
match {
|
|
127
|
+
$f: CodeFile { slug: $id }
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
$f.slug, $f.repo, $f.path, $f.language,
|
|
131
|
+
$f.content_hash, $f.indexed_at
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// All file hashes — read once per index run so the incremental skip check is
|
|
136
|
+
// in-memory rather than a query per file.
|
|
137
|
+
|
|
138
|
+
query all_file_hashes() {
|
|
139
|
+
match {
|
|
140
|
+
$f: CodeFile
|
|
141
|
+
}
|
|
142
|
+
return { $f.slug, $f.content_hash }
|
|
143
|
+
limit 1000000
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Symbol ids defined by a file — needed to delete their outgoing edges
|
|
147
|
+
// before reindexing the file.
|
|
148
|
+
|
|
149
|
+
query symbol_ids_in_file($file_id: String) {
|
|
150
|
+
match {
|
|
151
|
+
$s: Symbol { file_id: $file_id }
|
|
152
|
+
}
|
|
153
|
+
return { $s.slug }
|
|
154
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Code Graph — delete queries (reindex path).
|
|
2
|
+
//
|
|
3
|
+
// Omnigraph cannot mix deletes with inserts/updates in one query, so these
|
|
4
|
+
// run as their own change() calls BEFORE re-inserting a file's data.
|
|
5
|
+
//
|
|
6
|
+
// Edges are deleted by deleting their endpoint nodes: removing a Symbol or
|
|
7
|
+
// CodeFile removes its incident edges. We delete all Symbols belonging to a
|
|
8
|
+
// file (by file_id) and then the CodeFile itself.
|
|
9
|
+
|
|
10
|
+
query delete_symbols_in_file($file_id: String) {
|
|
11
|
+
delete Symbol where file_id = $file_id
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
query delete_file($id: String) {
|
|
15
|
+
delete CodeFile where slug = $id
|
|
16
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
; Bash / Zsh symbol extraction. The tree-sitter bash grammar covers most zsh.
|
|
2
|
+
|
|
3
|
+
; function foo() { ... } and foo() { ... }
|
|
4
|
+
(function_definition
|
|
5
|
+
name: (word) @symbol.function)
|
|
6
|
+
|
|
7
|
+
; calls — best-effort Calls/References edges to other functions.
|
|
8
|
+
(command
|
|
9
|
+
name: (command_name (word) @call.name))
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
; HCL symbol extraction (Terraform / Packer / Vault-policy style configs).
|
|
2
|
+
;
|
|
3
|
+
; Each labeled block (resource/variable/output/module/data/source/path, …)
|
|
4
|
+
; becomes a Symbol keyed on its LAST label — the part that's actually unique
|
|
5
|
+
; (`resource "aws_instance" "foo"` -> foo, not the shared "aws_instance" type;
|
|
6
|
+
; `variable "app_name"` -> app_name; Vault's `path "secret/data/*"` -> the
|
|
7
|
+
; path itself). The `.` anchor requires the label be immediately followed by
|
|
8
|
+
; the opening brace, which is only true of the last label when there are two.
|
|
9
|
+
;
|
|
10
|
+
; Unlabeled blocks (locals, bare provider blocks) aren't captured — there's
|
|
11
|
+
; no unique text to key them on.
|
|
12
|
+
|
|
13
|
+
(block
|
|
14
|
+
(string_lit) @symbol.block
|
|
15
|
+
.
|
|
16
|
+
(block_start))
|