Skip to content

Commit c93c0ae

Browse files
cdeustclaude
andcommitted
fix(ui+ast): board/knowledge hang + multi-language symbol coverage
Board + Knowledge views were rendering blank on large memory sets — my resolveMemorySymbols() was compiling ~4000 RegExp instances per memory card, which blocked the main thread for seconds on 1600-memory / 10000-symbol graphs. Rewrote with: - _hasWordMatch(hay, needle): indexOf + manual word-boundary char check, zero regex allocations. - Label cap lowered 4000 → 1500. - Case-sensitive label index — function names in memories are usually written with their original casing and case-sensitive matching avoids "do" matching every "Do" verb. - Minimum label length raised 3 → 4 to cut noise from common short words. AST multi-language coverage (workflow_graph_source_ast.py) — AP supports 27 languages but the extractor only queried 7 label kinds. Expanded _SYMBOL_LABELS to cover every label the Java / Kotlin / Swift / ObjC / C / C++ / Go parsers emit: Class, Interface, Field, Property (JVM) Protocol, Extension (Swift / ObjC) Union, Typedef, Macro (C / C++) Module, Package, Namespace, Variable (Go / general) _symbol_type_from_label maps them all onto the five palette slots: function · method · class · module · constant. Module-ish containers (module, package, namespace) now render with amber #FBBF24. AST cache cleared — next build re-queries AP and picks up the new symbol kinds automatically. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 111dd46 commit c93c0ae

2 files changed

Lines changed: 86 additions & 34 deletions

File tree

mcp_server/infrastructure/workflow_graph_source_ast.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -163,32 +163,58 @@ def _as_list(payload: Any) -> list[dict]:
163163
# stage-3 tree-sitter extractors; see
164164
# ``automatised-pipeline/src/clustering.rs`` for the canonical list.
165165
_SYMBOL_LABELS = (
166+
# Core — Rust + Python (original set)
166167
"Function",
167168
"Method",
168169
"Struct",
169170
"Enum",
170171
"Trait",
171172
"Constant",
172173
"TypeAlias",
174+
# JVM family — Java, Kotlin
175+
"Class",
176+
"Interface",
177+
"Field",
178+
"Property",
179+
# Swift / ObjC family
180+
"Protocol",
181+
"Extension",
182+
# C / C++
183+
"Union",
184+
"Typedef",
185+
"Macro",
186+
# Go / general
187+
"Module",
188+
"Package",
189+
"Namespace",
190+
"Variable",
173191
)
174192

175193

176194
def _symbol_type_from_label(label: str) -> str:
177195
"""Map AP's label → workflow-graph symbol_type.
178196
179-
Keeps the value set small (function/method/class/constant) so the
180-
palette (``SYMBOL_COLORS``) remains stable. Rust-specific labels
181-
(Struct/Enum/Trait) collapse to ``class``; ``TypeAlias`` becomes
182-
``constant``.
197+
Keeps the value set small so the palette (``SYMBOL_COLORS``) stays
198+
compact. Every AP label from every supported language collapses
199+
into one of: function · method · class · module · constant.
183200
"""
184201
low = label.lower()
185-
if low in ("function",):
202+
if low == "function":
186203
return "function"
187-
if low in ("method",):
204+
if low == "method":
188205
return "method"
189-
if low in ("struct", "enum", "trait"):
206+
# All type-like constructs → class. Covers Rust (struct/enum/trait),
207+
# Java/Kotlin (class/interface), Swift/ObjC (protocol/extension),
208+
# C/C++ (union).
209+
if low in ("struct", "enum", "trait", "class", "interface",
210+
"protocol", "extension", "union"):
190211
return "class"
191-
if low in ("constant", "typealias"):
212+
# Module-ish containers → module (amber).
213+
if low in ("module", "package", "namespace"):
214+
return "module"
215+
# Value-ish / alias-ish → constant (slate).
216+
if low in ("constant", "typealias", "typedef", "macro",
217+
"field", "property", "variable"):
192218
return "constant"
193219
return low
194220

