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 ADDED
File without changes
witan_code/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import cli as main
2
+
3
+ if __name__ == "__main__":
4
+ main()
witan_code/_detach.py ADDED
@@ -0,0 +1,27 @@
1
+ """Cross-platform detached-subprocess spawning.
2
+
3
+ ``subprocess.Popen``'s ``start_new_session=True`` (setsid) is POSIX-only — a
4
+ no-op on Windows, where a background hook child can otherwise get torn down
5
+ with its parent's process group/console. Used by every hook that must spawn
6
+ work and return immediately: the SessionStart indexer and the throttled
7
+ optimize checkpoint (maintenance.py).
8
+
9
+ Deliberately duplicated in witan/witan/_detach.py (no cross-package import,
10
+ matching this package's existing convention — see graph.py's docstring).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import sys
17
+
18
+
19
+ def popen_detached(args: list[str], **kwargs: object) -> subprocess.Popen:
20
+ """Spawn ``args`` fully detached from the current process, cross-platform."""
21
+ if sys.platform == "win32":
22
+ kwargs["creationflags"] = (
23
+ subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
24
+ )
25
+ else:
26
+ kwargs["start_new_session"] = True
27
+ return subprocess.Popen(args, **kwargs) # noqa: S603 — caller controls argv
witan_code/bridge.py ADDED
@@ -0,0 +1,392 @@
1
+ """Write path for the shared cross-repo bridge store (_bridge.omni).
2
+
3
+ Runs as a SEPARATE phase after the per-repo store write completes, so the two
4
+ stores' advisory write locks are never held at once (no nesting → no deadlock),
5
+ and a bridge failure can't corrupt a per-repo store that already succeeded.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ from . import config as cfg_module
13
+ from . import package_map
14
+ from .bridge_extractors import (
15
+ ParsedBinding,
16
+ adjust_confidence,
17
+ canonical_symbol,
18
+ parse_symbol,
19
+ )
20
+ from .graph import OmnigraphClient
21
+ from .store import bridge_store, ensure_bridge_store
22
+
23
+
24
+ def _now_iso() -> str:
25
+ return datetime.now(timezone.utc).isoformat()
26
+
27
+
28
+ def write_bindings(
29
+ bindings: list[ParsedBinding],
30
+ repo: str,
31
+ cfg: cfg_module.Config,
32
+ *,
33
+ full_repo: bool,
34
+ touched_files: tuple[str, ...] = (),
35
+ identity: package_map.PackageIdentity | None = None,
36
+ base: Path | None = None,
37
+ branch: str | None = None,
38
+ ) -> int:
39
+ """Purge stale bindings and merge in fresh ones for ``repo``.
40
+
41
+ Purging is always per-file: the files (re)parsed this run, the files the
42
+ fresh batch carries bindings for (covers Tier B sources like OpenAPI
43
+ specs, which aren't in ``touched_files``), and — on a full-repo run with
44
+ ``base`` — files whose stored bindings no longer exist on disk. An
45
+ incremental full-repo index skips unchanged files, so a repo-wide purge
46
+ here would drop those files' bindings with nothing in the batch to
47
+ restore them.
48
+
49
+ Store-level confidence adjustments (self_provided_key, known_provider_package)
50
+ are applied here before writing. The cross-repo half of each signal is
51
+ sourced from other repos' Stage 1 symbol tables (RepoSymbol `exported`
52
+ rows, docs/SYMBOL_TABLE.md) — the stable, deduplicated artifact those
53
+ repos' own writes already produced. This repo's own contribution can't
54
+ come from the same place: its Stage 1 table hasn't been rebuilt for THIS
55
+ write yet (that happens further down), so it's read directly from the
56
+ surviving + fresh bindings already in hand.
57
+
58
+ ``branch`` is the per-repo omnigraph branch name (``repo_module.
59
+ store_branch()`` — already sanitized, ``None`` for the default git
60
+ branch). When set, every read/write in this call targets the
61
+ repo-qualified bridge branch ``<sanitized-repo-slug>/<branch>``
62
+ (docs/BRANCH_INDEXING.md § Bridge store) instead of bridge ``main``: an
63
+ overlay forked once from bridge main, so it starts as every repo's main
64
+ bindings and then only ``repo``'s own writes land on it — the shared
65
+ main view never sees in-flight branch bindings, while a read scoped to
66
+ this branch sees this repo's in-flight state overlaid on everyone else's
67
+ (possibly since-updated) main.
68
+
69
+ Returns the number of binding records written.
70
+ """
71
+ # Skip creating the store for a contract-less repo on its first index.
72
+ if not bindings and not bridge_store(cfg).exists():
73
+ return 0
74
+
75
+ identity = identity or package_map.fallback_identity(repo)
76
+ store = ensure_bridge_store(cfg)
77
+ bridge_branch = f"{cfg_module.sanitize_slug(repo)}/{branch}" if branch else None
78
+ client = OmnigraphClient(str(store), cfg.queries_dir, branch=bridge_branch)
79
+ # The reads below never fork; create the branch (from bridge main) first.
80
+ client.ensure_branch()
81
+
82
+ try:
83
+ all_rows = client.read("bridge.gq", "all_bindings", {})
84
+ except Exception: # noqa: BLE001 — store may be empty or query unavailable
85
+ all_rows = []
86
+
87
+ # provider_keys/provider_pkg_slugs only feed adjust_confidence, which only
88
+ # runs for endpoint consumer bindings — skip the extra full-store
89
+ # RepoSymbol read entirely when this batch has none to adjust.
90
+ has_endpoint_consumers = any(
91
+ b.kind == "endpoint" and b.role == "consumer" for b in bindings
92
+ )
93
+ repo_symbol_rows = _read_repo_symbols(client) if has_endpoint_consumers else []
94
+
95
+ purge_files = set(touched_files) | {b.file for b in bindings}
96
+ if full_repo and base is not None:
97
+ purge_files |= {
98
+ r["file"]
99
+ for r in all_rows
100
+ if r.get("repo") == repo
101
+ and r.get("file")
102
+ and not (base / r["file"]).exists()
103
+ }
104
+ for rel in sorted(purge_files):
105
+ client.change(
106
+ "bridge.gq", "delete_bindings_in_file", {"repo_file": f"{repo}|{rel}"}
107
+ )
108
+
109
+ # Collect store-level context for confidence adjustments.
110
+ # provider_keys: (repo, key_norm) pairs so the self_provided_key penalty
111
+ # fires when the same repo both provides and consumes a key_norm. OTHER
112
+ # repos' pairs come from their Stage 1 symbol table (exported rows); this
113
+ # repo's own pairs come from its surviving (not-purged) + fresh provider
114
+ # bindings, since its own Stage 1 table is stale until the rebuild below.
115
+ # provider_pkg_slugs: key_norm values for package providers from OTHER
116
+ # repos' Stage 1 symbol tables, plus package names other repos DECLARE
117
+ # via their package map — the map is the source of truth for identities
118
+ # a repo doesn't necessarily emit a binding for.
119
+ provider_keys: frozenset[tuple[str, str]] = (
120
+ frozenset(
121
+ (r["repo"], r["key_norm"])
122
+ for r in repo_symbol_rows
123
+ if r.get("role") == "exported"
124
+ and r.get("repo")
125
+ and r.get("key_norm")
126
+ and r.get("repo") != repo
127
+ )
128
+ | frozenset(
129
+ (r["repo"], r["key_norm"])
130
+ for r in all_rows
131
+ if r.get("role") == "provider"
132
+ and r.get("repo") == repo
133
+ and r.get("key_norm")
134
+ and r.get("file") not in purge_files
135
+ )
136
+ | frozenset(
137
+ (repo, b.key_norm) for b in bindings if b.role == "provider" and b.key_norm
138
+ )
139
+ )
140
+ provider_pkg_slugs: frozenset[str] = frozenset(
141
+ r["key_norm"]
142
+ for r in repo_symbol_rows
143
+ if r.get("role") == "exported"
144
+ and r.get("kind") == "package"
145
+ and r.get("repo") != repo
146
+ and r.get("key_norm")
147
+ ) | _declared_provider_packages(client, exclude_repo=repo)
148
+
149
+ # Apply store-level confidence adjustments to endpoint consumer bindings.
150
+ adjusted: list[ParsedBinding] = []
151
+ for b in bindings:
152
+ if b.kind == "endpoint" and b.role == "consumer":
153
+ # known_provider_package: True when the same source file also
154
+ # contains a *package* consumer binding whose key_norm matches a
155
+ # provider package slug from another repo in the bridge store.
156
+ # Endpoint bindings don't carry package-import info themselves;
157
+ # _file_imports_known_provider checks co-located package consumers
158
+ # to supply this signal.
159
+ has_known_pkg = _file_imports_known_provider(
160
+ b.file, bindings, provider_pkg_slugs
161
+ )
162
+ adjust_confidence(
163
+ b,
164
+ consumer_repo=repo,
165
+ provider_keys=provider_keys,
166
+ has_known_provider_package=has_known_pkg,
167
+ )
168
+ adjusted.append(b)
169
+
170
+ for b in adjusted:
171
+ b.symbol = canonical_symbol(b, identity)
172
+
173
+ # Rebuild the repo's symbol table (docs/SYMBOL_TABLE.md) from every binding
174
+ # occurrence that survives this write: stored rows outside the purge set
175
+ # plus the fresh batch. Bindings are per-occurrence; the table is the
176
+ # deduplicated per-symbol artifact Stage 2 joins against.
177
+ surviving = [
178
+ r
179
+ for r in all_rows
180
+ if r.get("repo") == repo and r.get("file") not in purge_files
181
+ ]
182
+ client.change("bridge.gq", "delete_repo_symbols", {"repo": repo})
183
+
184
+ records = _dedupe([_record(b, repo) for b in adjusted])
185
+ n_bindings = len(records)
186
+ records.extend(_symbol_table_records(repo, surviving, adjusted))
187
+ if full_repo:
188
+ records.append(_package_map_record(identity, repo))
189
+ client.load(records, mode="merge")
190
+ return n_bindings
191
+
192
+
193
+ # Binding role → symbol-table role. Providers export contract surface;
194
+ # consumers hold unresolved external references (RANGER import placeholders).
195
+ # The legacy "shared" role has no table semantics and is skipped.
196
+ _TABLE_ROLE = {"provider": "exported", "consumer": "external"}
197
+
198
+
199
+ def _symbol_table_records(
200
+ repo: str,
201
+ surviving: list[dict],
202
+ fresh: list[ParsedBinding],
203
+ ) -> list[dict]:
204
+ """One RepoSymbol record per (role, symbol) across the given occurrences.
205
+
206
+ Aggregates: ``n_refs`` counts occurrences, ``confidence`` keeps the max
207
+ (absent treated as 1.0, matching readers of InterfaceBinding.confidence),
208
+ and the exemplar file/line is the lexicographic minimum so rebuilds are
209
+ deterministic. Rows without a symbol (pre-symbol stores) are skipped —
210
+ they regain table coverage on the next reindex of their file.
211
+ """
212
+ entries = [
213
+ (
214
+ r.get("symbol"),
215
+ r.get("kind"),
216
+ r.get("role"),
217
+ r.get("key_norm"),
218
+ r.get("file"),
219
+ r.get("line"),
220
+ r.get("confidence"),
221
+ )
222
+ for r in surviving
223
+ ] + [
224
+ (b.symbol, b.kind, b.role, b.key_norm, b.file, b.line, b.confidence)
225
+ for b in fresh
226
+ ]
227
+
228
+ agg: dict[tuple[str, str], dict] = {}
229
+ for symbol, kind, role, key_norm, file, line, confidence in entries:
230
+ table_role = _TABLE_ROLE.get(role or "")
231
+ if not symbol or table_role is None:
232
+ continue
233
+ conf = 1.0 if confidence is None else float(confidence)
234
+ loc = (file or "", line if isinstance(line, int) else 0)
235
+ row = agg.get((table_role, symbol))
236
+ if row is None:
237
+ agg[(table_role, symbol)] = {
238
+ "kind": kind,
239
+ "key_norm": key_norm,
240
+ "n_refs": 1,
241
+ "confidence": conf,
242
+ "loc": loc,
243
+ }
244
+ else:
245
+ row["n_refs"] += 1
246
+ row["confidence"] = max(row["confidence"], conf)
247
+ row["loc"] = min(row["loc"], loc)
248
+
249
+ now = _now_iso()
250
+ out: list[dict] = []
251
+ for (table_role, symbol), row in agg.items():
252
+ parsed = parse_symbol(symbol)
253
+ if parsed is None:
254
+ continue
255
+ file, line = row["loc"]
256
+ out.append(
257
+ {
258
+ "type": "RepoSymbol",
259
+ "data": {
260
+ "slug": f"{repo}|{table_role}|{symbol}",
261
+ "repo": repo,
262
+ "role": table_role,
263
+ "symbol": symbol,
264
+ "scheme": parsed.scheme,
265
+ "descriptor": parsed.descriptor,
266
+ "key_norm": row["key_norm"],
267
+ "manager": parsed.manager,
268
+ "package": parsed.package,
269
+ "version": parsed.version,
270
+ "kind": row["kind"],
271
+ "n_refs": row["n_refs"],
272
+ "confidence": row["confidence"],
273
+ "file": file or None,
274
+ "line": line or None,
275
+ "indexed_at": now,
276
+ },
277
+ }
278
+ )
279
+ return out
280
+
281
+
282
+ def _read_repo_symbols(client: OmnigraphClient) -> list[dict]:
283
+ """Every RepoSymbol row in the bridge store, best-effort.
284
+
285
+ Empty on a store that predates Stage 1 (no RepoSymbol node yet) or any
286
+ other query failure — the confidence heuristics that use this degrade to
287
+ their pre-Stage-1 baseline (no cross-repo boost/penalty) rather than
288
+ aborting the write.
289
+ """
290
+ try:
291
+ return client.read("bridge.gq", "all_repo_symbols", {})
292
+ except Exception: # noqa: BLE001 — store may predate RepoSymbol
293
+ return []
294
+
295
+
296
+ def _declared_provider_packages(
297
+ client: OmnigraphClient, *, exclude_repo: str
298
+ ) -> frozenset[str]:
299
+ """Package names declared by other repos' package maps in the bridge store."""
300
+ try:
301
+ rows = client.read("bridge.gq", "all_package_maps", {})
302
+ except Exception: # noqa: BLE001 — store may predate the PackageMap node
303
+ return frozenset()
304
+ names: set[str] = set()
305
+ for row in rows:
306
+ if row.get("repo") == exclude_repo:
307
+ continue
308
+ if row.get("name"):
309
+ names.add(row["name"])
310
+ try:
311
+ provides = json.loads(row.get("provides") or "[]")
312
+ except (TypeError, ValueError):
313
+ provides = []
314
+ if not isinstance(provides, list):
315
+ provides = []
316
+ for entry in provides:
317
+ if isinstance(entry, str):
318
+ _, _, name = entry.partition(":")
319
+ names.add(name or entry)
320
+ return frozenset(names)
321
+
322
+
323
+ def _package_map_record(identity: package_map.PackageIdentity, repo: str) -> dict:
324
+ return {
325
+ "type": "PackageMap",
326
+ "data": {
327
+ "slug": repo,
328
+ "repo": repo,
329
+ "name": identity.name,
330
+ "manager": identity.manager,
331
+ "version": identity.version,
332
+ "provides": json.dumps(list(identity.provides))
333
+ if identity.provides
334
+ else None,
335
+ "declared": "1" if identity.declared else None,
336
+ "indexed_at": _now_iso(),
337
+ },
338
+ }
339
+
340
+
341
+ def _file_imports_known_provider(
342
+ file: str,
343
+ bindings: list[ParsedBinding],
344
+ provider_pkg_slugs: frozenset[str],
345
+ ) -> bool:
346
+ """Return True if any package consumer binding in ``file`` matches a known provider package."""
347
+ for b in bindings:
348
+ if b.file == file and b.kind == "package" and b.role == "consumer":
349
+ if b.key_norm in provider_pkg_slugs:
350
+ return True
351
+ return False
352
+
353
+
354
+ def _record(b: ParsedBinding, repo: str) -> dict:
355
+ symbol_id = b.symbol_id or ""
356
+ sub_kind = b.sub_kind or ""
357
+ slug = f"{repo}|{b.file}|{b.kind}|{sub_kind}|{b.key_norm}|{b.role}|{symbol_id}"
358
+ return {
359
+ "type": "InterfaceBinding",
360
+ "data": {
361
+ "slug": slug,
362
+ "kind": b.kind,
363
+ "sub_kind": b.sub_kind or None,
364
+ "key": b.key,
365
+ "key_norm": b.key_norm,
366
+ "role": b.role,
367
+ "repo": repo,
368
+ "file": b.file,
369
+ "repo_file": f"{repo}|{b.file}",
370
+ "symbol_id": b.symbol_id or None,
371
+ "line": b.line,
372
+ "language": b.language,
373
+ "framework": b.framework,
374
+ "generic": "1" if b.generic else None,
375
+ "confidence": b.confidence,
376
+ "symbol": b.symbol,
377
+ "indexed_at": _now_iso(),
378
+ },
379
+ }
380
+
381
+
382
+ def _dedupe(records: list[dict]) -> list[dict]:
383
+ """Keep the first record per slug — a duplicate slug fails the whole load."""
384
+ seen: set[str] = set()
385
+ out: list[dict] = []
386
+ for record in records:
387
+ slug = record["data"]["slug"]
388
+ if slug in seen:
389
+ continue
390
+ seen.add(slug)
391
+ out.append(record)
392
+ return out