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.
@@ -0,0 +1,29 @@
1
+ ; Python symbol extraction.
2
+ ; Capture names drive Symbol kind + edge construction in indexer.py.
3
+
4
+ (function_definition
5
+ name: (identifier) @symbol.function)
6
+
7
+ (class_definition
8
+ name: (identifier) @symbol.class)
9
+
10
+ ; Base classes — used for best-effort Inherits edges.
11
+ (class_definition
12
+ superclasses: (argument_list (identifier) @inherit.base))
13
+
14
+ ; Imports — used for import-aware name resolution.
15
+ (import_statement
16
+ name: (dotted_name (identifier) @import.name))
17
+
18
+ (import_from_statement
19
+ name: (dotted_name (identifier) @import.name))
20
+
21
+ (import_from_statement
22
+ name: (aliased_import (dotted_name (identifier) @import.name)))
23
+
24
+ ; Calls — best-effort Calls/References edges.
25
+ (call
26
+ function: (identifier) @call.name)
27
+
28
+ (call
29
+ function: (attribute attribute: (identifier) @call.name))
@@ -0,0 +1,20 @@
1
+ ; SQL symbol extraction.
2
+ ;
3
+ ; dbt-style models are usually a bare `select` wrapped in Jinja ({{ }} / {% %})
4
+ ; that this grammar can't parse (each templated span becomes an isolated
5
+ ; ERROR node; the surrounding SQL still parses). Those files rarely contain a
6
+ ; CREATE statement — the CTEs inside the query are the only reliably
7
+ ; navigable structure, so they're captured alongside the CREATE-statement
8
+ ; definitions more common in migrations/raw SQL.
9
+
10
+ (create_table
11
+ (object_reference) @symbol.table)
12
+
13
+ (create_view
14
+ (object_reference) @symbol.table)
15
+
16
+ (create_function
17
+ (object_reference) @symbol.function)
18
+
19
+ (cte
20
+ (identifier) @symbol.cte)
@@ -0,0 +1,51 @@
1
+ ; TypeScript / JavaScript / JSX / TSX symbol extraction. All JS and TS variants
2
+ ; are parsed with the tsx grammar (a superset), so this single query covers them.
3
+
4
+ (function_declaration
5
+ name: (identifier) @symbol.function)
6
+
7
+ (generator_function_declaration
8
+ name: (identifier) @symbol.function)
9
+
10
+ (class_declaration
11
+ name: (type_identifier) @symbol.class)
12
+
13
+ (method_definition
14
+ name: (property_identifier) @symbol.method)
15
+
16
+ ; const Foo = () => {...} / const Foo = function () {...}
17
+ (variable_declarator
18
+ name: (identifier) @symbol.function
19
+ value: [(arrow_function) (function_expression)])
20
+
21
+ ; class fields holding arrows: render = () => {...}
22
+ (public_field_definition
23
+ name: (property_identifier) @symbol.method
24
+ value: (arrow_function))
25
+
26
+ (interface_declaration
27
+ name: (type_identifier) @symbol.interface)
28
+
29
+ (type_alias_declaration
30
+ name: (type_identifier) @symbol.type)
31
+
32
+ (enum_declaration
33
+ name: (identifier) @symbol.enum)
34
+
35
+ ; extends Base — best-effort Inherits.
36
+ (class_declaration
37
+ (class_heritage (extends_clause (identifier) @inherit.base)))
38
+
39
+ ; imports — best-effort resolution.
40
+ (import_statement
41
+ (import_clause (identifier) @import.name))
42
+
43
+ (import_statement
44
+ (import_clause (named_imports (import_specifier name: (identifier) @import.name))))
45
+
46
+ ; calls
47
+ (call_expression
48
+ function: (identifier) @call.name)
49
+
50
+ (call_expression
51
+ function: (member_expression property: (property_identifier) @call.name))
@@ -0,0 +1,5 @@
1
+ ; YAML symbol extraction. Each mapping key becomes a Symbol; nested keys get a
2
+ ; dotted qualified path (e.g. jobs.build.steps) so large configs are navigable.
3
+
4
+ (block_mapping_pair
5
+ key: (flow_node) @symbol.key)
witan_code/repo.py ADDED
@@ -0,0 +1,282 @@
1
+ import configparser
2
+ import os
3
+ import re
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+
8
+ def detect(override: str | None = None, start: Path | None = None) -> str | None:
9
+ """
10
+ Return a canonical repo key (HTTPS URI) for ``start`` (or the cwd).
11
+
12
+ Resolution order:
13
+ 1. ``override`` parameter (explicit caller value)
14
+ 2. ``WITAN_REPO`` environment variable
15
+ 3. ``git remote get-url origin`` (handles worktrees and multi-valued
16
+ config keys that ``configparser`` rejects)
17
+ 4. ``origin`` remote URL parsed from the nearest ``.git/config``
18
+ 5. directory name of the git root (fallback when no remote)
19
+ 6. ``None`` — no repo context available
20
+ """
21
+ if override is not None:
22
+ return override or None
23
+
24
+ if env_repo := os.environ.get("WITAN_REPO"):
25
+ return env_repo
26
+
27
+ base = start or Path.cwd()
28
+
29
+ if url := _git_origin(base):
30
+ return _normalise(url)
31
+
32
+ git_config_path = _find_git_config(base)
33
+ if git_config_path is None:
34
+ return None
35
+
36
+ if slug := _parse_origin(git_config_path):
37
+ return slug
38
+
39
+ # No origin remote — fall back to the repo root directory name.
40
+ return git_config_path.parent.parent.name
41
+
42
+
43
+ def _git_origin(start: Path) -> str | None:
44
+ """Resolve the ``origin`` remote URL via git itself.
45
+
46
+ git is the only correct parser of its own config: it resolves git worktrees
47
+ (where ``.git`` is a file) and tolerates multi-valued keys (e.g. several
48
+ ``fetch =`` lines) that Python's ``configparser`` rejects.
49
+ """
50
+ try:
51
+ result = subprocess.run(
52
+ ["git", "-C", str(start), "remote", "get-url", "origin"],
53
+ capture_output=True,
54
+ text=True,
55
+ check=False,
56
+ )
57
+ except OSError:
58
+ return None
59
+ if result.returncode != 0:
60
+ return None
61
+ return result.stdout.strip() or None
62
+
63
+
64
+ def _find_git_config(start: Path) -> Path | None:
65
+ """Walk up from ``start`` until a .git/config is found."""
66
+ for directory in [start, *start.parents]:
67
+ candidate = directory / ".git" / "config"
68
+ if candidate.exists():
69
+ return candidate
70
+ return None
71
+
72
+
73
+ def _parse_origin(git_config: Path) -> str | None:
74
+ """Parse .git/config and return the normalised ``origin`` remote URL."""
75
+ parser = configparser.RawConfigParser()
76
+ try:
77
+ parser.read(git_config)
78
+ except configparser.Error:
79
+ return None
80
+
81
+ section = 'remote "origin"'
82
+ if not parser.has_option(section, "url"):
83
+ return None
84
+
85
+ return _normalise(parser.get(section, "url"))
86
+
87
+
88
+ def _normalise(url: str) -> str:
89
+ """
90
+ Normalise a git remote URL to its canonical HTTPS project URI.
91
+
92
+ Must match ``witan/repo.py::_normalise`` so the repo key — used
93
+ in symbol ids (``repo#path::Name``) and the Layer-1 ``symbol_refs`` that point
94
+ at them — is identical across both stores.
95
+
96
+ Examples
97
+ --------
98
+ git@github.com:mitodl/ol-django.git → https://github.com/mitodl/ol-django
99
+ https://github.com/mitodl/ol-django → https://github.com/mitodl/ol-django
100
+ """
101
+ url = re.sub(r"\.git$", "", url.strip()).rstrip("/")
102
+
103
+ if m := re.match(r"(?:ssh://)?[^@]+@([^:/]+)[:/](.+)", url):
104
+ return f"https://{m.group(1)}/{m.group(2)}"
105
+
106
+ if m := re.match(r"https?://(?:[^@/]+@)?([^/]+)/(.+)", url):
107
+ return f"https://{m.group(1)}/{m.group(2)}"
108
+
109
+ return url
110
+
111
+
112
+ # Omnigraph scratch branch for detached-HEAD checkouts: never index a detached
113
+ # state onto main (it would overwrite the shared view with an arbitrary commit).
114
+ DETACHED_BRANCH = "_detached"
115
+
116
+ _DEFAULT_BRANCHES = frozenset({"main", "master"})
117
+
118
+
119
+ def sanitize_branch(name: str) -> str:
120
+ """Make a git branch name safe as an omnigraph branch name."""
121
+ return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_") or DETACHED_BRANCH
122
+
123
+
124
+ def branch_store_name(name: str) -> str:
125
+ """Omnigraph branch name for a NON-DEFAULT git branch.
126
+
127
+ Omnigraph reserves ``main`` for the store's default branch, so a feature
128
+ branch literally named ``main`` (possible when the repo's default is
129
+ ``master``) maps to ``_main``. sanitize_branch strips leading
130
+ underscores, so no other git branch can produce a ``_``-prefixed name —
131
+ the underscore namespace (``_main``, ``_detached``) is collision-free.
132
+ """
133
+ sanitized = sanitize_branch(name)
134
+ return "_main" if sanitized == "main" else sanitized
135
+
136
+
137
+ def current_branch(start: Path | None = None) -> str | None:
138
+ """Current git branch name for ``start`` (or the cwd).
139
+
140
+ Returns ``"HEAD"`` for a detached checkout (git's own convention) and
141
+ ``None`` outside a git repository or when git is unavailable.
142
+ """
143
+ base = start or Path.cwd()
144
+ try:
145
+ result = subprocess.run(
146
+ ["git", "-C", str(base), "rev-parse", "--abbrev-ref", "HEAD"],
147
+ capture_output=True,
148
+ text=True,
149
+ check=False,
150
+ )
151
+ except OSError:
152
+ return None
153
+ if result.returncode != 0:
154
+ return None
155
+ return result.stdout.strip() or None
156
+
157
+
158
+ def _origin_default_branch(base: Path) -> str | None:
159
+ """Short name of origin's HEAD branch (``main``), if resolvable locally."""
160
+ try:
161
+ result = subprocess.run(
162
+ [
163
+ "git",
164
+ "-C",
165
+ str(base),
166
+ "symbolic-ref",
167
+ "--short",
168
+ "refs/remotes/origin/HEAD",
169
+ ],
170
+ capture_output=True,
171
+ text=True,
172
+ check=False,
173
+ )
174
+ except OSError:
175
+ return None
176
+ if result.returncode != 0:
177
+ return None
178
+ ref = result.stdout.strip()
179
+ return (ref.partition("/")[2] or None) if ref else None
180
+
181
+
182
+ def _default_branch(base: Path) -> str | None:
183
+ """The repo's default branch: origin HEAD, else main/master by presence.
184
+
185
+ The local fallback prefers ``main`` over ``master`` when both exist so
186
+ the choice is deterministic; whichever loses maps to its own store branch
187
+ (collision-free either way). Returns None when no default is
188
+ recognizable — then every branch gets its own store branch and nothing
189
+ claims the store's main.
190
+ """
191
+ if default := _origin_default_branch(base):
192
+ return default
193
+ try:
194
+ result = subprocess.run(
195
+ ["git", "-C", str(base), "branch", "--list", *_DEFAULT_BRANCHES],
196
+ capture_output=True,
197
+ text=True,
198
+ check=False,
199
+ )
200
+ except OSError:
201
+ return None
202
+ present = {line.lstrip("*+ ").strip() for line in result.stdout.splitlines()}
203
+ for name in ("main", "master"):
204
+ if name in present:
205
+ return name
206
+ return None
207
+
208
+
209
+ def store_branch(start: Path | None = None) -> str | None:
210
+ """Omnigraph branch for the checkout at ``start``: ``None`` = store main.
211
+
212
+ The repo's default branch maps to the store's main branch; any other
213
+ branch maps through ``branch_store_name`` (so a non-default branch named
214
+ ``main`` gets ``_main``, never colliding with the store's default); a
215
+ detached HEAD maps to the ``_detached`` scratch branch.
216
+ """
217
+ base = start or Path.cwd()
218
+ branch = current_branch(base)
219
+ if branch is None:
220
+ return None
221
+ if branch == "HEAD":
222
+ return DETACHED_BRANCH
223
+ if branch == _default_branch(base):
224
+ return None
225
+ return branch_store_name(branch)
226
+
227
+
228
+ def local_branches(start: Path | None = None) -> frozenset[str] | None:
229
+ """Store-branch names of all local git branches, or None when git fails.
230
+
231
+ Uses the same mapping as ``store_branch`` (``branch_store_name``) so
232
+ ``branches --prune`` compares like with like — a git branch named
233
+ ``main`` yields ``_main`` here, protecting its store branch from pruning.
234
+ """
235
+ base = start or Path.cwd()
236
+ try:
237
+ result = subprocess.run(
238
+ [
239
+ "git",
240
+ "-C",
241
+ str(base),
242
+ "for-each-ref",
243
+ "--format=%(refname:short)",
244
+ "refs/heads",
245
+ ],
246
+ capture_output=True,
247
+ text=True,
248
+ check=False,
249
+ )
250
+ except OSError:
251
+ return None
252
+ if result.returncode != 0:
253
+ return None
254
+ return frozenset(
255
+ branch_store_name(line) for line in result.stdout.splitlines() if line.strip()
256
+ )
257
+
258
+
259
+ def root(start: Path | None = None) -> Path | None:
260
+ """Return the git repository root for ``start`` (or the cwd).
261
+
262
+ Prefers ``git rev-parse --show-toplevel`` so worktrees and submodules (where
263
+ ``.git`` is a file, not a directory) resolve correctly; falls back to walking
264
+ for ``.git/config`` when the git binary is unavailable.
265
+ """
266
+ base = start or Path.cwd()
267
+ try:
268
+ result = subprocess.run(
269
+ ["git", "-C", str(base), "rev-parse", "--show-toplevel"],
270
+ capture_output=True,
271
+ text=True,
272
+ check=False,
273
+ )
274
+ if result.returncode == 0 and result.stdout.strip():
275
+ return Path(result.stdout.strip())
276
+ except OSError:
277
+ pass
278
+
279
+ git_config = _find_git_config(base)
280
+ if git_config is None:
281
+ return None
282
+ return git_config.parent.parent
@@ -0,0 +1,82 @@
1
+ // Cross-Repo Context Bridge (Layer 2.5) — shared LOCAL store (_bridge.omni).
2
+ //
3
+ // One flat node per interface binding. Cross-repo linkages are computed by
4
+ // GROUPING bindings on (kind, key_norm) where roles/repos differ — there are
5
+ // NO link edges. An anchor-node + edge model would force every repo touching a
6
+ // shared contract (e.g. env_var DATABASE_URL) to upsert the same node, maxing
7
+ // write contention on a store with many concurrent writers (every repo's index
8
+ // run + every PostToolUse reindex hook). Flat bindings are each scoped to their
9
+ // own repo+file, so concurrent writers never touch the same row.
10
+ //
11
+ // Local-only and re-derivable, like the per-repo Layer-2 stores; never synced.
12
+ //
13
+ // Id convention:
14
+ // InterfaceBinding.slug = repo|file|kind|key_norm|role|symbol_id
15
+ // e.g. https://github.com/mitodl/mit-learn|main/settings.py|env_var|
16
+ // MITOL_APP_BASE_URL|consumer|...settings.py::<module>
17
+
18
+ node InterfaceBinding {
19
+ slug: String @key
20
+ kind: enum(env_var, package, service, endpoint) @index
21
+ key: String @index // raw as written (METHOD path, pkg name, env NAME)
22
+ key_norm: String @index // normalized join key (collapsed path params, …)
23
+ role: enum(provider, consumer, shared) @index
24
+ repo: String @index // canonical HTTPS repo URI (join key to Layer 2)
25
+ file: String @index // repo-relative source path of the binding
26
+ repo_file: String @index // "repo|file" — single-field key for per-file delete
27
+ // (delete `where` takes one condition, no compound `and`)
28
+ sub_kind: String? // service anchor variant: repo | image | name
29
+ symbol_id: String? // enclosing Symbol id (repo#path::Qn) when applicable
30
+ line: I32?
31
+ language: String?
32
+ framework: String? // django | drf | pulumi | npm | nextjs | …
33
+ generic: String? // "1" for stoplisted generic keys (DEBUG, PORT, …)
34
+ confidence: F32? // 0.0–1.0 endpoint-consumer trust score (phantom
35
+ // suppression); null/absent treated as 1.0 by readers
36
+ symbol: String? @index // canonical symbol string (docs/SYMBOL_FORMAT.md):
37
+ // {scheme}:{manager}:{package}:{version}:{descriptor}
38
+ // Stage-2 precise join key; "."-fields = unresolved
39
+ indexed_at: DateTime
40
+ }
41
+
42
+ // Per-repo symbol table (Stage 1 artifact — docs/SYMBOL_TABLE.md): one row per
43
+ // (repo, role, symbol), aggregated from that repo's InterfaceBinding rows on
44
+ // every bridge write. role=exported is the repo's public contract surface;
45
+ // role=external is an unresolved reference to another repo's contract (a
46
+ // RANGER-style import placeholder, redirected at read time by Stage 2 — no
47
+ // cross-repo edges are ever written). Rows are repo-scoped, so concurrent
48
+ // writers never touch the same row (same argument as InterfaceBinding).
49
+ node RepoSymbol {
50
+ slug: String @key // repo|role|symbol
51
+ repo: String @index // canonical HTTPS repo URI
52
+ role: enum(exported, external) @index
53
+ symbol: String @index // full canonical string (docs/SYMBOL_FORMAT.md)
54
+ scheme: String @index // http | env | pkg | svc
55
+ descriptor: String @index // precise Stage-2 join key (with scheme)
56
+ key_norm: String @index // coarse join key — descriptor minus the
57
+ // method for http (consumer methods are "*")
58
+ manager: String? // "." = unknown
59
+ package: String? // "." = unresolved (typical for external)
60
+ version: String?
61
+ kind: String // binding kind: env_var | package | service | endpoint
62
+ n_refs: I32 // occurrence count in this repo
63
+ confidence: F32? // max occurrence confidence; 1.0 for exported
64
+ file: String? // exemplar occurrence (deterministic: min file/line)
65
+ line: I32?
66
+ indexed_at: DateTime
67
+ }
68
+
69
+ // One row per indexed repo: its declared (or fallback) package identity from
70
+ // witan-code.toml (docs/PACKAGE_MAP.md). Overwritten on each full-repo index
71
+ // (merge by slug). Qualifies provider symbols and backs the
72
+ // known_provider_package confidence heuristic.
73
+ node PackageMap {
74
+ slug: String @key // canonical repo URI
75
+ repo: String @index
76
+ name: String @index // canonical package name
77
+ manager: String // pypi | npm | "."
78
+ version: String // "main" = trunk-tracking
79
+ provides: String? // JSON array of extra "manager:name" identities
80
+ declared: String? // "1" when read from witan-code.toml (vs fallback)
81
+ indexed_at: DateTime
82
+ }
@@ -0,0 +1,51 @@
1
+ // Code Graph (Layer 2) — per-repo tree-sitter-derived symbol graph.
2
+ //
3
+ // One store per repository, kept LOCAL only (never synced to team S3).
4
+ // Composes with witan (Layer 1) via soft symbol-ID references:
5
+ // Layer-1 nodes store symbol ids of the form `repo#path::Qualified.Name`,
6
+ // which resolve here without a hard cross-store edge.
7
+ //
8
+ // Id conventions:
9
+ // CodeFile.slug = repo#relative/path.py
10
+ // Symbol.slug = repo#relative/path.py::QualifiedName
11
+ // e.g. github.com/mitodl/repo#app/svc.py::Service.run
12
+
13
+ node CodeFile {
14
+ slug: String @key
15
+ repo: String @index
16
+ path: String @index
17
+ language: String @index
18
+ content_hash: String
19
+ indexed_at: DateTime
20
+ }
21
+
22
+ node Symbol {
23
+ slug: String @key
24
+ repo: String @index
25
+ file_id: String @index
26
+ name: String @index
27
+ qualified_name: String @index
28
+ kind: enum(function, method, class, module, variable, interface, type, enum, key, table, cte, block) @index
29
+ start_line: I32
30
+ end_line: I32
31
+ signature: String?
32
+ docstring: String?
33
+ decorators: [String]? // @decorators on the def (framework semantics: routes, tasks, fixtures, …)
34
+ indexed_at: DateTime
35
+ }
36
+
37
+ // Defines: a file defines a symbol (top-level or nested).
38
+ edge Defines: CodeFile -> Symbol
39
+
40
+ // Contains: lexical nesting (class Contains method, module Contains class).
41
+ edge Contains: Symbol -> Symbol
42
+
43
+ // Calls / References: HEURISTIC name-resolved usage edges (best-effort).
44
+ edge Calls: Symbol -> Symbol
45
+ edge References: Symbol -> Symbol
46
+
47
+ // Imports: a file imports a symbol (best-effort module/name resolution).
48
+ edge Imports: CodeFile -> Symbol
49
+
50
+ // Inherits: a class inherits from a base class (best-effort resolution).
51
+ edge Inherits: Symbol -> Symbol