@polycode-projects/seonix 0.9.1 → 0.10.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/README.md +33 -12
- package/bin/cli.mjs +92 -11
- package/package.json +4 -9
- package/scripts/build-roslyn.mjs +11 -0
- package/src/ask-browser.bundle.js +136 -18
- package/src/browser.mjs +9 -3
- package/src/codegraph.mjs +126 -18
- package/src/cs_roslyn.mjs +13 -7
- package/src/cs_treesitter.mjs +58 -3
- package/src/extract.mjs +98 -23
- package/src/extract_lang.mjs +4 -2
- package/src/interfaces.mjs +110 -16
- package/src/jsts_tsc.mjs +51 -0
- package/src/roslyn_build.mjs +59 -0
- package/src/schema-docs.mjs +33 -3
- package/src/server.mjs +51 -7
- package/src/summary.mjs +14 -3
- package/src/telemetry.mjs +31 -0
|
@@ -952,12 +952,62 @@
|
|
|
952
952
|
);
|
|
953
953
|
var MISSPELLING_RE = correctionRe(MISSPELLINGS);
|
|
954
954
|
var WRONG_WORD_RE = correctionRe(WRONG_WORDS);
|
|
955
|
+
var RELATION_VERB_RE = new RegExp(
|
|
956
|
+
"\\b(?:" + Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
957
|
+
"i"
|
|
958
|
+
);
|
|
959
|
+
var INTERROGATIVE_LEAD_RE = /^(?:which|what|who|whose|where|when|why|how)\b/i;
|
|
960
|
+
var LISTING_TAIL_KINDS = /* @__PURE__ */ new Set([
|
|
961
|
+
"modules",
|
|
962
|
+
"files",
|
|
963
|
+
"functions",
|
|
964
|
+
"methods",
|
|
965
|
+
"classes",
|
|
966
|
+
"attributes",
|
|
967
|
+
"fields",
|
|
968
|
+
"properties",
|
|
969
|
+
"variables",
|
|
970
|
+
"globals",
|
|
971
|
+
"commits",
|
|
972
|
+
"changes",
|
|
973
|
+
"tests",
|
|
974
|
+
"members"
|
|
975
|
+
]);
|
|
976
|
+
var BARE_KIND_RE = /^(?:all\s+|the\s+)?(?:module|file|function|method|class|attribute|field|property|variable|global|commit|change|test|member)\??$/i;
|
|
977
|
+
var isListingRemainder = (rest) => {
|
|
978
|
+
if (BARE_KIND_RE.test(rest)) return true;
|
|
979
|
+
const words = rest.replace(/\?+\s*$/, "").trim().split(/\s+/);
|
|
980
|
+
return LISTING_TAIL_KINDS.has((words[words.length - 1] || "").toLowerCase());
|
|
981
|
+
};
|
|
982
|
+
var GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy)(?:\s+there)?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
|
|
983
|
+
var MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(?:[,\s]+please)?\??$/i;
|
|
984
|
+
var SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
985
|
+
function applyPreambleFrames(text) {
|
|
986
|
+
let q = String(text || "");
|
|
987
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
988
|
+
const before = q;
|
|
989
|
+
let m = q.match(GREETING_PREAMBLE_RE);
|
|
990
|
+
if (m) q = m[1].trim();
|
|
991
|
+
m = q.match(MODAL_WRAPPER_RE);
|
|
992
|
+
if (m) q = m[1].trim();
|
|
993
|
+
m = q.match(SHOW_GIVE_ME_RE);
|
|
994
|
+
if (m) {
|
|
995
|
+
const rest = m[1].trim();
|
|
996
|
+
if (!isListingRemainder(rest)) {
|
|
997
|
+
q = RELATION_VERB_RE.test(rest) || INTERROGATIVE_LEAD_RE.test(rest) ? rest : `describe ${rest}`;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (q === before) break;
|
|
1001
|
+
}
|
|
1002
|
+
return q;
|
|
1003
|
+
}
|
|
955
1004
|
function normalizeQuery(text) {
|
|
956
1005
|
let q = String(text || "");
|
|
957
1006
|
q = q.replace(CONTRACTION_RE, (m) => CONTRACTIONS[m.toLowerCase()]);
|
|
958
1007
|
q = q.replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
959
1008
|
q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
|
|
960
1009
|
q = q.replace(G_DROP, "$1ing");
|
|
1010
|
+
q = applyPreambleFrames(q);
|
|
961
1011
|
if (FILLER_WORDS.length) {
|
|
962
1012
|
const fillerRe = new RegExp(
|
|
963
1013
|
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
@@ -1045,8 +1095,24 @@
|
|
|
1045
1095
|
// separate authorship edge; "touched" IS the authorship signal (the churn commits
|
|
1046
1096
|
// carry the author), so these are true synonyms of "who touched X", not a new
|
|
1047
1097
|
// capability. Anaphora rides through untouched ("who wrote it" → "who touched it").
|
|
1048
|
-
|
|
1049
|
-
|
|
1098
|
+
// SHA GUARD (0.8.2 feel wave): a COMMIT object is NOT a synonym — "who is the
|
|
1099
|
+
// author of abc1234" rewritten to "who touched abc1234" dumps the commit's
|
|
1100
|
+
// touch-SET instead of naming its author. The negative lookahead refuses the
|
|
1101
|
+
// rewrite when the object is a bare (optionally "commit "-prefixed) 7-40 char
|
|
1102
|
+
// hex sha, leaving the un-rewritten form for the author lane to consume;
|
|
1103
|
+
// file/symbol objects (anything non-sha, e.g. "deadbeef.mjs") keep the rewrite.
|
|
1104
|
+
{ re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
|
|
1105
|
+
{ re: /^who\s+is\s+the\s+authors?\s+of\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
|
|
1106
|
+
// HAS-TESTS → the coverage question the RELATIONS table answers. "does X have
|
|
1107
|
+
// tests" parses "have" as a defines-verb (VERB_TO_KIND), producing the garbled
|
|
1108
|
+
// "No — no defines edge found from X to <whatever resolves>" receipt; "is X
|
|
1109
|
+
// tested" traverses tests edges from the WRONG side (subject = X). Both mean
|
|
1110
|
+
// the coverage question "what tests X" — rewrite onto it. Closed to a
|
|
1111
|
+
// tests/coverage object ("does X have methods/members" stays the members
|
|
1112
|
+
// family) and refuses any "not" in the subject span, so the set-complement
|
|
1113
|
+
// negations ("is X not tested") keep their own handler downstream.
|
|
1114
|
+
{ re: /^(?:does|do)\s+(?!.*\bnot\b)(.+?)\s+have\s+(?:any\s+)?(?:tests?|test\s+coverage|coverage)\??$/i, to: (m) => `what tests ${m[1]}` },
|
|
1115
|
+
{ re: /^(?:is|are)\s+(?!.*\bnot\b)(.+?)\s+tested\??$/i, to: (m) => `what tests ${m[1]}` },
|
|
1050
1116
|
// NEEDS-TESTS → the untested-module survey. "what needs tests" / "what needs
|
|
1051
1117
|
// testing" is the plainest way to ask which modules are uncovered, and it hit the
|
|
1052
1118
|
// grammar wall ("no module matching 'needs'…"). Route it onto the same attributive
|
|
@@ -1864,7 +1930,9 @@
|
|
|
1864
1930
|
|
|
1865
1931
|
// node_modules/@polycode-projects/the-mechanical-code-talker/src/interpret/merge.mjs
|
|
1866
1932
|
var DEFAULT_CONFIDENCE = 0.5;
|
|
1867
|
-
var cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ").replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");
|
|
1933
|
+
var cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ").replace(/^(?:the|a|an)\s+/, "").replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");
|
|
1934
|
+
var LEADING_DET_RE = /^\s*(?:the|a|an)\s+/i;
|
|
1935
|
+
var detCount = (p) => [p?.subject, p?.object].filter((t) => LEADING_DET_RE.test(String(t || ""))).length;
|
|
1868
1936
|
function sameParse(p, q) {
|
|
1869
1937
|
if (p.shape !== q.shape || p.kind !== q.kind) return false;
|
|
1870
1938
|
if (p.shape === "ask") return cmpTerm(p.subject) === cmpTerm(q.subject) && cmpTerm(p.object) === cmpTerm(q.object);
|
|
@@ -1904,6 +1972,7 @@
|
|
|
1904
1972
|
if (dup) {
|
|
1905
1973
|
dup.agreed += 1;
|
|
1906
1974
|
dup.confidence = Math.max(dup.confidence, c.confidence);
|
|
1975
|
+
if (detCount(c.parsed) < detCount(dup.parsed)) dup.parsed = c.parsed;
|
|
1907
1976
|
continue;
|
|
1908
1977
|
}
|
|
1909
1978
|
distinct.push({ ...c, agreed: 1 });
|
|
@@ -2049,11 +2118,15 @@
|
|
|
2049
2118
|
// node_modules/@polycode-projects/the-mechanical-code-talker/src/ask.mjs
|
|
2050
2119
|
function edgesOfKind(graph, kind) {
|
|
2051
2120
|
const out = [];
|
|
2052
|
-
for (const g of graph.relations)
|
|
2121
|
+
for (const g of graph.relations) {
|
|
2122
|
+
if (relationKind(g) !== kind) continue;
|
|
2123
|
+
for (const e of g.edges) out.push(e);
|
|
2124
|
+
}
|
|
2053
2125
|
return out;
|
|
2054
2126
|
}
|
|
2055
2127
|
var SYMBOL_GRAIN_SIBLING = { calls: "callsSymbol", touches: "touchesSymbol" };
|
|
2056
2128
|
var FINE_ENTITY_TYPES = /* @__PURE__ */ new Set(["Function", "Method", "Class", "Attribute", "GlobalVariable"]);
|
|
2129
|
+
var FINE_CLASS_SIBLING = { Function: "Method", Method: "Function" };
|
|
2057
2130
|
var KIND_UNIONS = { uses: ["imports", "calls", "callsSymbol"] };
|
|
2058
2131
|
var kindsFor = (kind) => KIND_UNIONS[kind] || [kind];
|
|
2059
2132
|
var OVERFLOW_CAP = 12;
|
|
@@ -2077,6 +2150,10 @@
|
|
|
2077
2150
|
function verbFor(kind) {
|
|
2078
2151
|
return REVERSE_MISS_VERB[kind] || kind;
|
|
2079
2152
|
}
|
|
2153
|
+
var LEADING_RELATION_VERB_RE = new RegExp(
|
|
2154
|
+
`^(?:${Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?:s|ing|ed)?\\s+`,
|
|
2155
|
+
"i"
|
|
2156
|
+
);
|
|
2080
2157
|
function defaultNlp() {
|
|
2081
2158
|
return typeof nlpAdapter === "function" ? nlpAdapter() : null;
|
|
2082
2159
|
}
|
|
@@ -2981,7 +3058,24 @@
|
|
|
2981
3058
|
const token = (i.attributes || []).find((a) => a.key === "token")?.value;
|
|
2982
3059
|
return token && String(token).toLowerCase() === termLc;
|
|
2983
3060
|
});
|
|
2984
|
-
if (!match)
|
|
3061
|
+
if (!match) {
|
|
3062
|
+
const classHits = (graph.individuals || []).filter((i) => i.class === "Class" && String(i.label).toLowerCase() === termLc);
|
|
3063
|
+
if (classHits.length === 1) {
|
|
3064
|
+
const hit2 = classHits[0];
|
|
3065
|
+
const mid = moduleIdOf2(graph, hit2);
|
|
3066
|
+
const modLabel = mid && graph.byId.get(mid)?.label || String((hit2.attributes || []).find((a) => a.key === "site")?.value || "").split(":")[0] || null;
|
|
3067
|
+
return {
|
|
3068
|
+
matches: [hit2],
|
|
3069
|
+
objMatch: hit2,
|
|
3070
|
+
candidates: [],
|
|
3071
|
+
ambiguous: false,
|
|
3072
|
+
metaCodeClass: true,
|
|
3073
|
+
metaModuleLabel: modLabel,
|
|
3074
|
+
traversal: `schema lookup for "${term}" (miss), then unique Class individual by label`
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
return { matches: [], objMatch: null, candidates: [], traversal: `schema lookup for "${term}"`, ambiguous: false };
|
|
3078
|
+
}
|
|
2985
3079
|
return {
|
|
2986
3080
|
matches: [match],
|
|
2987
3081
|
objMatch: match,
|
|
@@ -3090,9 +3184,13 @@
|
|
|
3090
3184
|
return commitTouches(graph, objMatch, entityType, { candidates, ambiguous, matchedVia });
|
|
3091
3185
|
}
|
|
3092
3186
|
if (shape === "forward") {
|
|
3093
|
-
const
|
|
3094
|
-
const
|
|
3095
|
-
|
|
3187
|
+
const fwdSibling = SYMBOL_GRAIN_SIBLING[kind];
|
|
3188
|
+
const subjIsFineSymbol = !!(fwdSibling && objMatch.class && FINE_ENTITY_TYPES.has(objMatch.class));
|
|
3189
|
+
const fwdKinds = subjIsFineSymbol ? [.../* @__PURE__ */ new Set([...kindsFor(kind), fwdSibling])] : kindsFor(kind);
|
|
3190
|
+
const edges2 = fwdKinds.flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.subject === objMatch.id);
|
|
3191
|
+
const targets = edges2.map((e) => graph.byId.get(e.object)).filter(Boolean);
|
|
3192
|
+
const matches2 = subjIsFineSymbol ? uniqueById(targets) : targets;
|
|
3193
|
+
return { matches: matches2, objMatch, candidates, traversal: `${fwdKinds.join("+")} edges where subject = ${objMatch.label}`, ambiguous, matchedVia };
|
|
3096
3194
|
}
|
|
3097
3195
|
if (parsed.modifier === "transitive") {
|
|
3098
3196
|
const levels = impactClosure(graph, objMatch, { maxDepth: TRANSITIVE_MAX_DEPTH });
|
|
@@ -3111,8 +3209,17 @@
|
|
|
3111
3209
|
if (symbolKind && (FINE_ENTITY_TYPES.has(entityType) || objIsFineSymbol)) {
|
|
3112
3210
|
const edges2 = edgesOfKind(graph, symbolKind).filter((e) => e.object === objMatch.id);
|
|
3113
3211
|
const subjects2 = uniqueById(edges2.map((e) => graph.byId.get(e.subject)).filter(Boolean));
|
|
3114
|
-
|
|
3115
|
-
|
|
3212
|
+
let matches2 = !entityType || entityType === "Change" ? subjects2 : subjects2.filter((i) => i.class === entityType);
|
|
3213
|
+
let widenNote = "";
|
|
3214
|
+
const siblingClass = FINE_CLASS_SIBLING[entityType];
|
|
3215
|
+
if (!matches2.length && siblingClass) {
|
|
3216
|
+
const widened = subjects2.filter((i) => i.class === siblingClass);
|
|
3217
|
+
if (widened.length) {
|
|
3218
|
+
matches2 = widened;
|
|
3219
|
+
widenNote = `, widened to ${siblingClass} subjects (no ${entityType} recorded)`;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
return { matches: matches2, objMatch, candidates, traversal: `${symbolKind} edges where object = ${objMatch.label}${widenNote}`, ambiguous, matchedVia };
|
|
3116
3223
|
}
|
|
3117
3224
|
let edges = kindsFor(kind).flatMap((k) => edgesOfKind(graph, k)).filter((e) => e.object === objMatch.id);
|
|
3118
3225
|
let extNote = "";
|
|
@@ -3214,6 +3321,16 @@
|
|
|
3214
3321
|
ambiguous: false
|
|
3215
3322
|
};
|
|
3216
3323
|
}
|
|
3324
|
+
if (result.metaCodeClass) {
|
|
3325
|
+
const label = result.objMatch.label;
|
|
3326
|
+
const definedIn = result.metaModuleLabel ? `, defined in ${result.metaModuleLabel}` : "";
|
|
3327
|
+
return {
|
|
3328
|
+
content: `${label} is a class in this codebase${definedIn} \u2014 try "describe ${label}" or "which classes inherit from ${label}".`,
|
|
3329
|
+
miss: false,
|
|
3330
|
+
ambiguous: false,
|
|
3331
|
+
matches: result.matches
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3217
3334
|
const doc = (result.objMatch.attributes || []).find((a) => a.key === "doc")?.value || "";
|
|
3218
3335
|
const kindWord = result.objMatch.class === "SchemaClass" ? "a class in the graph's schema" : "a predicate (relation) in the graph's schema";
|
|
3219
3336
|
return { content: `${result.objMatch.label} is ${kindWord}: ${doc}`, miss: false, ambiguous: false, matches: result.matches };
|
|
@@ -3221,7 +3338,7 @@
|
|
|
3221
3338
|
if (result.mentionsShape) {
|
|
3222
3339
|
if (!result.matches.length) {
|
|
3223
3340
|
return {
|
|
3224
|
-
content: `"${parsed.object}" is not mentioned in any indexed identifier or doc-comment prose
|
|
3341
|
+
content: `"${parsed.object}" is not mentioned in any indexed identifier or doc-comment prose.`,
|
|
3225
3342
|
miss: true,
|
|
3226
3343
|
ambiguous: false
|
|
3227
3344
|
};
|
|
@@ -3278,7 +3395,7 @@
|
|
|
3278
3395
|
if (result.whenShape) {
|
|
3279
3396
|
const subject = result.objMatch.label;
|
|
3280
3397
|
if (!result.matches.length) {
|
|
3281
|
-
return { content: `no recorded commit touches ${subject} in this index
|
|
3398
|
+
return { content: `no recorded commit touches ${subject} in this index.`, miss: true, ambiguous: false };
|
|
3282
3399
|
}
|
|
3283
3400
|
const newest = result.matches[0];
|
|
3284
3401
|
const date = (newest.attributes || []).find((a) => a.key === "date")?.value || "";
|
|
@@ -3306,7 +3423,7 @@
|
|
|
3306
3423
|
const cite = `commit ${result.objMatch.label}`;
|
|
3307
3424
|
if (!result.matches.length) {
|
|
3308
3425
|
return {
|
|
3309
|
-
content: `${cite} touched nothing recorded in the index
|
|
3426
|
+
content: `${cite} touched nothing recorded in the index.`,
|
|
3310
3427
|
miss: true,
|
|
3311
3428
|
ambiguous: false
|
|
3312
3429
|
};
|
|
@@ -3326,7 +3443,7 @@
|
|
|
3326
3443
|
return { content: `couldn't resolve one of the terms in this question.`, miss: true, ambiguous: false };
|
|
3327
3444
|
}
|
|
3328
3445
|
return {
|
|
3329
|
-
content: result.answer ? `Yes
|
|
3446
|
+
content: result.answer ? `Yes \u2014 ${result.traversal}.` : `No \u2014 no ${parsed.kind} edge found from ${result.subjMatch.label} to ${result.objMatch.label}.`,
|
|
3330
3447
|
miss: !result.answer,
|
|
3331
3448
|
ambiguous: false
|
|
3332
3449
|
};
|
|
@@ -3334,22 +3451,23 @@
|
|
|
3334
3451
|
if (!result.matches.length) {
|
|
3335
3452
|
if (parsed.shape === "forward") {
|
|
3336
3453
|
return {
|
|
3337
|
-
content: `${result.objMatch.label} has no ${parsed.kind} edges in the index
|
|
3454
|
+
content: `${result.objMatch.label} has no ${parsed.kind} edges in the index.`,
|
|
3338
3455
|
miss: true,
|
|
3339
3456
|
ambiguous: false
|
|
3340
3457
|
};
|
|
3341
3458
|
}
|
|
3342
3459
|
if (parsed.kind === "tests" && !parsed.entityType) {
|
|
3343
|
-
const
|
|
3460
|
+
const stripped = String(parsed.object || "").replace(LEADING_RELATION_VERB_RE, "").trim();
|
|
3461
|
+
const obj = stripped || String(parsed.object || "").trim();
|
|
3344
3462
|
return {
|
|
3345
|
-
content: `No tests cover ${obj}
|
|
3463
|
+
content: `No tests cover ${obj}.`,
|
|
3346
3464
|
miss: true,
|
|
3347
3465
|
ambiguous: false
|
|
3348
3466
|
};
|
|
3349
3467
|
}
|
|
3350
3468
|
const entityWord = nounFor(parsed.entityType || "Module", 2);
|
|
3351
3469
|
return {
|
|
3352
|
-
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}
|
|
3470
|
+
content: `No ${entityWord} found whose module directly ${verbFor(parsed.kind)} ${parsed.object}.`,
|
|
3353
3471
|
miss: true,
|
|
3354
3472
|
ambiguous: false
|
|
3355
3473
|
};
|
package/src/browser.mjs
CHANGED
|
@@ -46,6 +46,7 @@ const PROP_KIND = {
|
|
|
46
46
|
"mgx:touchessymbol": "touchesSymbol",
|
|
47
47
|
"mgx:callssymbol": "callsSymbol",
|
|
48
48
|
"mgx:serves": "serves", // un-typed interfaces (gated); inert on a default graph
|
|
49
|
+
"mgx:callshttp": "callsHttp", // un-typed interfaces (gated); client call-site → Route
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
const pathOfId = (id) => {
|
|
@@ -284,7 +285,10 @@ export function buildTemporalGraph(raw, orderShas, { scope = "product", parentsB
|
|
|
284
285
|
}
|
|
285
286
|
const directBorn = (id) => {
|
|
286
287
|
const s = touchesByNode.get(id);
|
|
287
|
-
|
|
288
|
+
if (!s || !s.size) return null;
|
|
289
|
+
let min = Infinity; // running min, not Math.min(...spread) — touch sets scale with history
|
|
290
|
+
for (const o of s) if (o < min) min = o;
|
|
291
|
+
return min;
|
|
288
292
|
};
|
|
289
293
|
const fallbackBorn = (id) => {
|
|
290
294
|
let cur = parentOf.get(id);
|
|
@@ -604,7 +608,8 @@ function render(){
|
|
|
604
608
|
const hs=(S.heat&&!cmp)?heatScale(S.tg,idx):null;
|
|
605
609
|
const grav=S.grav?gravityAt(S.tg,hi):null;
|
|
606
610
|
const gW=grav?new Map(grav.map(e=>[e.src+'|'+e.dst,e.w])):null;
|
|
607
|
-
|
|
611
|
+
let gMax=1; // running max, not Math.max(...spread) — gravity edge lists scale with the graph
|
|
612
|
+
if(grav&&grav.length){gMax=grav[0].w;for(const e of grav)if(e.w>gMax)gMax=e.w;}
|
|
608
613
|
cy.batch(()=>{
|
|
609
614
|
cy.nodes().forEach(cn=>{
|
|
610
615
|
const n=S.byId.get(cn.id());
|
|
@@ -667,7 +672,8 @@ function runGravityLayout(){
|
|
|
667
672
|
const hi=S.cursorB!=null?Math.max(S.cursor,S.cursorB):S.cursor;
|
|
668
673
|
const g=gravityAt(S.tg,hi);
|
|
669
674
|
const wByKey=new Map(g.map(e=>[e.src+'|'+e.dst,e.w]));
|
|
670
|
-
|
|
675
|
+
let maxW=1; // running max, not Math.max(...spread) — gravity edge lists scale with the graph
|
|
676
|
+
if(g.length){maxW=g[0].w;for(const e of g)if(e.w>maxW)maxW=e.w;}
|
|
671
677
|
const wOf=e=>{const a=e.data('source'),b=e.data('target');return wByKey.get(a+'|'+b)??wByKey.get(b+'|'+a)??0;};
|
|
672
678
|
const vis=S.cy.elements().filter(el=>el.style('display')==='element');
|
|
673
679
|
vis.layout({name:'cose',animate:false,nodeRepulsion:6000,
|
package/src/codegraph.mjs
CHANGED
|
@@ -89,9 +89,10 @@ const PROP_KIND = {
|
|
|
89
89
|
// closure (module-coarse) is unchanged.
|
|
90
90
|
"mgx:touchessymbol": "touchesSymbol",
|
|
91
91
|
"mgx:callssymbol": "callsSymbol",
|
|
92
|
-
// un-typed interfaces (PLAN_UNTYPED_INTERFACES.md) — route→handler; only present
|
|
93
|
-
// gated [interfaces] feature emitted
|
|
92
|
+
// un-typed interfaces (PLAN_UNTYPED_INTERFACES.md) — route→handler + client→route; only present
|
|
93
|
+
// when the gated [interfaces] feature emitted edges, so these keys are inert on a default graph.
|
|
94
94
|
"mgx:serves": "serves",
|
|
95
|
+
"mgx:callshttp": "callsHttp",
|
|
95
96
|
// legacy tokens (pre-realign graphs) — kept so a stale artifact still classifies
|
|
96
97
|
"seon:usescomplextype": "imports",
|
|
97
98
|
"seon:invokesmethod": "calls",
|
|
@@ -189,6 +190,10 @@ export function resolveSymbol(graph, symbol) {
|
|
|
189
190
|
return {
|
|
190
191
|
match: scored[0]?.ind || null,
|
|
191
192
|
candidates: scored.slice(1, 5).map((x) => x.ind),
|
|
193
|
+
// `exact` distinguishes a literal label/id hit from a path-normalised or substring
|
|
194
|
+
// (fuzzy) hit, so a caller can NAME a substitution instead of silently answering
|
|
195
|
+
// about a different symbol than the one asked for (wh dogfood T14).
|
|
196
|
+
exact: (scored[0]?.score ?? 0) === 100,
|
|
192
197
|
};
|
|
193
198
|
}
|
|
194
199
|
|
|
@@ -320,6 +325,36 @@ export function impactClosure(graph, ind, { maxDepth = 8 } = {}) {
|
|
|
320
325
|
const subjLabel = graph.byId.get(subjModId)?.label || subjModId;
|
|
321
326
|
addDependent(objModId, subjModId, subjLabel, g.predicate);
|
|
322
327
|
}
|
|
328
|
+
} else if (kind === "serves") {
|
|
329
|
+
// Un-typed interfaces (interfaces.mjs, gated — these kinds never occur in a default
|
|
330
|
+
// graph, so the default closure is untouched). Edge direction as W4 emits it:
|
|
331
|
+
// subject = Route individual, object = handler symbol (fn:<path>#name). The Route
|
|
332
|
+
// and its handler are one interface point seen from two sides, so impact crosses
|
|
333
|
+
// the edge BOTH ways:
|
|
334
|
+
// - reverse (like imports/calls): a handler change impacts its Route — keyed on
|
|
335
|
+
// the handler symbol AND its module (coarsened on read, the callsSymbol
|
|
336
|
+
// technique), so the closure crosses the edge whether it entered at symbol or
|
|
337
|
+
// module level;
|
|
338
|
+
// - forward: a Route change impacts its handler's module — so the closure from a
|
|
339
|
+
// Route continues into the handler's own dependents at the next depth.
|
|
340
|
+
for (const e of g.edges) {
|
|
341
|
+
const handlerModId = moduleIdOfId(graph, e.object);
|
|
342
|
+
addDependent(e.object, e.subject, e.subjectLabel || e.subject, g.predicate);
|
|
343
|
+
if (handlerModId) {
|
|
344
|
+
addDependent(handlerModId, e.subject, e.subjectLabel || e.subject, g.predicate);
|
|
345
|
+
addDependent(e.subject, handlerModId, graph.byId.get(handlerModId)?.label || handlerModId, g.predicate);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
} else if (kind === "callsHttp") {
|
|
349
|
+
// Gated, like serves. subject = client call-site symbol, object = Route individual.
|
|
350
|
+
// Reverse-only (a Route change impacts its HTTP clients; a client change never
|
|
351
|
+
// impacts the Route), caller coarsened to its module — completing the chain
|
|
352
|
+
// handler → Route (serves) → client modules (callsHttp).
|
|
353
|
+
for (const e of g.edges) {
|
|
354
|
+
const subjModId = moduleIdOfId(graph, e.subject);
|
|
355
|
+
if (!subjModId) continue;
|
|
356
|
+
addDependent(e.object, subjModId, graph.byId.get(subjModId)?.label || subjModId, g.predicate);
|
|
357
|
+
}
|
|
323
358
|
} else if (kind === "tests") {
|
|
324
359
|
for (const e of g.edges) {
|
|
325
360
|
if (!coveredBy.has(e.object)) coveredBy.set(e.object, []);
|
|
@@ -689,9 +724,9 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
689
724
|
}
|
|
690
725
|
const [survivors, kindOverflow] = pruneToBeam(candidates);
|
|
691
726
|
for (const [id, score] of survivors) merged.set(id, Math.max(merged.get(id) || 0, score));
|
|
692
|
-
plyOverflow.push(...kindOverflow);
|
|
727
|
+
plyOverflow.push(...kindOverflow); // spread OK: bounded by BEAM_OVERFLOW_CAP (≤4 args)
|
|
693
728
|
}
|
|
694
|
-
overflow.push(...plyOverflow);
|
|
729
|
+
overflow.push(...plyOverflow); // spread OK: ≤ cap × edge groups = 16 args, never repo-scaled
|
|
695
730
|
// Apply the bounded nudge once per module (first ply it's reached), same shape as the other
|
|
696
731
|
// proximity families — a nudge, never a replacement.
|
|
697
732
|
for (const [id, propagated] of merged) {
|
|
@@ -1292,21 +1327,20 @@ export function renderSignature(graph, ind) {
|
|
|
1292
1327
|
return lines.join("\n");
|
|
1293
1328
|
}
|
|
1294
1329
|
|
|
1295
|
-
/**
|
|
1296
|
-
|
|
1297
|
-
export function renderSubclasses(graph, ind) {
|
|
1298
|
-
const inherits = edgesOfKind(graph, "inherits");
|
|
1299
|
-
const bases = inherits.filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object);
|
|
1330
|
+
/** child map over the `inherits` edges: base id → [{id,label}] of direct subclasses. */
|
|
1331
|
+
function inheritsChildrenOf(inherits) {
|
|
1300
1332
|
const childrenOf = new Map();
|
|
1301
1333
|
for (const e of inherits) {
|
|
1302
1334
|
if (!childrenOf.has(e.object)) childrenOf.set(e.object, []);
|
|
1303
1335
|
childrenOf.get(e.object).push({ id: e.subject, label: e.subjectLabel || e.subject });
|
|
1304
1336
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1337
|
+
return childrenOf;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** BFS the reverse-inheritance closure below `frontier` (depth-capped at 8); returns
|
|
1341
|
+
* sorted label levels. `visited` guards cycles and is mutated. */
|
|
1342
|
+
function descendSubclassLevels(childrenOf, frontier, visited) {
|
|
1308
1343
|
const levels = [];
|
|
1309
|
-
let frontier = [ind.id];
|
|
1310
1344
|
for (let depth = 1; depth <= 8 && frontier.length; depth += 1) {
|
|
1311
1345
|
const next = [];
|
|
1312
1346
|
const level = [];
|
|
@@ -1324,6 +1358,18 @@ export function renderSubclasses(graph, ind) {
|
|
|
1324
1358
|
}
|
|
1325
1359
|
frontier = next;
|
|
1326
1360
|
}
|
|
1361
|
+
return levels;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/** Forward bases + the transitive reverse inheritance closure (who extends this) —
|
|
1365
|
+
* replaces grepping `class X(Base)` across the tree. Uses `inherits` (mgx:subclassOf). */
|
|
1366
|
+
export function renderSubclasses(graph, ind) {
|
|
1367
|
+
const inherits = edgesOfKind(graph, "inherits");
|
|
1368
|
+
const bases = inherits.filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object);
|
|
1369
|
+
const childrenOf = inheritsChildrenOf(inherits);
|
|
1370
|
+
const lines = [`${ind.label} — ${ind.class || "Entity"} (id: ${ind.id})`];
|
|
1371
|
+
lines.push(bases.length ? `extends: ${capJoin(bases, SUBCLASS_CAP)}` : "extends: (no internal/recorded base classes)");
|
|
1372
|
+
const levels = descendSubclassLevels(childrenOf, [ind.id], new Set([ind.id]));
|
|
1327
1373
|
const total = levels.reduce((n, l) => n + l.length, 0);
|
|
1328
1374
|
if (!total) {
|
|
1329
1375
|
lines.push("subclasses: none recorded — nothing extends it in the extracted graph.");
|
|
@@ -1334,6 +1380,44 @@ export function renderSubclasses(graph, ind) {
|
|
|
1334
1380
|
return lines.join("\n");
|
|
1335
1381
|
}
|
|
1336
1382
|
|
|
1383
|
+
/** Honest subclasses answer for a base-class NAME with no individual of its own — an
|
|
1384
|
+
* external/framework base (e.g. ASP.NET's ControllerBase) that appears only as a
|
|
1385
|
+
* supertype label on `inherits` edges. Lists the classes whose recorded supertype
|
|
1386
|
+
* label matches the name, plus their transitive subclasses. Returns null when no
|
|
1387
|
+
* inherits edge carries the label, so the caller can fall through. (wh dogfood T14:
|
|
1388
|
+
* the silent fuzzy substitution answered "none recorded" while 176 classes inherited
|
|
1389
|
+
* from the asked-for external base.) */
|
|
1390
|
+
export function renderExternalSubclasses(graph, name) {
|
|
1391
|
+
const want = String(name || "").trim().toLowerCase();
|
|
1392
|
+
if (!want) return null;
|
|
1393
|
+
const inherits = edgesOfKind(graph, "inherits");
|
|
1394
|
+
const matched = inherits.filter((e) => {
|
|
1395
|
+
const label = String(e.objectLabel || "").trim().toLowerCase();
|
|
1396
|
+
if (label) return label === want;
|
|
1397
|
+
const id = String(e.object || "").toLowerCase();
|
|
1398
|
+
return id === want || id.endsWith(`#${want}`); // label-less edge: match the id tail
|
|
1399
|
+
});
|
|
1400
|
+
if (!matched.length) return null;
|
|
1401
|
+
const visited = new Set();
|
|
1402
|
+
const direct = [];
|
|
1403
|
+
for (const e of matched) {
|
|
1404
|
+
if (visited.has(e.subject)) continue;
|
|
1405
|
+
visited.add(e.subject);
|
|
1406
|
+
direct.push({ id: e.subject, label: e.subjectLabel || e.subject });
|
|
1407
|
+
}
|
|
1408
|
+
const directLabels = direct.map((d) => d.label).sort((a, b) => String(a).localeCompare(String(b)));
|
|
1409
|
+
const deeper = descendSubclassLevels(inheritsChildrenOf(inherits), direct.map((d) => d.id), visited);
|
|
1410
|
+
const levels = [directLabels, ...deeper];
|
|
1411
|
+
const total = levels.reduce((n, l) => n + l.length, 0);
|
|
1412
|
+
const lines = [
|
|
1413
|
+
`${name} — external/framework base class: not an individual in the graph, but ` +
|
|
1414
|
+
`${matched.length} inherits edge(s) name it as a supertype.`,
|
|
1415
|
+
`subclasses: ${total} total across ${levels.length} level(s).`,
|
|
1416
|
+
];
|
|
1417
|
+
levels.forEach((l, i) => lines.push(` depth ${i + 1} (${l.length}): ${capJoin(l, SUBCLASS_CAP)}`));
|
|
1418
|
+
return lines.join("\n");
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1337
1421
|
const ARCH_PKG_CAP = 25;
|
|
1338
1422
|
const ARCH_HUB_CAP = 15;
|
|
1339
1423
|
|
|
@@ -1520,13 +1604,24 @@ export function renderClassHistory(graph, ind) {
|
|
|
1520
1604
|
return renderSymbolHistory(graph, ind);
|
|
1521
1605
|
}
|
|
1522
1606
|
|
|
1523
|
-
// ---- symbol search (kind=function/class/method/attribute, with name/decorator filters)
|
|
1607
|
+
// ---- symbol search (kind=function/class/method/attribute/route, with name/decorator filters)
|
|
1524
1608
|
|
|
1525
|
-
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute" };
|
|
1609
|
+
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute", route: "Route" };
|
|
1610
|
+
|
|
1611
|
+
/** handler labels (with file:line where sited) a Route individual's `serves` edges point at —
|
|
1612
|
+
* a Route has no source span of its own, so the handler is what makes the line useful. */
|
|
1613
|
+
function routeHandlers(graph, servesEdges, routeId) {
|
|
1614
|
+
return servesEdges
|
|
1615
|
+
.filter((e) => e.subject === routeId)
|
|
1616
|
+
.map((e) => {
|
|
1617
|
+
const h = graph.byId.get(e.object);
|
|
1618
|
+
return `${e.objectLabel || h?.label || e.object}${spanTag(h ? siteOf(h) : null)}`;
|
|
1619
|
+
});
|
|
1620
|
+
}
|
|
1526
1621
|
|
|
1527
1622
|
function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, nameRe }) {
|
|
1528
1623
|
const targetClass = SYMBOL_CLASSES[kind];
|
|
1529
|
-
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, or module).`;
|
|
1624
|
+
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, route, or module).`;
|
|
1530
1625
|
const hits = [];
|
|
1531
1626
|
for (const ind of graph.individuals) {
|
|
1532
1627
|
if ((ind.class || "") !== targetClass) continue;
|
|
@@ -1542,8 +1637,21 @@ function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, n
|
|
|
1542
1637
|
hits.sort((a, b) => b.score - a.score || String(a.ind.label).length - String(b.ind.label).length);
|
|
1543
1638
|
const top = hits.slice(0, limit);
|
|
1544
1639
|
const lines = [`${hits.length} ${kind}(s) match (top ${top.length}):`];
|
|
1545
|
-
|
|
1546
|
-
|
|
1640
|
+
// Routes were dead data (wh dogfood C9/C18/T3/T11): 22 Route individuals + serves edges in the
|
|
1641
|
+
// graph, reachable from no surface. A Route line shows verb + path template (its label) → the
|
|
1642
|
+
// handler symbol(s) its serves edges point at, so the hit is actionable without another call.
|
|
1643
|
+
const servesEdges = targetClass === "Route" ? edgesOfKind(graph, "serves") : null;
|
|
1644
|
+
for (const { ind } of top) {
|
|
1645
|
+
if (servesEdges) {
|
|
1646
|
+
const handlers = routeHandlers(graph, servesEdges, ind.id);
|
|
1647
|
+
lines.push(`- ${ind.label}${handlers.length ? ` → ${capJoin(handlers, 4)}` : " (no handler edge recorded)"}`);
|
|
1648
|
+
} else {
|
|
1649
|
+
lines.push(`- ${ind.label}${spanTag(siteOf(ind))}`);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
lines.push(servesEdges
|
|
1653
|
+
? "Then seonix_snippet <Class.method> for a handler body, or seonix_describe for a route's edges."
|
|
1654
|
+
: "Then seonix_snippet <name> for the exact body, or seonix_describe for its edges.");
|
|
1547
1655
|
return lines.join("\n");
|
|
1548
1656
|
}
|
|
1549
1657
|
|
package/src/cs_roslyn.mjs
CHANGED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
// prints the {modules:[…]} contract; this wrapper just shells out and parses it.
|
|
4
4
|
// Falls back is the caller's job (cs_treesitter) when dotnet/the tool is absent.
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
|
-
import {
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { access } from "node:fs/promises";
|
|
6
|
+
import { DLL as TOOL_DLL, ensureRoslynBuilt } from "./roslyn_build.mjs";
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
// Cached across calls within a process: chooseCs() in extract_lang.mjs calls
|
|
9
|
+
// toolAvailable() once per ingestRepo() run, but tests and multi-repo indexing
|
|
10
|
+
// can call it repeatedly — avoid re-spawning `dotnet --version` every time.
|
|
11
|
+
// A fresh process (the normal case: one index run = one process) always
|
|
12
|
+
// re-checks, so a dotnet toolchain installed after a prior failed attempt is
|
|
13
|
+
// picked up automatically on the next run, no reinstall needed.
|
|
14
|
+
let cachedAvailable;
|
|
12
15
|
|
|
16
|
+
/** True if the Roslyn dll is present, lazily building it first (once per
|
|
17
|
+
* process) when it's missing and `dotnet` is on PATH. */
|
|
13
18
|
export async function toolAvailable() {
|
|
14
|
-
|
|
19
|
+
if (cachedAvailable === undefined) cachedAvailable = await ensureRoslynBuilt();
|
|
20
|
+
return cachedAvailable;
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
function run(cmd, args) {
|
|
@@ -26,7 +32,7 @@ function run(cmd, args) {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export async function ingest(root) {
|
|
29
|
-
if (!(await toolAvailable())) throw new Error("roslyn-extract.dll not
|
|
35
|
+
if (!(await toolAvailable())) throw new Error("roslyn-extract.dll not available (no dotnet on PATH, or the lazy build failed — run `npm run build:roslyn` to retry with warnings)");
|
|
30
36
|
const { code, out, err } = await run("dotnet", [TOOL_DLL, root]);
|
|
31
37
|
if (code !== 0) throw new Error(`roslyn-extract failed (exit ${code}): ${err.slice(-300)}`);
|
|
32
38
|
let parsed;
|