ui/unified/js/knowledge.js

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -312,55 +312,81 @@
312312
var base = p.split('/').pop();
313313
if (base && base !== p) (byPath[base] = byPath[base] || []).push(n);
314314
}
315+
// Case-sensitive key — function names in memories are usually
316+
// written with their original casing (`appendGraphDelta`), and
317+
// case-sensitive matching avoids "do" matching every "Do" verb.
315318
var lbl = (n.label || '').trim();
316-
if (lbl && lbl.length >= 3) {
317-
var k = lbl.toLowerCase();
318-
(byLabel[k] = byLabel[k] || []).push(n);
319+
if (lbl && lbl.length >= 4) {
320+
(byLabel[lbl] = byLabel[lbl] || []).push(n);
319321
}
320322
});
321323
_symIndexCache = { byPath: byPath, byLabel: byLabel };
322324
_symIndexKey = key;
323325
return _symIndexCache;
324326
}
327+
function _isWordChar(ch) {
328+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
329+
(ch >= '0' && ch <= '9') || ch === '_';
330+
}
331+
function _hasWordMatch(hay, needle) {
332+
// Case-sensitive indexOf + manual word-boundary check — avoids
333+
// the 4000-per-card RegExp churn that was freezing the tab.
334+
if (!needle) return false;
335+
var idx = 0;
336+
while (true) {
337+
var pos = hay.indexOf(needle, idx);
338+
if (pos === -1) return false;
339+
var before = pos === 0 ? '' : hay.charAt(pos - 1);
340+
var after = pos + needle.length >= hay.length ? '' : hay.charAt(pos + needle.length);
341+
if (!_isWordChar(before) && !_isWordChar(after)) return true;
342+
idx = pos + 1;
343+
}
344+
}
345+
325346
function resolveMemorySymbols(mem, maxN) {
326347
var idx = _buildSymbolIndex();
327348
if (!idx) return [];
328349
var refs = [];
329-
// File-based matches.
350+
var seen = {};
351+
// File-based matches (cheap, exact).
330352
var fileRefs = [];
331353
if (mem.path) fileRefs.push(mem.path);
332354
if (Array.isArray(mem.file_refs)) fileRefs = fileRefs.concat(mem.file_refs);
333355
if (Array.isArray(mem.fileRefs)) fileRefs = fileRefs.concat(mem.fileRefs);
334-
var seen = {};
335-
fileRefs.forEach(function (fp) {
336-
if (!fp) return;
356+
for (var f = 0; f < fileRefs.length && refs.length < (maxN || 12); f++) {
357+
var fp = fileRefs[f];
358+
if (!fp) continue;
337359
var hits = idx.byPath[fp] || [];
338360
var base = fp.split('/').pop();
339-
if (base && base !== fp) hits = hits.concat(idx.byPath[base] || []);
340-
hits.forEach(function (s) {
341-
if (seen[s.id]) return;
361+
if (base && base !== fp && idx.byPath[base]) hits = hits.concat(idx.byPath[base]);
362+
for (var h = 0; h < hits.length && refs.length < (maxN || 12); h++) {
363+
var s = hits[h];
364+
if (seen[s.id]) continue;
342365
seen[s.id] = 1;
343366
refs.push({ node: s, via: 'file' });
344-
});
345-
});
346-
// Label-based matches in body / content / tags.
347-
var hay = ((mem.content || mem.body || '') + ' ' +
348-
((mem.tags || []).join(' '))).toLowerCase();
367+
}
368+
}
369+
if (refs.length >= (maxN || 12)) return refs.slice(0, maxN || 12);
370+
371+
// Label-based matches — iterate labels, not characters. Cap at
372+
// 1500 labels and stop as soon as we've filled maxN to keep the
373+
// per-card cost bounded on 10k-symbol graphs.
374+
var hay = (mem.content || mem.body || '') + ' ' +
375+
((mem.tags || []).join(' '));
349376
if (hay.length > 4) {
350377
var labelKeys = Object.keys(idx.byLabel);
351-
// Guard — avoid O(N·M) on very large inventories; cap the scan.
352-
var cap = Math.min(labelKeys.length, 4000);
378+
var cap = Math.min(labelKeys.length, 1500);
353379
for (var i = 0; i < cap && refs.length < (maxN || 12); i++) {
354380
var k = labelKeys[i];
355-
if (hay.indexOf(k) === -1) continue;
356-
// Word-boundary sanity — avoid matching "set" inside "setup".
357-
var re = new RegExp('(^|[^A-Za-z0-9_])' + k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '([^A-Za-z0-9_]|$)');
358-
if (!re.test(hay)) continue;
359-
idx.byLabel[k].forEach(function (s) {
360-
if (seen[s.id]) return;
361-
seen[s.id] = 1;
362-
refs.push({ node: s, via: 'label' });
363-
});
381+
if (hay.indexOf(k) === -1) continue; // cheap pre-filter
382+
if (!_hasWordMatch(hay, k)) continue; // word-boundary check
383+
var syms = idx.byLabel[k];
384+
for (var j = 0; j < syms.length && refs.length < (maxN || 12); j++) {
385+
var sym = syms[j];
386+
if (seen[sym.id]) continue;
387+
seen[sym.id] = 1;
388+
refs.push({ node: sym, via: 'label' });
389+
}
364390
}
365391
}
366392
return refs.slice(0, maxN || 12);

0 commit comments

Comments
 (0)