@polycode-projects/seonix 0.1.0 → 0.2.1
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.
- package/bin/cli.mjs +95 -12
- package/package.json +5 -2
- package/src/ask-vocab.mjs +258 -0
- package/src/ask.mjs +762 -0
- package/src/browser.mjs +769 -0
- package/src/codegraph.mjs +352 -23
- package/src/cs_treesitter.mjs +2 -2
- package/src/extract.mjs +51 -8
- package/src/extract_lang.mjs +17 -2
- package/src/java_javaparser.mjs +54 -0
- package/src/java_treesitter.mjs +216 -0
- package/src/jsts_tsc.mjs +2 -2
- package/src/prose.mjs +145 -0
- package/src/schema-docs.mjs +225 -0
- package/src/server.mjs +35 -4
- package/src/temporal.mjs +559 -0
- package/src/viz.mjs +666 -94
- package/src/walk.mjs +52 -5
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// schema-docs.mjs — the single source of truth for seonix's own ontology documentation.
|
|
2
|
+
//
|
|
3
|
+
// seonix's typed graph (packages/seonix/src/extract.mjs's buildEntities, queried by
|
|
4
|
+
// codegraph.mjs) has had its schema documented since day one, but only in scattered code
|
|
5
|
+
// comments (extract.mjs's header, the `note` fields on some — not all — of the `vocabulary`
|
|
6
|
+
// array entries it emits) — readable by a human editing the source, invisible to anything
|
|
7
|
+
// that only sees graph.json. This file completes that documentation (every entity class,
|
|
8
|
+
// every predicate actually emitted — verified against real `prop:`/`class:` literals in
|
|
9
|
+
// extract.mjs, not just what the existing vocabulary array already claimed) and
|
|
10
|
+
// `ingestSchemaDocs()` (called once, from `indexRepository`, see extract.mjs) merges it
|
|
11
|
+
// into every graph build so a question like "what does cochange mean" or "what is a
|
|
12
|
+
// Commit" is answerable by querying the SAME graph the same way any other question is —
|
|
13
|
+
// not via separate hardcoded documentation logic.
|
|
14
|
+
//
|
|
15
|
+
// GLOBAL, NOT PER-REPO: this documentation does not vary by repository — "Module" means
|
|
16
|
+
// the same thing indexing Django or a JS library — so it is static, committed data
|
|
17
|
+
// (this file), never recomputed at index time. Ingesting it is a fixed-size merge, not a
|
|
18
|
+
// computation (see the ~0ms measured delta in schema-docs.test.mjs / extract.test.mjs).
|
|
19
|
+
//
|
|
20
|
+
// Mirrors the pattern in marginalia's app/lib/vocab.mjs (a `comment`/description per
|
|
21
|
+
// schema term, single-sourced) minus the RDF/OWL external-alignment machinery, which
|
|
22
|
+
// doesn't apply here — seonix's schema is code-relationship-specific, not general-domain.
|
|
23
|
+
|
|
24
|
+
// ---- entity classes (7 — verified against every `class: "X"` individual literal in
|
|
25
|
+
// extract.mjs) -----------------------------------------------------------------------
|
|
26
|
+
export const CLASS_DOCS = Object.freeze([
|
|
27
|
+
{ name: "Module", description:
|
|
28
|
+
"A source file — one importable unit within the indexed repository (a .py/.mjs/.ts/" +
|
|
29
|
+
".cs/.java file, etc). The coarsest unit seonix ranks, locates, and injects as a " +
|
|
30
|
+
"digest; every other entity class belongs to exactly one Module." },
|
|
31
|
+
{ name: "Class", description:
|
|
32
|
+
"A class definition inside a Module. Contains Methods and Attributes (seon:" +
|
|
33
|
+
"containsCodeEntity) and may declare a base class (seon:hasSuperType)." },
|
|
34
|
+
{ name: "Function", description:
|
|
35
|
+
"A top-level, module-scope function definition — not a method (methods belong to a " +
|
|
36
|
+
"Class and are their own entity class)." },
|
|
37
|
+
{ name: "Method", description:
|
|
38
|
+
"A function defined inside a Class — an instance, static, or class method. Distinct " +
|
|
39
|
+
"from Function so class membership (seon:containsCodeEntity) and free functions " +
|
|
40
|
+
"never get confused." },
|
|
41
|
+
{ name: "Attribute", description:
|
|
42
|
+
"A field or property belonging to a Class — either a class-level assignment or a " +
|
|
43
|
+
"self-scoped instance field seen in a method body (self.<name> =). Distinct from a " +
|
|
44
|
+
"module-level GlobalVariable." },
|
|
45
|
+
{ name: "GlobalVariable", description:
|
|
46
|
+
"A module-level variable or constant assignment that belongs to no Class or " +
|
|
47
|
+
"function — a name defined directly in a Module's top-level scope." },
|
|
48
|
+
{ name: "Commit", description:
|
|
49
|
+
"A single recorded git commit. Carries author/date/message attributes and connects " +
|
|
50
|
+
"to the Modules it touched (mgx:touchedByCommit) and, more precisely, the specific " +
|
|
51
|
+
"symbols whose current source span its changed lines intersect (mgx:touchesSymbol)." },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// ---- predicates / attributes (every `prop:` token actually emitted by buildEntities,
|
|
55
|
+
// verified by grep against extract.mjs — not just what the pre-existing `vocabulary`
|
|
56
|
+
// array already documented; three real gaps found this way: seon:startsAt, mgx:value,
|
|
57
|
+
// mgx:dotted, plus the prose second-pass's mgx:hasProseTokens) ------------------------
|
|
58
|
+
export const PREDICATE_DOCS = Object.freeze([
|
|
59
|
+
// ---- object properties (edges between individuals) ----
|
|
60
|
+
{ prop: "mgx:importsNamespace", kind: "imports", description:
|
|
61
|
+
"Module → Module. The subject module has a top-level import statement that resolves " +
|
|
62
|
+
"to a module already present in this repository's index. External/unresolved " +
|
|
63
|
+
"imports are dropped, never guessed." },
|
|
64
|
+
{ prop: "mgx:callsCoarse", kind: "calls", description:
|
|
65
|
+
"Module → Module. An import-backed, module-granular \"this file calls into that " +
|
|
66
|
+
"file\" edge — not resolved to a specific symbol (see callsSymbol for that)." },
|
|
67
|
+
{ prop: "mgx:callsSymbol", kind: "callsSymbol", description:
|
|
68
|
+
"Function/Method → Function/Class. A call site resolved to exactly one same-named " +
|
|
69
|
+
"definition in the repository — unambiguous names only; an ambiguous or external " +
|
|
70
|
+
"call is dropped rather than guessed (no wrong edge)." },
|
|
71
|
+
{ prop: "seon:declaresMethod", kind: "defines", description:
|
|
72
|
+
"Module → Function/Class/Method/Attribute/GlobalVariable. What a module's top-level " +
|
|
73
|
+
"scope actually declares." },
|
|
74
|
+
{ prop: "seon:containsCodeEntity", kind: "contains", description:
|
|
75
|
+
"Class → Method/Attribute. Class membership — which methods and fields belong to " +
|
|
76
|
+
"which class." },
|
|
77
|
+
{ prop: "mgx:touchedByCommit", kind: "touches", description:
|
|
78
|
+
"Commit → Module. From `git log --name-only`, restricted to modules already in the " +
|
|
79
|
+
"index — which files a commit's diff lists, at file granularity." },
|
|
80
|
+
{ prop: "mgx:touchesSymbol", kind: "touchesSymbol", description:
|
|
81
|
+
"Commit → Function/Method/Class/Attribute. A commit's changed-line-range intersected " +
|
|
82
|
+
"with a symbol's current source span — WHICH specific function or class a commit " +
|
|
83
|
+
"actually edited, not just which file it touched." },
|
|
84
|
+
{ prop: "mgx:testsCoverage", kind: "tests", description:
|
|
85
|
+
"Module → Module. A test module's own internal imports — a lightweight \"this test " +
|
|
86
|
+
"file exercises that source file\" signal, not line-level coverage-tool data." },
|
|
87
|
+
{ prop: "seon:hasSuperType", kind: "inherits", description:
|
|
88
|
+
"Class → Class. A class's base class, resolved to an internal Class when the name " +
|
|
89
|
+
"matches one defined in the repo; otherwise recorded as an external `ext:` reference." },
|
|
90
|
+
{ prop: "mgx:changeCoupledWith", kind: "cochange", description:
|
|
91
|
+
"Module ↔ Module. Two modules frequently committed together, independent of any " +
|
|
92
|
+
"import or call relationship — surfaces coupling that static analysis alone misses " +
|
|
93
|
+
"(e.g. a module and its test file, or two files that always change together for a " +
|
|
94
|
+
"reason no import edge captures)." },
|
|
95
|
+
{ prop: "mgx:reExports", kind: "reexports", description:
|
|
96
|
+
"Module → Function/Class. A module's public-API (`__all__`) entry that re-exports a " +
|
|
97
|
+
"symbol defined or imported elsewhere — answers \"where is X importable from,\" not " +
|
|
98
|
+
"just \"where is X defined.\"" },
|
|
99
|
+
{ prop: "mgx:hasProseTokens", kind: "prose", description:
|
|
100
|
+
"Individual → word tokens (an attribute, not a between-individuals edge). The " +
|
|
101
|
+
"decomposed word sequence extracted from an identifier's name (camelCase/snake_case " +
|
|
102
|
+
"split) and any doc-comment prose, used by the second-pass prose-to-symbol " +
|
|
103
|
+
"cross-reference index (PLAN_PROSE_INDEX.md) to resolve free-text object terms that " +
|
|
104
|
+
"don't exact-match an identifier." },
|
|
105
|
+
|
|
106
|
+
// ---- attributes (individual-scoped facts, not edges) ----
|
|
107
|
+
{ prop: "seon:startsAt", kind: "attribute", description:
|
|
108
|
+
"The `path:line` or `path:start-end` source location of a Function/Method/Class/" +
|
|
109
|
+
"Attribute/GlobalVariable — where in its Module the definition actually lives." },
|
|
110
|
+
{ prop: "mgx:dotted", kind: "attribute", description:
|
|
111
|
+
"A Module's dotted import name (e.g. `pkg.sub.mod` for Python), when the language " +
|
|
112
|
+
"has one — used to resolve import statements to internal modules." },
|
|
113
|
+
{ prop: "mgx:exportsAll", kind: "attribute", description:
|
|
114
|
+
"A Module's literal `__all__` membership list, stored even for entries that don't " +
|
|
115
|
+
"resolve to a real symbol — so the digest can always tell an agent \"this module has " +
|
|
116
|
+
"an __all__, add your new symbol to it.\"" },
|
|
117
|
+
{ prop: "mgx:decorator", kind: "attribute", description:
|
|
118
|
+
"Python/framework decorators applied to a Function/Method/Class definition (e.g. " +
|
|
119
|
+
"`@property`, `@app.route(...)`), as written." },
|
|
120
|
+
{ prop: "mgx:value", kind: "attribute", description:
|
|
121
|
+
"The literal right-hand-side value of a GlobalVariable's assignment, when the AST " +
|
|
122
|
+
"extractor could capture one cheaply (a constant or simple literal)." },
|
|
123
|
+
{ prop: "mgx:commitAuthor", kind: "attribute", description: "A Commit's author name (git %an)." },
|
|
124
|
+
{ prop: "mgx:commitDate", kind: "attribute", description:
|
|
125
|
+
"A Commit's author date, ISO-8601 (git %aI)." },
|
|
126
|
+
{ prop: "mgx:commitMessage", kind: "attribute", description:
|
|
127
|
+
"A Commit's subject line (git %s), capped to 120 characters." },
|
|
128
|
+
{ prop: "seon:hasParameter", kind: "attribute", description:
|
|
129
|
+
"A Function/Method's formal parameter list, as a signature string (the AST's " +
|
|
130
|
+
"unparsed argument list — not resolved types)." },
|
|
131
|
+
{ prop: "seon:hasReturnType", kind: "attribute", description:
|
|
132
|
+
"A Function/Method's return type ANNOTATION exactly as written — not an inferred or " +
|
|
133
|
+
"resolved type." },
|
|
134
|
+
{ prop: "seon:throwsException", kind: "attribute", description:
|
|
135
|
+
"Exception class names literally named in a `raise` statement inside the definition — " +
|
|
136
|
+
"not resolved to their own Class individuals." },
|
|
137
|
+
{ prop: "seon:catchesException", kind: "attribute", description:
|
|
138
|
+
"Exception type names literally named in an `except` handler inside the definition." },
|
|
139
|
+
{ prop: "seon:accessesField", kind: "attribute", description:
|
|
140
|
+
"`self.<field>` names a Method reads or writes — self-scoped only (an honest, " +
|
|
141
|
+
"narrow signal, not general data-flow analysis)." },
|
|
142
|
+
{ prop: "seon:isStatic", kind: "attribute", description:
|
|
143
|
+
"The definition is decorated `@staticmethod` or `@classmethod`." },
|
|
144
|
+
{ prop: "seon:isAbstract", kind: "attribute", description:
|
|
145
|
+
"The definition is decorated `@abstractmethod` or `@abstractproperty`." },
|
|
146
|
+
{ prop: "seon:isConstant", kind: "attribute", description:
|
|
147
|
+
"An ALL_CAPS module-level GlobalVariable — a naming-convention signal, not enforced " +
|
|
148
|
+
"immutability." },
|
|
149
|
+
{ prop: "seon:hasAccessModifier", kind: "attribute", description:
|
|
150
|
+
"Visibility inferred from a leading underscore (private/protected); a public member " +
|
|
151
|
+
"carries no value for this attribute." },
|
|
152
|
+
{ prop: "seon:hasDoc", kind: "attribute", description:
|
|
153
|
+
"The first line of a docstring, length-capped — a one-line purpose summary without " +
|
|
154
|
+
"the full docstring body." },
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const classDocByName = new Map(CLASS_DOCS.map((c) => [c.name, c]));
|
|
158
|
+
const predicateDocByProp = new Map(PREDICATE_DOCS.map((p) => [p.prop, p]));
|
|
159
|
+
|
|
160
|
+
/** Meta-individual ids are namespaced (`schema:class:`/`schema:predicate:`) so they can
|
|
161
|
+
* never collide with a real code entity's id (`mod:`/`fn:`/`commit:`) and are trivially
|
|
162
|
+
* filterable out of normal code-entity queries by anything that cares to. */
|
|
163
|
+
const classIndividual = ({ name, description }) => ({
|
|
164
|
+
id: `schema:class:${name}`,
|
|
165
|
+
label: name,
|
|
166
|
+
class: "SchemaClass",
|
|
167
|
+
derived_from: [],
|
|
168
|
+
mentions: [],
|
|
169
|
+
attributes: [{ prop: "mgx:schemaDoc", key: "doc", value: description }],
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const predicateIndividual = ({ prop, kind, description }) => ({
|
|
173
|
+
id: `schema:predicate:${prop}`,
|
|
174
|
+
// The label is the human-facing relation name (what a user would actually type — "what
|
|
175
|
+
// does cochange mean" — matching ask-vocab.mjs's VERB_TO_KIND/kind vocabulary), not the
|
|
176
|
+
// raw prop token; the token is kept as a separate attribute for exact lookup.
|
|
177
|
+
label: kind,
|
|
178
|
+
class: "SchemaPredicate",
|
|
179
|
+
derived_from: [],
|
|
180
|
+
mentions: [],
|
|
181
|
+
attributes: [
|
|
182
|
+
{ prop: "mgx:schemaDoc", key: "doc", value: description },
|
|
183
|
+
{ prop: "mgx:schemaToken", key: "token", value: prop },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/** Merge the static schema documentation into a just-built `entities` payload (the
|
|
188
|
+
* object `buildEntities` returns, before it's serialized to graph.json):
|
|
189
|
+
* - `entities.classes[].description` filled in for every known class,
|
|
190
|
+
* - `entities.vocabulary[].note` backfilled wherever it was missing (existing notes
|
|
191
|
+
* are left as-is — this only fills gaps, it doesn't overwrite a human's wording),
|
|
192
|
+
* - `entities.individuals` gains one SchemaClass + one SchemaPredicate individual per
|
|
193
|
+
* documented term, so the SAME graph traversal that answers "what calls X" can also
|
|
194
|
+
* answer "what is a Commit" or "what does cochange mean".
|
|
195
|
+
* Mutates and returns `entities`. Idempotent (safe to call more than once — individuals
|
|
196
|
+
* are only appended if not already present by id). Pure w.r.t. repo content: the output
|
|
197
|
+
* is identical regardless of what was indexed. */
|
|
198
|
+
export function ingestSchemaDocs(entities) {
|
|
199
|
+
if (!entities || typeof entities !== "object") return entities;
|
|
200
|
+
|
|
201
|
+
if (Array.isArray(entities.classes)) {
|
|
202
|
+
for (const c of entities.classes) {
|
|
203
|
+
const doc = classDocByName.get(c.name);
|
|
204
|
+
if (doc && c.description === undefined) c.description = doc.description;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(entities.vocabulary)) {
|
|
208
|
+
for (const v of entities.vocabulary) {
|
|
209
|
+
const doc = predicateDocByProp.get(v.prop);
|
|
210
|
+
if (doc && !v.note) v.note = doc.description;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
entities.individuals ||= [];
|
|
215
|
+
const existingIds = new Set(entities.individuals.map((i) => i?.id));
|
|
216
|
+
for (const c of CLASS_DOCS) {
|
|
217
|
+
const ind = classIndividual(c);
|
|
218
|
+
if (!existingIds.has(ind.id)) { entities.individuals.push(ind); existingIds.add(ind.id); }
|
|
219
|
+
}
|
|
220
|
+
for (const p of PREDICATE_DOCS) {
|
|
221
|
+
const ind = predicateIndividual(p);
|
|
222
|
+
if (!existingIds.has(ind.id)) { entities.individuals.push(ind); existingIds.add(ind.id); }
|
|
223
|
+
}
|
|
224
|
+
return entities;
|
|
225
|
+
}
|
package/src/server.mjs
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
// seonix_architecture, seonix_tests_for, seonix_untested, seonix_history, seonix_callers,
|
|
10
10
|
// seonix_callees. Each answers one question in ONE compact call so the agent need not
|
|
11
11
|
// Read/Grep. Errors reach the caller as clean tool errors — message only, never a stack.
|
|
12
|
+
//
|
|
13
|
+
// seonix_ask (PLAN_MECHANICAL_CHAT.md, hot tool): a mechanical, zero-model-call NL query
|
|
14
|
+
// over the graph — collapses the search+describe+traversal composition loop an agent would
|
|
15
|
+
// otherwise hand-compose into one deterministic round-trip. See ask.mjs.
|
|
12
16
|
|
|
13
17
|
import { readFile } from "node:fs/promises";
|
|
14
18
|
import { dirname, join } from "node:path";
|
|
@@ -46,6 +50,7 @@ import {
|
|
|
46
50
|
renderMethodHistory,
|
|
47
51
|
renderClassHistory,
|
|
48
52
|
} from "./codegraph.mjs";
|
|
53
|
+
import { ask } from "./ask.mjs";
|
|
49
54
|
|
|
50
55
|
const SNIPPET_MAX_LINES = 200;
|
|
51
56
|
|
|
@@ -82,6 +87,18 @@ export const TOOLS = [
|
|
|
82
87
|
},
|
|
83
88
|
},
|
|
84
89
|
},
|
|
90
|
+
{
|
|
91
|
+
name: "seonix_ask",
|
|
92
|
+
description:
|
|
93
|
+
"Ask a structural question in plain English (e.g. \"which functions call X\") — one call, no model, replaces manual search+describe+traversal. A clean miss beats a guess.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
required: ["query"],
|
|
97
|
+
properties: {
|
|
98
|
+
query: { type: "string", description: "A free-text question, e.g. \"which functions explicitly couple to logging\"." },
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
85
102
|
];
|
|
86
103
|
|
|
87
104
|
async function loadGraph(config, source) {
|
|
@@ -123,22 +140,26 @@ export async function buildContextBundle(args, { config, source = defaultSource,
|
|
|
123
140
|
if (!symbol) throw new ToolError("symbol is required");
|
|
124
141
|
const depth = String(args?.depth || "auto").trim().toLowerCase();
|
|
125
142
|
// Tuning-flag contract: `min` forces the LEANEST bundle (TINY mask, no top-up) regardless of
|
|
126
|
-
// exemplar length; `untuned` reproduces the
|
|
143
|
+
// exemplar length; `untuned` reproduces the earlier escalation (tuning #1 bypassed). Neither
|
|
127
144
|
// → the tuned default (sizeBundle's anchor-gated escalation).
|
|
128
145
|
const min = Boolean(args?.min);
|
|
129
146
|
const untuned = Boolean(args?.untuned);
|
|
147
|
+
// `max` forces the injection CEILING — FULL tier (every section + inlined depth-1 callee
|
|
148
|
+
// bodies) with top-up, and it OVERRIDES trim so even secondary modules get the full bundle. Used
|
|
149
|
+
// by the seonix-max arm to test whether more injection re-bloats.
|
|
150
|
+
const max = Boolean(args?.max);
|
|
130
151
|
const graph = await loadGraph(config, source);
|
|
131
152
|
const { match } = resolveOrThrow(graph, symbol, "symbol");
|
|
132
153
|
const plan = contextPlan(graph, match);
|
|
133
|
-
// #6/B1/B6: pick the section mask by depth — min forces TINY, full forces everything, auto
|
|
154
|
+
// #6/B1/B6: pick the section mask by depth — min forces TINY, full/max forces everything, auto
|
|
134
155
|
// runs the size classifier (lean TINY default + one-tier top-up when the edit needs it).
|
|
135
156
|
let tier;
|
|
136
157
|
let mask;
|
|
137
158
|
let topup = false;
|
|
138
159
|
if (min || depth === "min") { tier = "TINY"; mask = bundleMask("TINY"); }
|
|
139
|
-
else if (depth === "full") { tier = "FULL"; mask = bundleMask("FULL"); }
|
|
160
|
+
else if (max || depth === "full") { tier = "FULL"; mask = bundleMask("FULL"); topup = true; }
|
|
140
161
|
else ({ tier, mask, topup } = sizeBundle(plan, graph, { untuned }));
|
|
141
|
-
if (trim) mask = trimBundleMask(mask); // B2: secondary digest module → signatures + region only
|
|
162
|
+
if (trim && !max) mask = trimBundleMask(mask); // B2: secondary digest module → signatures + region only (max keeps the full bundle)
|
|
142
163
|
const repoRoot = dirname(dirname(config.graphFile));
|
|
143
164
|
let lines = null;
|
|
144
165
|
if (plan.moduleLabel) {
|
|
@@ -341,6 +362,16 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
341
362
|
const graph = await loadGraph(config, source);
|
|
342
363
|
return renderUntested(graph);
|
|
343
364
|
}
|
|
365
|
+
if (name === "seonix_ask") {
|
|
366
|
+
const query = String(args?.query || "").trim();
|
|
367
|
+
if (!query) throw new ToolError("query is required");
|
|
368
|
+
const graph = await loadGraph(config, source);
|
|
369
|
+
const { content, seonix_ask } = ask(graph, query);
|
|
370
|
+
// Every dispatchTool caller (this MCP handler, the CLI fallback) expects a plain string —
|
|
371
|
+
// append the structured envelope as a delimited, machine-parseable block rather than
|
|
372
|
+
// changing that shared contract for one tool. PLAN_MECHANICAL_CHAT.md §6.2.
|
|
373
|
+
return `${content}\n\n---seonix_ask---\n${JSON.stringify(seonix_ask, null, 2)}`;
|
|
374
|
+
}
|
|
344
375
|
if (
|
|
345
376
|
name === "seonix_tests_for" || name === "seonix_history" || name === "seonix_callers" ||
|
|
346
377
|
name === "seonix_callees" || name === "seonix_cochanges" || name === "seonix_calls" ||
|