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
witan_code/__init__.py
ADDED
|
File without changes
|
witan_code/__main__.py
ADDED
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
|