@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.0
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 +441 -202
- package/bin/tmct.mjs +126 -1
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2872
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
package/src/codegraph.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { lookupByProseTokens, proseLayerHits } from "./prose.mjs";
|
|
2
2
|
import { cosine } from "./embed.mjs";
|
|
3
|
-
// Single-sourced predicate strings (memory/core.mjs owns these constants) — no
|
|
4
|
-
// circular-import risk: core.mjs imports trust.mjs/shacl.mjs/planning.mjs, never
|
|
5
|
-
// codegraph.mjs, in either direction.
|
|
6
3
|
import { CREATED_AT_PROP, UPDATED_AT_PROP, provenanceTagToSource } from "./memory/core.mjs";
|
|
7
4
|
|
|
8
5
|
// Pure (no-network, no-fs) query logic over the typed `entities` payload that the
|
|
@@ -16,15 +13,6 @@ import { CREATED_AT_PROP, UPDATED_AT_PROP, provenanceTagToSource } from "./memor
|
|
|
16
13
|
// individuals: [{id, label, class, derived_from: [ref], mentions: [{id, count}],
|
|
17
14
|
// attributes?: [{prop, key, value}]}],
|
|
18
15
|
// }
|
|
19
|
-
//
|
|
20
|
-
// Ported ≈verbatim from marginalia seon-mcp/src/codegraph.mjs (the shipped,
|
|
21
|
-
// tested typed-edge query layer). The only edits: provenance/attestation wording
|
|
22
|
-
// is code-graph-generic (git:<sha> / file:line refs, not memory-node prose), and
|
|
23
|
-
// a renderSearch() is added for the local, deterministic tmct_search.
|
|
24
|
-
//
|
|
25
|
-
// Edge inventory is read DYNAMICALLY from the payload (predicate verb + the closed
|
|
26
|
-
// `prop` token like "mg:imports"); only the kind-classifier for the impact closure
|
|
27
|
-
// hardcodes the relation set.
|
|
28
16
|
|
|
29
17
|
// ---- payload parsing ---------------------------------------------------------
|
|
30
18
|
|
|
@@ -54,10 +42,7 @@ export function parseEntities(payload) {
|
|
|
54
42
|
relations,
|
|
55
43
|
truncated,
|
|
56
44
|
generatedAt: payload?.generated_at || null,
|
|
57
|
-
//
|
|
58
|
-
// byte-identical from the payload so ask.mjs's resolveObject can consult it as a
|
|
59
|
-
// fallback tier without reaching back into the raw payload itself. {} when the
|
|
60
|
-
// build had prose disabled or the payload predates this field.
|
|
45
|
+
// word -> [individual ids]; {} when prose was disabled at build time
|
|
61
46
|
proseIndex: payload?.proseIndex || {},
|
|
62
47
|
};
|
|
63
48
|
}
|
|
@@ -80,9 +65,7 @@ const PROP_KIND = {
|
|
|
80
65
|
"seon:hassupertype": "inherits",
|
|
81
66
|
"mgx:changecoupledwith": "cochange",
|
|
82
67
|
"mgx:reexports": "reexports",
|
|
83
|
-
//
|
|
84
|
-
// These stay SEPARATE kinds from the module-coarse "touches"/"calls" so the impact
|
|
85
|
-
// closure (module-coarse) is unchanged.
|
|
68
|
+
// symbol-level edges stay separate kinds so the module-coarse impact closure is unchanged
|
|
86
69
|
"mgx:touchessymbol": "touchesSymbol",
|
|
87
70
|
"mgx:callssymbol": "callsSymbol",
|
|
88
71
|
// legacy tokens (pre-realign graphs) — kept so a stale artifact still classifies
|
|
@@ -95,20 +78,12 @@ const PROP_KIND = {
|
|
|
95
78
|
"mg:defines": "defines",
|
|
96
79
|
"mg:tests": "tests",
|
|
97
80
|
"mg:touches": "touches",
|
|
98
|
-
// memory-graph predicates
|
|
99
|
-
// itself as its own kind name (no module-rollup abbreviation needed, unlike
|
|
100
|
-
// imports/calls) so adjacencyForKinds/edgesOfKind can walk the memory graph too.
|
|
81
|
+
// memory-graph predicates map to themselves so adjacencyForKinds/edgesOfKind can walk them too
|
|
101
82
|
"mgx:saidinsession": "saidInSession",
|
|
102
83
|
"mgx:inreplyto": "inReplyTo",
|
|
103
84
|
"mgx:statedby": "statedBy",
|
|
104
85
|
"mgx:canonicalisedfrom": "canonicalisedFrom",
|
|
105
|
-
//
|
|
106
|
-
// deriveFactTermGraph (below) synthesizes on every Fact — Fact -> its own
|
|
107
|
-
// subject/object Term individual. Without these a walk seeded on a Fact (the
|
|
108
|
-
// default mostRecentIndividual seed right after a teach turn) could never
|
|
109
|
-
// reach the term graph at all. Distinct from the per-predicate kinds below
|
|
110
|
-
// (an open-ended, DYNAMIC set — see relationKind's "factrel:" branch), these
|
|
111
|
-
// two are fixed and few, so a plain PROP_KIND row is the simplest fit.
|
|
86
|
+
// structural links deriveFactTermGraph synthesizes on every Fact (Fact -> its own subject/object Term)
|
|
112
87
|
"mgx:factsubjectterm": "factSubjectTerm",
|
|
113
88
|
"mgx:factobjectterm": "factObjectTerm",
|
|
114
89
|
};
|
|
@@ -116,18 +91,10 @@ const PROP_KIND = {
|
|
|
116
91
|
export function relationKind(group) {
|
|
117
92
|
const prop = String(group?.prop || "").toLowerCase();
|
|
118
93
|
if (PROP_KIND[prop]) return PROP_KIND[prop];
|
|
119
|
-
//
|
|
120
|
-
// groups (Term -> Term, one group per DISTINCT fact predicate actually
|
|
121
|
-
// present in the data — there is no fixed vocabulary to enumerate here: a
|
|
122
|
-
// freshly taught "mgx:<verb>" predicate (generalVerbTeach) must classify
|
|
123
|
-
// automatically, never requiring a PROP_KIND edit per predicate). Those
|
|
124
|
-
// groups self-namespace their `prop` as `factrel:<predicate>` specifically
|
|
125
|
-
// so they can self-classify here, verbatim, with zero collision risk against
|
|
126
|
-
// any real code-graph or memory-graph prop token (none use this prefix).
|
|
94
|
+
// deriveFactTermGraph namespaces taught predicates as "factrel:<predicate>"
|
|
127
95
|
if (prop.startsWith("factrel:")) return group.predicate || null;
|
|
128
96
|
const pred = String(group?.predicate || "").toLowerCase();
|
|
129
|
-
// symbol-granular fallbacks first, so a near-miss
|
|
130
|
-
// fine-grained kind rather than collapsing to module-coarse calls/touches.
|
|
97
|
+
// symbol-granular fallbacks first, so a near-miss still classifies fine-grained
|
|
131
98
|
if (/symbol/.test(pred)) {
|
|
132
99
|
if (/\b(call|invoke)/.test(pred)) return "callsSymbol";
|
|
133
100
|
if (/(touch|chang|modif)/.test(pred)) return "touchesSymbol";
|
|
@@ -157,9 +124,6 @@ function basename(p) {
|
|
|
157
124
|
return parts[parts.length - 1];
|
|
158
125
|
}
|
|
159
126
|
|
|
160
|
-
// Attestation: a ref prefixed `git:` (a commit that touched the entity) counts as
|
|
161
|
-
// one mention, so better-attested (more-churned) entities rank/render ahead of
|
|
162
|
-
// untouched ones even before per-node mention counts exist.
|
|
163
127
|
const isProvRef = (r) => /^(git|turn):/.test(String(r || ""));
|
|
164
128
|
|
|
165
129
|
export function turnRefCount(ind) {
|
|
@@ -240,9 +204,7 @@ function relLabel(g) {
|
|
|
240
204
|
return g.prop ? `${g.predicate} [${g.prop}]` : g.predicate;
|
|
241
205
|
}
|
|
242
206
|
|
|
243
|
-
//
|
|
244
|
-
// hub entities must never dump hundreds of edges. Show the first `n`, then a
|
|
245
|
-
// "+K more" tail with the true count.
|
|
207
|
+
// Show the first `n` items, then a "+K more" tail with the true count.
|
|
246
208
|
function capJoin(items, n, sep = ", ") {
|
|
247
209
|
if (items.length <= n) return items.join(sep);
|
|
248
210
|
return items.slice(0, n).join(sep) + `, +${items.length - n} more`;
|
|
@@ -295,23 +257,17 @@ function truncationNote(graph) {
|
|
|
295
257
|
return `note: partial edge lists for: ${list}. Counts are complete; the lists are not.`;
|
|
296
258
|
}
|
|
297
259
|
|
|
298
|
-
// ---- compare
|
|
260
|
+
// ---- compare ----
|
|
299
261
|
|
|
300
|
-
/** One side-by-side row for a
|
|
301
|
-
*
|
|
302
|
-
* classified relation groups and edge-cap discipline renderDescribe reads), just
|
|
303
|
-
* paired up instead of listed independently per entity. `field` picks the
|
|
304
|
-
* correct edge endpoint for the direction (`out` reads the OBJECT end,
|
|
305
|
-
* `incoming` reads the SUBJECT end) — edgesFor's own out/incoming split. */
|
|
262
|
+
/** One side-by-side row for a classified relation. `field` picks the edge
|
|
263
|
+
* endpoint for the direction ("out" reads object, "incoming" reads subject). */
|
|
306
264
|
function compareRow(prefix, key, aEdges, bEdges, labelA, labelB, field) {
|
|
307
265
|
const fmt = (edges) => (edges.length ? capJoin(edges.map((e) => e[`${field}Label`] || e[field]), DESCRIBE_EDGE_CAP) : "none");
|
|
308
266
|
return ` ${prefix}${key}: ${labelA} (${aEdges.length}) -> ${fmt(aEdges)}; ${labelB} (${bEdges.length}) -> ${fmt(bEdges)}`;
|
|
309
267
|
}
|
|
310
268
|
|
|
311
|
-
/** predicate-label -> {group, aEdges, bEdges},
|
|
312
|
-
* groups for one direction
|
|
313
|
-
* new graph query: every group/edges pair here is exactly what edgesFor already
|
|
314
|
-
* returned for each individual separately. */
|
|
269
|
+
/** predicate-label -> {group, aEdges, bEdges}, a union-by-key merge of both
|
|
270
|
+
* sides' edge groups for one direction. */
|
|
315
271
|
function pairByPredicate(aGroups, bGroups) {
|
|
316
272
|
const byPred = new Map();
|
|
317
273
|
for (const { group, edges } of aGroups) byPred.set(relLabel(group), { group, aEdges: edges, bEdges: [] });
|
|
@@ -323,16 +279,8 @@ function pairByPredicate(aGroups, bGroups) {
|
|
|
323
279
|
return byPred;
|
|
324
280
|
}
|
|
325
281
|
|
|
326
|
-
/**
|
|
327
|
-
*
|
|
328
|
-
* the exact edgesFor/relLabel/capJoin machinery renderDescribe already reads
|
|
329
|
-
* (same classified relation groups, same DESCRIBE_EDGE_CAP discipline), just
|
|
330
|
-
* rendered as a paired diff instead of two independent one-entity reports —
|
|
331
|
-
* no new graph traversal, only a new presentation over data describe already
|
|
332
|
-
* surfaces. Deliberately refuses (returns null) rather than forcing a
|
|
333
|
-
* comparison across mismatched kinds or the same individual twice — the
|
|
334
|
-
* caller (chat.mjs's compare lane) renders its own honest message for those
|
|
335
|
-
* cases instead of an empty/degenerate report. */
|
|
282
|
+
/** Side-by-side comparison of two same-kind individuals. Returns null for
|
|
283
|
+
* mismatched kinds or comparing an individual to itself. */
|
|
336
284
|
export function renderCompare(graph, indA, indB) {
|
|
337
285
|
if (!indA || !indB || indA.id === indB.id) return null;
|
|
338
286
|
const klass = indA.class || "Entity";
|
|
@@ -355,8 +303,7 @@ export function renderCompare(graph, indA, indB) {
|
|
|
355
303
|
}
|
|
356
304
|
}
|
|
357
305
|
|
|
358
|
-
//
|
|
359
|
-
// surfacing (a shared attribute value isn't a "difference").
|
|
306
|
+
// only mismatches are worth surfacing as a "difference"
|
|
360
307
|
const attrsA = new Map((indA.attributes || []).map((x) => [x.key, x.value]));
|
|
361
308
|
const attrsB = new Map((indB.attributes || []).map((x) => [x.key, x.value]));
|
|
362
309
|
const attrKeys = new Set([...attrsA.keys(), ...attrsB.keys()]);
|
|
@@ -372,31 +319,16 @@ export function renderCompare(graph, indA, indB) {
|
|
|
372
319
|
|
|
373
320
|
// ---- impact (transitive reverse closure over imports/calls) ---------------------
|
|
374
321
|
|
|
375
|
-
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* the test modules covering it (subjects of tests-kind edges pointing at it).
|
|
380
|
-
*
|
|
381
|
-
* Module-coarse "calls" (`mgx:callsCoarse`, graph-build.mjs) is deliberately
|
|
382
|
-
* conservative — it only fires when the callee's module is ALREADY in the
|
|
383
|
-
* caller's import list ("coarse, import-backed calls", graph-build.mjs's own
|
|
384
|
-
* comment), so by construction every "calls" edge is a strict subset of an
|
|
385
|
-
* "imports" edge between the same pair — it never independently extends this
|
|
386
|
-
* closure's reach beyond what "imports" alone already gives it. `callsSymbol`
|
|
387
|
-
* (fn/method-granular, no import-backing requirement — same-module calls,
|
|
388
|
-
* ambiguous-name calls the coarse pass drops) is the richer signal; this
|
|
389
|
-
* closure also folds it in, coarsened to module level on read (never stored),
|
|
390
|
-
* mirroring the technique `adjacencyForKinds`/`BEAM_EDGE_GROUPS` already use
|
|
391
|
-
* for the same reason.
|
|
392
|
-
*/
|
|
322
|
+
/** BFS the reverse of imports/calls edges from `ind` — "what would break".
|
|
323
|
+
* Diamonds collapse to shortest depth; cycles terminate via the visited set.
|
|
324
|
+
* `callsSymbol` is folded in too, coarsened to module level on read: it has
|
|
325
|
+
* no import-backing requirement, so it can reach dependents "calls" alone would miss. */
|
|
393
326
|
export function impactClosure(graph, ind, { maxDepth = 8 } = {}) {
|
|
394
327
|
const dependents = new Map();
|
|
395
328
|
const coveredBy = new Map(); // moduleId → [test labels]
|
|
396
329
|
const addDependent = (objectId, subjectId, subjectLabel, via) => {
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// itself (imports/calls edges are already module-to-module and can't self-loop).
|
|
330
|
+
// callsSymbol coarsens to module level, so two symbols in the same module
|
|
331
|
+
// calling each other must not produce a module pointing at itself
|
|
400
332
|
if (!objectId || !subjectId || objectId === subjectId) return;
|
|
401
333
|
if (!dependents.has(objectId)) dependents.set(objectId, []);
|
|
402
334
|
dependents.get(objectId).push({ id: subjectId, label: subjectLabel, via });
|
|
@@ -513,190 +445,94 @@ function definesIndex(graph) {
|
|
|
513
445
|
*/
|
|
514
446
|
export const SEARCH_LIMIT = 10;
|
|
515
447
|
const SEARCH_SYMBOLS_SHOWN = 8;
|
|
516
|
-
// Locate scoring
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
const PATH_W = 3; // token == a path component (django/utils/<text>.py)
|
|
448
|
+
// Locate scoring is IDF-weighted and component-aware: weight each token by
|
|
449
|
+
// rarity across modules so a distinctive term decides over ubiquitous ones,
|
|
450
|
+
// and match identifier components (boundary-aware) so "text" hits
|
|
451
|
+
// utils/text.py but not "ci<text>". Deterministic; no models.
|
|
452
|
+
const PATH_W = 3; // token == a path component
|
|
522
453
|
const SYM_W = 2; // token == a component of a defined symbol name
|
|
523
|
-
const EXACT_W = 5; // token == a whole defined symbol name
|
|
524
|
-
const SYM_MATCH_CAP = 4; // only the top-K highest-IDF symbol-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
const PROX_CAP_FRAC = 0.35; // … capped at this × the module's own score (a nudge — hubs can't run away)
|
|
454
|
+
const EXACT_W = 5; // token == a whole defined symbol name
|
|
455
|
+
const SYM_MATCH_CAP = 4; // only the top-K highest-IDF symbol-component hits count
|
|
456
|
+
const PROX_FRAC = 0.2; // import-adjacency bonus = this × the strongest matched neighbour
|
|
457
|
+
const PROX_CAP_FRAC = 0.35; // capped at this × the module's own score
|
|
528
458
|
const isTestLabel = (s) => /(^|\/)tests?\//.test(s) || /(^|\/)test_[^/]*\.py$/.test(s) || /\.tests(\.|$)/.test(s);
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
// and shadow it in locate (B015: js-express injected examples/route-middleware/index.js at
|
|
532
|
-
// rank 1; java-gson's TOP2 slot 2 was a test-shrinker fixture). DEMOTED, not excluded: none of
|
|
533
|
-
// the B015 truths live under these paths (checked corpus/instances-*/…/spec.json 2026-07-02),
|
|
534
|
-
// but a future task whose truth IS a test/example file must stay reachable.
|
|
459
|
+
// opt-in via demoteNonProd: demote (not exclude) example/fixture/sample/demo/test-*
|
|
460
|
+
// paths, which share vocabulary with production modules and would otherwise shadow them
|
|
535
461
|
const NONPROD_DEMOTE = 0.15;
|
|
536
462
|
const isNonProdLabel = (s) => /(^|\/)(examples?|fixtures?|samples?|demos?|benchmarks?|test-[^/]+)(\/|$)/.test(s);
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
// the syntax-level C#/Java extractors emit ~none today, so this flag is Python-value only.
|
|
463
|
+
// opt-in via callAdjacency: same bounded-nudge shape as import-proximity, Python-value
|
|
464
|
+
// only today (C#/Java extractors emit ~no call edges)
|
|
540
465
|
const CALL_PROX_FRAC = 0.2;
|
|
541
466
|
const CALL_PROX_CAP_FRAC = 0.35;
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
// individuals carry `isAbstract` in all three. The only real distinguishing signal in the data
|
|
547
|
-
// is C#'s naming convention (interfaces prefixed `I<Uppercase>`, e.g. IBasketService) — and C#'s
|
|
548
|
-
// `inherits` edges point at an UNRESOLVED `ext:<Name>` id rather than the interface's own
|
|
549
|
-
// individual, so the object must be resolved by an exact label match against internal
|
|
550
|
-
// Class-labeled individuals. SCOPED to `.cs` implementer modules only: without that scope, 11 of
|
|
551
|
-
// django's 7,014 inherits edges superficially match `I[A-Z]` (IOBase, IExact, IContains, …ordinary
|
|
552
|
-
// Python class names, not interfaces) and would reintroduce the over-injection E1a already showed
|
|
553
|
-
// on class-heavy Python graphs. Java's `inherits` predicate resolves cleanly to real individuals
|
|
554
|
-
// but carries no tag or naming convention distinguishing interface implementation from concrete
|
|
555
|
-
// inheritance (TypeAdapterFactory IS an interface in Gson, no "I" prefix) — a Java-safe guard does
|
|
556
|
-
// not exist without an extractor change (E1c, deferred). E1b is C#-only until then.
|
|
467
|
+
// opt-in via implOfInterface: boost a module implementing an interface in a strongly-
|
|
468
|
+
// matched module. C#-only: interfaces are detected by the `I<Uppercase>` naming
|
|
469
|
+
// convention against unresolved `ext:<Name>` inherits targets; Java's inherits edges
|
|
470
|
+
// carry no equivalent signal to distinguish interface implementation from inheritance.
|
|
557
471
|
const IMPL_PROX_FRAC = 0.2;
|
|
558
472
|
const IMPL_PROX_CAP_FRAC = 0.35;
|
|
559
473
|
const isCsModuleLabel = (s) => /\.cs$/i.test(s);
|
|
560
474
|
const looksLikeCsInterface = (label) => /^I[A-Z]/.test(String(label || ""));
|
|
561
475
|
|
|
562
|
-
//
|
|
563
|
-
//
|
|
564
|
-
// only overlaps a decomposed identifier or a doc-comment elsewhere in that module (e.g. "billing
|
|
565
|
-
// calculation" never appears in `calculateTotalPrice`'s own path, only in its prose tokens).
|
|
566
|
-
// Same bounded-nudge shape/magnitude as the other proximity families — a nudge onto modules that
|
|
567
|
-
// ALREADY matched lexically (never a new zero-match candidate), never a replacement for the
|
|
568
|
-
// lexical score. NOT wired into any bench arm and NOT a shipped default — an available lever
|
|
569
|
-
// only, exactly like §5.15 beam search before it, pending its own gate/benchmark evidence.
|
|
476
|
+
// opt-in via proseBoost: bounded nudge for modules whose match comes from a
|
|
477
|
+
// decomposed identifier or doc-comment elsewhere in the module, not its path/symbol names
|
|
570
478
|
const PROSE_PROX_FRAC = 0.2;
|
|
571
479
|
const PROSE_PROX_CAP_FRAC = 0.35;
|
|
572
|
-
const PROSE_LOOKUP_LIMIT = 50;
|
|
573
|
-
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
|
|
585
|
-
//
|
|
586
|
-
const
|
|
587
|
-
const
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// PLAN_SEON_TUNING.md §7.5 finding 1 / §7.6(5a) (opt-in via literalMention, 2026-07-02): the query
|
|
593
|
-
// tokenizer split(/[^a-z0-9_]+/) DESTROYS a literal dotted module reference present verbatim in
|
|
594
|
-
// task text — "django.utils.http" scatters into {django,utils,http}, tokens so common across
|
|
595
|
-
// 2,931 modules that utils/http.py ranked 41 on B016's domain-filter — while every Module
|
|
596
|
-
// individual carries an unread `dotted` attribute. The lever scans the RAW query (threaded through
|
|
597
|
-
// as opts.rawQuery by searchModulesRanked) for whole, boundary-checked occurrences of each
|
|
598
|
-
// module's `dotted` name and repo-relative path (label). Boundary rule: a match flanked by an
|
|
599
|
-
// identifier/dotted/path continuation char ([a-z0-9_./]) does not count — which is also
|
|
600
|
-
// longest-match-wins for free: a package __init__'s dotted prefix ("django.utils" inside
|
|
601
|
-
// "django.utils.http") is followed by ".", so only the full module's own name fires (the two
|
|
602
|
-
// __init__.py prefix artifacts the 2026-07-02 review flagged). Specificity floor: a candidate
|
|
603
|
-
// with fewer than LIT_MIN_COMPONENTS dot/slash components never fires (a bare "utils" — or
|
|
604
|
-
// "django.utils" — must not). A hit adds a bounded BASE-score component weighted like the
|
|
605
|
-
// exact-symbol channel (LIT_W = EXACT_W per component IDF, top-LIT_COMP_CAP components like
|
|
606
|
-
// SYM_MATCH_CAP), then capped at LIT_CAP_FRAC × the strongest base score — the FRAC/CAP shape of
|
|
607
|
-
// the proximity families, anchored to the query's own best lexical evidence: a verbatim mention
|
|
608
|
-
// can lift a module INTO the top ranks but can never become an unbounded override. Applied
|
|
609
|
-
// BEFORE the proximity families so a mentioned module also donates adjacency like any other
|
|
610
|
-
// strong match. Only modules that already matched lexically are eligible (a mentioned module
|
|
611
|
-
// always is — its path components are query tokens by construction), preserving the levers'
|
|
612
|
-
// shared no-new-candidates safety scope.
|
|
613
|
-
const LIT_W = EXACT_W; // per-component weight — a verbatim module mention is the strongest locate signal
|
|
614
|
-
const LIT_MIN_COMPONENTS = 3; // "django.utils.http" fires; "django.utils"/"utils" never do
|
|
615
|
-
const LIT_COMP_CAP = 4; // like SYM_MATCH_CAP: only the top-K highest-IDF components accrue
|
|
616
|
-
const LIT_FRAC = 1.0; // bonus = min(litWeight × this, maxBase × LIT_CAP_FRAC)
|
|
617
|
-
const LIT_CAP_FRAC = 0.9; // … so a mention approaches — never dwarfs — the best lexical score
|
|
618
|
-
|
|
619
|
-
// PLAN_SEON_TUNING.md §7.6(5b) (opt-in via embedRank + an injected embedder, 2026-07-02): static-
|
|
620
|
-
// embedding re-rank — the deterministic "near-LLM" lever. The caller loads embed.mjs's
|
|
621
|
-
// potion-base-8M table (loadEmbedder(); null when the one-time-fetch weights are absent) and
|
|
622
|
-
// passes it as opts.embedder, keeping this module pure (no fs here; the flag no-ops with a
|
|
623
|
-
// one-time stderr note when the embedder is missing, so CI never needs the 30 MB artifact).
|
|
624
|
-
// Per-module text = path components + defined symbol names + doc first-lines — all read from the
|
|
625
|
-
// graph, never from source — embedded lazily and cached per process (EMB_CACHE, WeakMap-keyed on
|
|
626
|
-
// the graph). Cosine(query, module) becomes the same bounded FRAC/CAP nudge as the proximity
|
|
627
|
-
// families: only re-ranks modules that ALREADY matched lexically, never introduces a candidate.
|
|
480
|
+
const PROSE_LOOKUP_LIMIT = 50;
|
|
481
|
+
|
|
482
|
+
// opt-in via proseLayers: lets a query token that only matches via a normalised
|
|
483
|
+
// layer (stem/lemma/spell-corrected/canonical) still contribute, discounted (halved)
|
|
484
|
+
// relative to a verbatim match, via prose.mjs's proseLayerHits
|
|
485
|
+
const PROSE_LAYER_FRAC = 0.2;
|
|
486
|
+
const PROSE_LAYER_CAP_FRAC = 0.35;
|
|
487
|
+
const PROSE_LAYER_DISCOUNT = 0.5;
|
|
488
|
+
|
|
489
|
+
// opt-in via literalMention: recovers a literal dotted module reference the query
|
|
490
|
+
// tokenizer would otherwise scatter ("django.utils.http" -> {django,utils,http}),
|
|
491
|
+
// by scanning the raw query for whole, boundary-checked module dotted-name/path hits
|
|
492
|
+
const LIT_W = EXACT_W;
|
|
493
|
+
const LIT_MIN_COMPONENTS = 3; // "django.utils.http" fires; "utils" alone never does
|
|
494
|
+
const LIT_COMP_CAP = 4;
|
|
495
|
+
const LIT_FRAC = 1.0;
|
|
496
|
+
const LIT_CAP_FRAC = 0.9;
|
|
497
|
+
|
|
498
|
+
// opt-in via embedRank + an injected embedder: static-embedding re-rank over
|
|
499
|
+
// path/symbol/doc text read from the graph (never source), cached per-process in EMB_CACHE
|
|
628
500
|
const EMB_FRAC = 0.2;
|
|
629
501
|
const EMB_CAP_FRAC = 0.35;
|
|
630
|
-
const EMB_TEXT_SYMBOL_CAP = 64;
|
|
631
|
-
const EMB_TEXT_DOC_CAP = 12;
|
|
502
|
+
const EMB_TEXT_SYMBOL_CAP = 64;
|
|
503
|
+
const EMB_TEXT_DOC_CAP = 12;
|
|
632
504
|
const EMB_CACHE = new WeakMap(); // graph -> { embedder, texts, vecs: Map<moduleId, Float32Array> }
|
|
633
505
|
let embedWarned = false;
|
|
634
506
|
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
638
|
-
//
|
|
639
|
-
//
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
//
|
|
643
|
-
// Harpy's own beamwidth was a MARGIN/THRESHOLD relative to the ply's best score ("candidates
|
|
644
|
-
// that fall below a threshold of acceptability are pruned"), not a fixed count — this is a
|
|
645
|
-
// threshold+cap HYBRID (keep everyone within BEAM_MARGIN_FRAC of the ply's best, THEN cap at β),
|
|
646
|
-
// not naive top-k. A fixed-count beam would prematurely discard exactly the kind of weak-then-
|
|
647
|
-
// strong candidate E1b's own motivating case demonstrated: BasketService.cs sat at lexical rank 4
|
|
648
|
-
// and was only promoted by considering impl-of-interface structure beyond the first pass — a
|
|
649
|
-
// hard top-k cut at ply 0 could drop such a candidate before any later ply had a chance to
|
|
650
|
-
// recover it (Russell & Norvig's "local beam search... quickly becomes concentrated in a small
|
|
651
|
-
// region" failure mode, which Wikipedia's article cites for exactly this risk).
|
|
652
|
-
//
|
|
653
|
-
// Successors are generated PER EDGE KIND separately (not pooled then pruned once), so a dense
|
|
654
|
-
// edge type (imports) cannot crowd out a sparse-but-discriminative one (inherits) — each kind's
|
|
655
|
-
// survivors are computed independently, then MERGED (Harpy's own "candidate merging": two states
|
|
656
|
-
// reaching the same successor collapse to one path, keeping the better score). A short overflow
|
|
657
|
-
// list of near-miss pruned candidates is kept as a safety valve: if a ply's beam runs dry, the
|
|
658
|
-
// overflow is reconsidered rather than the walk simply stopping.
|
|
659
|
-
//
|
|
660
|
-
// SAFETY SCOPE: like every proximity family above, this only re-ranks modules that ALREADY
|
|
661
|
-
// matched lexically (present in `scored`) — it never introduces a zero-match candidate, so it
|
|
662
|
-
// cannot regress precision/over-injection the way an unbounded multi-hop walk could.
|
|
663
|
-
const BEAM_MARGIN_FRAC = 0.5; // keep ply candidates scoring >= (ply-best * this), before the cap
|
|
664
|
-
const BEAM_PROX_FRAC = 0.2; // bounded nudge — same shape/magnitude as the other proximity families
|
|
507
|
+
// opt-in via beamSearch: multi-ply adaptive expansion of the proximity nudge above.
|
|
508
|
+
// Beam width is a margin relative to each ply's best score (not a fixed count), so a
|
|
509
|
+
// weak-then-strong candidate isn't prematurely discarded. Successors are generated
|
|
510
|
+
// and pruned per edge kind independently so a dense kind (imports) can't crowd out a
|
|
511
|
+
// sparse one (inherits). Only re-ranks modules already present in `scored`.
|
|
512
|
+
const BEAM_MARGIN_FRAC = 0.5;
|
|
513
|
+
const BEAM_PROX_FRAC = 0.2;
|
|
665
514
|
const BEAM_PROX_CAP_FRAC = 0.35;
|
|
666
515
|
const BEAM_OVERFLOW_CAP = 4; // near-miss safety valve size
|
|
667
|
-
const BEAM_PLIES = 2;
|
|
516
|
+
const BEAM_PLIES = 2;
|
|
668
517
|
const BEAM_EDGE_GROUPS = [["imports"], ["calls", "callsSymbol"], ["inherits"], ["cochange"]];
|
|
669
518
|
|
|
670
|
-
// ----
|
|
671
|
-
//
|
|
672
|
-
//
|
|
673
|
-
// (it walks the graph from the seeds), so it can in principle lift a lexically-invisible truth into
|
|
674
|
-
// top-k — the whole point. cochange is dropped (temporal-coupling noise; see the research synthesis).
|
|
675
|
-
// • spiralDepth — max hop radius from the seeds (bounded ego expansion). Default 3.
|
|
676
|
-
// • mostDistinctiveBeams — degree-quantile gate q∈(0,1]: at each expansion step keep only the
|
|
677
|
-
// lowest-degree ⌊q·n⌋ candidates (drop the top (1−q) hubs); q=1.0 keeps
|
|
678
|
-
// all. Never empties the frontier (keeps ≥1 — the least-connected).
|
|
679
|
-
// • spiralNodeLimit — emit budget: how many newly-reached nodes the spiral surfaces. Held at
|
|
680
|
-
// 12 (MID-tier digest breadth, a KNOWN-DOABLE token budget) — a fixed
|
|
681
|
-
// budget, NOT a recall dial.
|
|
519
|
+
// ---- spiral expansion (opt-in, default off): a deterministic bounded-radius ego
|
|
520
|
+
// walk from the lexical seeds, ordered fewest-arcs-first with a degree-quantile hub
|
|
521
|
+
// gate. Unlike beamExpand it may introduce modules with no lexical match at all. ----
|
|
682
522
|
const SPIRAL_DEPTH_DEFAULT = 3;
|
|
683
523
|
const SPIRAL_NODE_LIMIT_DEFAULT = 12;
|
|
684
|
-
const SPIRAL_Q_DEFAULT = 0.9;
|
|
524
|
+
const SPIRAL_Q_DEFAULT = 0.9; // keep only the least-connected 90% at each step
|
|
685
525
|
const SPIRAL_EXPAND_KINDS = ["imports", "calls", "callsSymbol", "inherits"]; // cochange dropped
|
|
686
|
-
// The memory graph's
|
|
687
|
-
//
|
|
688
|
-
// passes so it walks Session/Fact/Source/Utterance individuals rather than code-graph Modules.
|
|
689
|
-
// NOTE: mgx:asksAbout (src/sessions.mjs) is deliberately EXCLUDED — that predicate lives in the
|
|
690
|
-
// CODE graph (Session ↔ code entities a chat turn resolved/answered), not the memory graph.
|
|
526
|
+
// The memory graph's edge-kind inventory a memory-graph spiralExpand call walks
|
|
527
|
+
// instead of code-graph Modules. mgx:asksAbout is excluded — it lives in the code graph.
|
|
691
528
|
export const MEMORY_SPIRAL_EXPAND_KINDS = ["saidInSession", "inReplyTo", "statedBy", "canonicalisedFrom"];
|
|
692
|
-
const SPIRAL_EMIT_FRAC = 0.5;
|
|
693
|
-
const SPIRAL_HOP_DECAY = 0.6;
|
|
694
|
-
const SPIRAL_PROX_FRAC = 0.2;
|
|
695
|
-
const SPIRAL_PROX_CAP_FRAC = 0.35;
|
|
696
|
-
|
|
697
|
-
/** embedRank: per-module embeddable text
|
|
698
|
-
* first-lines,
|
|
699
|
-
* caps. Built once per graph and cached alongside the vectors in EMB_CACHE. */
|
|
529
|
+
const SPIRAL_EMIT_FRAC = 0.5;
|
|
530
|
+
const SPIRAL_HOP_DECAY = 0.6;
|
|
531
|
+
const SPIRAL_PROX_FRAC = 0.2;
|
|
532
|
+
const SPIRAL_PROX_CAP_FRAC = 0.35;
|
|
533
|
+
|
|
534
|
+
/** embedRank: per-module embeddable text from path components + defined symbol
|
|
535
|
+
* names + doc first-lines, cached alongside the vectors in EMB_CACHE. */
|
|
700
536
|
function moduleEmbedTexts(graph) {
|
|
701
537
|
const texts = new Map(); // moduleId -> text
|
|
702
538
|
const defIdx = definesIndex(graph);
|
|
@@ -730,14 +566,9 @@ function identComponents(name) {
|
|
|
730
566
|
return new Set(String(name).replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean));
|
|
731
567
|
}
|
|
732
568
|
|
|
733
|
-
/**
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* edges live at function granularity), matching the existing E1a call-adjacency convention.
|
|
737
|
-
* `idNormalizer` (default null) lets a caller fold edge endpoints some OTHER way — the memory
|
|
738
|
-
* graph has no "containing module" concept, so a memory-graph caller passes `(id) => id` to walk
|
|
739
|
-
* its raw individual ids unchanged. Defaulting to null (rather than `moduleIdOfId` directly)
|
|
740
|
-
* keeps the sole existing caller (`adjacencyForKinds(graph, kinds)` in beamExpand) byte-identical. */
|
|
569
|
+
/** Undirected adjacency (Map<id, Set<id>>) over any edge in `kinds`. Endpoints
|
|
570
|
+
* are folded to their containing module by default; `idNormalizer` lets a
|
|
571
|
+
* caller (e.g. the memory graph, which has no "module" concept) walk raw ids instead. */
|
|
741
572
|
export function adjacencyForKinds(graph, kinds, idNormalizer = null) {
|
|
742
573
|
const norm = idNormalizer || ((id) => moduleIdOfId(graph, id));
|
|
743
574
|
const adj = new Map();
|
|
@@ -756,16 +587,14 @@ export function adjacencyForKinds(graph, kinds, idNormalizer = null) {
|
|
|
756
587
|
return adj;
|
|
757
588
|
}
|
|
758
589
|
|
|
759
|
-
/** Beam-search-style multi-
|
|
760
|
-
*
|
|
761
|
-
* it boosts — same bounded-nudge shape as the single-hop proximity families, just reachable over
|
|
762
|
-
* more than one hop when a ply's beam survives that far. Pure otherwise (no fs/network). */
|
|
590
|
+
/** Beam-search-style multi-ply expansion (see BEAM_* constants above). Mutates
|
|
591
|
+
* `s.score` in place on boosted `scored` entries. */
|
|
763
592
|
function beamExpand(graph, scored, beamWidth) {
|
|
764
593
|
if (scored.length < 2) return;
|
|
765
594
|
const byId = new Map(scored.map((s) => [s.ind.id, s]));
|
|
766
595
|
const baseScore = new Map(scored.map((s) => [s.ind.id, s.score]));
|
|
767
596
|
|
|
768
|
-
// Margin+cap prune a candidate-score Map down to this ply's beam, returning [survivors, overflow]
|
|
597
|
+
// Margin+cap prune a candidate-score Map down to this ply's beam, returning [survivors, overflow]
|
|
769
598
|
const pruneToBeam = (candidates) => {
|
|
770
599
|
if (!candidates.size) return [[], []];
|
|
771
600
|
let best = 0;
|
|
@@ -780,13 +609,12 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
780
609
|
return [survivors, overflow];
|
|
781
610
|
};
|
|
782
611
|
|
|
783
|
-
// Ply 0 beam = the current top-scoring already-matched modules
|
|
612
|
+
// Ply 0 beam = the current top-scoring already-matched modules
|
|
784
613
|
let [beam, overflow] = pruneToBeam(new Map(scored.map((s) => [s.ind.id, s.score])));
|
|
785
614
|
const boosted = new Set(beam.map(([id]) => id));
|
|
786
615
|
|
|
787
616
|
for (let ply = 0; ply < BEAM_PLIES && beam.length; ply++) {
|
|
788
|
-
//
|
|
789
|
-
// like imports can't crowd out a sparse-but-discriminative one like inherits), then merged.
|
|
617
|
+
// per-edge-kind successor generation, scored/pruned independently, then merged
|
|
790
618
|
const merged = new Map(); // successorId -> best propagated score across all kinds this ply
|
|
791
619
|
const plyOverflow = [];
|
|
792
620
|
for (const kinds of BEAM_EDGE_GROUPS) {
|
|
@@ -803,8 +631,6 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
803
631
|
plyOverflow.push(...kindOverflow);
|
|
804
632
|
}
|
|
805
633
|
overflow.push(...plyOverflow);
|
|
806
|
-
// Apply the bounded nudge once per module (first ply it's reached), same shape as the other
|
|
807
|
-
// proximity families — a nudge, never a replacement.
|
|
808
634
|
for (const [id, propagated] of merged) {
|
|
809
635
|
if (boosted.has(id)) continue;
|
|
810
636
|
const s = byId.get(id);
|
|
@@ -813,57 +639,21 @@ function beamExpand(graph, scored, beamWidth) {
|
|
|
813
639
|
boosted.add(id);
|
|
814
640
|
}
|
|
815
641
|
beam = [...merged.entries()];
|
|
816
|
-
//
|
|
817
|
-
// just stopping — cheap insurance against a total pruning failure.
|
|
642
|
+
// if this ply's beam ran dry, reconsider the near-miss overflow instead of stopping
|
|
818
643
|
if (!beam.length && overflow.length) {
|
|
819
644
|
beam = overflow.splice(0, BEAM_OVERFLOW_CAP).filter(([id]) => !boosted.has(id));
|
|
820
645
|
}
|
|
821
646
|
}
|
|
822
647
|
}
|
|
823
648
|
|
|
824
|
-
/**
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* a
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
* ceiling. Mutates `scored` (nudges re-reached matches in place; APPENDS newly-surfaced modules)
|
|
833
|
-
* when the score-nudge machinery is active. Pure otherwise (no fs/network); deterministic total
|
|
834
|
-
* ordering throughout.
|
|
835
|
-
*
|
|
836
|
-
* Generalised (2026-07-11) past its original code-graph-only, `scored`-only shape so a pure
|
|
837
|
-
* graph-visualisation walk (no lexical match list at all) can reuse the exact same traversal:
|
|
838
|
-
* - `scored` is now OPTIONAL (default `[]`) — a bare walk with no ranking machinery.
|
|
839
|
-
* - `kinds` (default `SPIRAL_EXPAND_KINDS`) — the edge-kind set to walk; a memory-graph caller
|
|
840
|
-
* passes `MEMORY_SPIRAL_EXPAND_KINDS`.
|
|
841
|
-
* - `classPredicate` (default `(ind) => (ind.class || "") === "Module"`) — replaces the two
|
|
842
|
-
* hardcoded `"Module"` checks below, so a memory-graph caller can pass `() => true` (every
|
|
843
|
-
* class walkable) or any other individual filter.
|
|
844
|
-
* - `idNormalizer` (default `null`) — threaded straight into the internal `adjacencyForKinds`
|
|
845
|
-
* call; a memory-graph caller passes `(id) => id` (no module-folding).
|
|
846
|
-
* - `seeds` (default derived from `scored`, as before) — an explicit id iterable, so a caller
|
|
847
|
-
* with no `scored` list at all (e.g. `mostRecentIndividual`'s single seed) can still drive
|
|
848
|
-
* the walk.
|
|
849
|
-
* - `hubDegree` (default `Infinity`, PLAN_VIZ_MEMORY.md's page-size strategy — seonix's own
|
|
850
|
-
* third cap, default 40 there): stop expanding THROUGH a node with MORE than this many
|
|
851
|
-
* in-graph neighbours over `kinds` — the node itself is still popped/emitted normally (still
|
|
852
|
-
* shown), it just contributes no candidates for the NEXT hop. Distinct from `q` (a relative,
|
|
853
|
-
* per-step quantile gate that always keeps at least one candidate) and from `nodeLimit` (a
|
|
854
|
-
* total emit budget): `hubDegree` is an absolute per-node gate that can drop a hub's entire
|
|
855
|
-
* fan-out to zero, so an ultra-common hypernym ("thing", "entity" — reachable from thousands
|
|
856
|
-
* of IsA facts) can't swallow the whole node budget in one hop. `Infinity` (no gate) keeps
|
|
857
|
-
* every existing caller byte-identical. EXEMPTS the seed(s) (hop 0) themselves — a walk
|
|
858
|
-
* started directly ON a hub (e.g. `tmct viz --term tree` where "tree" is a 1,972-fact
|
|
859
|
-
* ConceptNet hub, measured live this session) still shows that hub's own immediate
|
|
860
|
-
* neighbourhood; only a hub reached MID-walk (hop > 0) has its own further fan-out gated.
|
|
861
|
-
* The score-nudge machinery (mutating `scored`/introducing newly-surfaced individuals into it)
|
|
862
|
-
* is gated behind `scored.length > 0 && maxSeed > 0` — the exact condition the original early
|
|
863
|
-
* return checked — so an empty `scored` degrades gracefully into a pure walk rather than erroring.
|
|
864
|
-
* Returns `[{id, hop}]` for every node the walk actually pops (seeds included, at hop 0) — this
|
|
865
|
-
* used to return `undefined`; safe, since the sole caller (`scoreModules`) already discards the
|
|
866
|
-
* return value (confirmed by inspection, not assumed). */
|
|
649
|
+
/** Deterministic bounded-radius ego walk from the lexical seeds (`scored`, or
|
|
650
|
+
* an explicit `seeds` override), popped fewest-arcs-first with a degree-
|
|
651
|
+
* quantile hub gate. Unlike beamExpand, it may push modules with no lexical
|
|
652
|
+
* match into `scored`. `kinds`/`classPredicate`/`idNormalizer` let a
|
|
653
|
+
* memory-graph caller reuse the same walk over its own individuals/edges;
|
|
654
|
+
* `hubDegree` (default Infinity) caps a node's own fan-out so a hypernym hub
|
|
655
|
+
* can't swallow the whole emit budget, but never blocks a walk started
|
|
656
|
+
* directly on that hub. Returns `[{id, hop}]` for every popped node. */
|
|
867
657
|
export function spiralExpand(graph, scored = [], {
|
|
868
658
|
depth = SPIRAL_DEPTH_DEFAULT,
|
|
869
659
|
q = SPIRAL_Q_DEFAULT,
|
|
@@ -943,19 +733,10 @@ export function spiralExpand(graph, scored = [], {
|
|
|
943
733
|
emitted++;
|
|
944
734
|
}
|
|
945
735
|
if (node.hop >= depth) continue;
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
// `tmct viz --term tree` (a 1,972-fact hub term) with the gate applied unconditionally
|
|
950
|
-
// returned a single, useless lone node — seeding directly ON a term the user explicitly
|
|
951
|
-
// asked to centre on must always show ITS OWN immediate neighbourhood, or the whole
|
|
952
|
-
// `--term`/click-to-recentre feature is pointless on exactly the popular, interesting terms
|
|
953
|
-
// it exists for. A hub only reached mid-walk (hop > 0) still gates normally — this only
|
|
954
|
-
// changes the walk's own STARTING point(s), not general hub suppression elsewhere.
|
|
736
|
+
// exempts hop 0: seeding directly on a hub term must still show its own
|
|
737
|
+
// immediate neighbourhood, or --term/click-to-recentre is pointless on exactly
|
|
738
|
+
// the popular terms it exists for
|
|
955
739
|
if (node.hop > 0 && degree(node.id) > hubDegree) continue;
|
|
956
|
-
// This step's candidate set = the popped node's unvisited neighbours matching classPredicate;
|
|
957
|
-
// quantile-gate by degree, keeping the lowest-degree ⌊q·n⌋ (drop the densest hubs), never
|
|
958
|
-
// fewer than one.
|
|
959
740
|
const cands = [];
|
|
960
741
|
for (const nid of adj.get(node.id) || []) {
|
|
961
742
|
if (visited.has(nid)) continue;
|
|
@@ -975,13 +756,9 @@ export function spiralExpand(graph, scored = [], {
|
|
|
975
756
|
return results;
|
|
976
757
|
}
|
|
977
758
|
|
|
978
|
-
/** The individual with the most recent `createdAtProp` attribute
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* same timestamp — same total-order convention `spiralExpand`'s own heap uses. Null when no
|
|
982
|
-
* individual carries the attribute at all (empty graph, or a graph that predates timestamps).
|
|
983
|
-
* ISO-8601 timestamps compare correctly as plain strings (same zero-padded width throughout this
|
|
984
|
-
* codebase), so no Date parsing is needed. */
|
|
759
|
+
/** The individual with the most recent `createdAtProp` attribute (ties break
|
|
760
|
+
* on lowest id); null if none carry the attribute. ISO-8601 timestamps
|
|
761
|
+
* compare correctly as plain strings, so no Date parsing is needed. */
|
|
985
762
|
export function mostRecentIndividual(graph, createdAtProp = CREATED_AT_PROP) {
|
|
986
763
|
let best = null; // { ind, v }
|
|
987
764
|
for (const ind of graph?.individuals || []) {
|
|
@@ -992,11 +769,9 @@ export function mostRecentIndividual(graph, createdAtProp = CREATED_AT_PROP) {
|
|
|
992
769
|
return best ? best.ind : null;
|
|
993
770
|
}
|
|
994
771
|
|
|
995
|
-
/**
|
|
996
|
-
* IDF-weights each query token
|
|
997
|
-
*
|
|
998
|
-
* + EXACT-symbol matches, re-ranks with a bounded import-proximity bonus, and breaks ties by
|
|
999
|
-
* matched-symbol DENSITY (a concrete signal — never ground truth). Pure; deterministic. */
|
|
772
|
+
/** Shared module-ranking core behind renderSearch and searchModulesRanked.
|
|
773
|
+
* IDF-weights each query token, scores path/symbol/exact-symbol matches, and
|
|
774
|
+
* re-ranks with a bounded import-proximity bonus. Pure; deterministic. */
|
|
1000
775
|
function scoreModules(graph, tokens, opts = {}) {
|
|
1001
776
|
const { demoteNonProd = false, callAdjacency = false, implOfInterface = false, beamSearch = false, spiral = false, proseBoost = false, proseLayers = false, literalMention = false, embedRank = false, rawQuery = "" } = opts;
|
|
1002
777
|
const beamWidth = Number.isFinite(opts.beamWidth) && opts.beamWidth > 0 ? opts.beamWidth : 8;
|
|
@@ -1011,19 +786,13 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1011
786
|
const symSet = new Set(defines.map((d) => d.toLowerCase())); // exact symbol names
|
|
1012
787
|
const symComps = new Set();
|
|
1013
788
|
for (const d of defines) for (const c of identComponents(d)) symComps.add(c);
|
|
1014
|
-
// literalMention only: the Module's `dotted` attribute (mgx:dotted) — the verbatim form a
|
|
1015
|
-
// task statement uses ("django.utils.http"); "" when absent. Gated so OFF does zero work.
|
|
1016
789
|
const dotted = literalMention
|
|
1017
790
|
? String((ind.attributes || []).find((a) => a.key === "dotted")?.value || "").toLowerCase()
|
|
1018
791
|
: "";
|
|
1019
792
|
modules.push({ ind, label, labelLc, defines, symSet, symComps, dotted });
|
|
1020
793
|
}
|
|
1021
794
|
const N = modules.length || 1;
|
|
1022
|
-
//
|
|
1023
|
-
// decides. df = modules where the token appears in the path (substring — keeps "filter" matching
|
|
1024
|
-
// "defaultfilters"), as a symbol component, or as an exact symbol name. A loose path substring like
|
|
1025
|
-
// "text" that hits many modules therefore earns a low weight, so "ci<text>" can't beat utils/text.py.
|
|
1026
|
-
// idf = log(1 + N/(1+df)) → ~0 for ubiquitous tokens, large for rare ones.
|
|
795
|
+
// idf = log(1 + N/(1+df)): near-zero for ubiquitous tokens, large for rare ones
|
|
1027
796
|
const idf = new Map();
|
|
1028
797
|
for (const t of tokens) {
|
|
1029
798
|
if (idf.has(t)) continue;
|
|
@@ -1047,28 +816,22 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1047
816
|
for (let i = 0; i < Math.min(compWeights.length, SYM_MATCH_CAP); i++) symScore += compWeights[i] * SYM_W;
|
|
1048
817
|
let score = exactScore + pathScore + symScore;
|
|
1049
818
|
if (!score) continue;
|
|
1050
|
-
if (demoteNonProd && (isTestLabel(m.labelLc) || isNonProdLabel(m.labelLc))) score *= NONPROD_DEMOTE;
|
|
819
|
+
if (demoteNonProd && (isTestLabel(m.labelLc) || isNonProdLabel(m.labelLc))) score *= NONPROD_DEMOTE;
|
|
1051
820
|
else if (isTestLabel(m.labelLc)) score *= 0.4; // source first; tests still discoverable
|
|
1052
821
|
const matching = m.defines.filter((d) => { const dl = d.toLowerCase(); const cs = identComponents(d); return tokens.some((t) => dl === t || cs.has(t)); });
|
|
1053
822
|
const density = m.defines.length ? matchCount / m.defines.length : 0;
|
|
1054
823
|
scored.push({ ind: m.ind, score, defineCount: m.defines.length, matching, density });
|
|
1055
824
|
}
|
|
1056
|
-
//
|
|
1057
|
-
// see the LIT_* constants' comment above for the full design. Runs before the proximity
|
|
1058
|
-
// families so a mentioned module donates adjacency like any other strong match.
|
|
825
|
+
// literalMention: runs before the proximity families so a mentioned module donates adjacency too
|
|
1059
826
|
if (literalMention && rawQuery && scored.length) {
|
|
1060
827
|
const rawLc = String(rawQuery).toLowerCase();
|
|
1061
828
|
const continues = (ch) => ch != null && /[a-z0-9_./]/.test(ch);
|
|
1062
|
-
// Whole, boundary-checked occurrence of `cand` in the raw query (see boundary rule above).
|
|
1063
829
|
const mentioned = (cand) => {
|
|
1064
830
|
for (let i = rawLc.indexOf(cand); i !== -1; i = rawLc.indexOf(cand, i + 1)) {
|
|
1065
831
|
if (!continues(rawLc[i - 1]) && !continues(rawLc[i + cand.length])) return true;
|
|
1066
832
|
}
|
|
1067
833
|
return false;
|
|
1068
834
|
};
|
|
1069
|
-
// IDF for a candidate's components: normally already in the map (they are query tokens by
|
|
1070
|
-
// construction when tokens came from this same raw query); computed-and-cached otherwise
|
|
1071
|
-
// (a caller passing mismatched tokens/rawQuery must not crash or skew).
|
|
1072
835
|
const idfOf = (t) => {
|
|
1073
836
|
if (!idf.has(t)) {
|
|
1074
837
|
let df = 0;
|
|
@@ -1086,9 +849,8 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1086
849
|
let litWeight = 0; // best single matched candidate (dotted vs path share components anyway)
|
|
1087
850
|
for (const cand of new Set([m.dotted, m.labelLc])) {
|
|
1088
851
|
if (!cand) continue;
|
|
1089
|
-
if (cand.split(/[./]+/).filter(Boolean).length < LIT_MIN_COMPONENTS) continue;
|
|
852
|
+
if (cand.split(/[./]+/).filter(Boolean).length < LIT_MIN_COMPONENTS) continue;
|
|
1090
853
|
if (!mentioned(cand)) continue;
|
|
1091
|
-
// IDF-weight the candidate's tokens (same tokenizer as the query), highest first.
|
|
1092
854
|
const weights = [...new Set(cand.split(/[^a-z0-9_]+/).filter(Boolean))].map(idfOf).sort((a, b) => b - a);
|
|
1093
855
|
let w = 0;
|
|
1094
856
|
for (let i = 0; i < Math.min(weights.length, LIT_COMP_CAP); i++) w += weights[i] * LIT_W;
|
|
@@ -1097,9 +859,7 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1097
859
|
if (litWeight) s.score += Math.min(litWeight * LIT_FRAC, maxBase * LIT_CAP_FRAC);
|
|
1098
860
|
}
|
|
1099
861
|
}
|
|
1100
|
-
//
|
|
1101
|
-
// STRONGER-matching module gets a bonus proportional to that neighbour, so a genuine 2nd module
|
|
1102
|
-
// (truncatelines' text.py) rises with its sibling. Only re-ranks modules that ALREADY matched.
|
|
862
|
+
// import-graph proximity: a matched module importing/imported-by a stronger match rises with it
|
|
1103
863
|
if (scored.length > 1) {
|
|
1104
864
|
const baseById = new Map(scored.map((s) => [s.ind.id, s.score]));
|
|
1105
865
|
const adj = new Map();
|
|
@@ -1116,11 +876,7 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1116
876
|
s.score += Math.min(bestNeighbor * PROX_FRAC, s.score * PROX_CAP_FRAC);
|
|
1117
877
|
}
|
|
1118
878
|
}
|
|
1119
|
-
//
|
|
1120
|
-
// stronger-matching module rises with it (initials-filter: defaultfilters.py calls into
|
|
1121
|
-
// utils/text.py, whose lexical rank was 8). Call edges live at function level, so endpoints
|
|
1122
|
-
// map to their containing modules first. Same bounded-nudge formula as import-proximity;
|
|
1123
|
-
// only re-ranks modules that already matched.
|
|
879
|
+
// callAdjacency: same formula as import-proximity, over resolved call edges (folded to modules)
|
|
1124
880
|
if (callAdjacency && scored.length > 1) {
|
|
1125
881
|
const baseById = new Map(scored.map((s) => [s.ind.id, s.score]));
|
|
1126
882
|
const adj = new Map();
|
|
@@ -1142,12 +898,7 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1142
898
|
s.score += Math.min(bestNeighbor * CALL_PROX_FRAC, s.score * CALL_PROX_CAP_FRAC);
|
|
1143
899
|
}
|
|
1144
900
|
}
|
|
1145
|
-
//
|
|
1146
|
-
// stronger-matching module rises with it (eshoponweb: IBasketService.cs rank 1, BasketService.cs
|
|
1147
|
-
// rank 4). `inherits` edges point the OBJECT at an unresolved `ext:<Name>` id for C#, so resolve
|
|
1148
|
-
// by exact label match against internal Class individuals. Only re-ranks modules that already
|
|
1149
|
-
// matched, and only when both the implementer module is `.cs` and the base name looks like a C#
|
|
1150
|
-
// interface (see the const block above for why — isAbstract does not exist in the data).
|
|
901
|
+
// implOfInterface: a C# module implementing an interface rises with the interface's own match
|
|
1151
902
|
if (implOfInterface && scored.length > 1) {
|
|
1152
903
|
const baseById = new Map(scored.map((s) => [s.ind.id, s.score]));
|
|
1153
904
|
const classByLabel = new Map();
|
|
@@ -1171,13 +922,7 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1171
922
|
s.score += Math.min(bestNeighbor * IMPL_PROX_FRAC, s.score * IMPL_PROX_CAP_FRAC);
|
|
1172
923
|
}
|
|
1173
924
|
}
|
|
1174
|
-
//
|
|
1175
|
-
// prose tokens — see the PROSE_PROX_* comment above for the full rationale. One
|
|
1176
|
-
// lookupByProseTokens call for the whole query (not per-module), then aggregated into a
|
|
1177
|
-
// per-module signal via moduleIdOfId, same as the call-adjacency/impl-of-interface families.
|
|
1178
|
-
// Unlike the proximity families above, this signal is absolute per-module (prose-token
|
|
1179
|
-
// overlap), not relative to a stronger NEIGHBOUR in `scored` — so it applies even when
|
|
1180
|
-
// only one module matched lexically (no ">1" gate needed).
|
|
925
|
+
// proseBoost: absolute per-module prose-token overlap, so it applies with only one match too
|
|
1181
926
|
if (proseBoost && scored.length && graph.proseIndex) {
|
|
1182
927
|
const proseHits = lookupByProseTokens(graph.proseIndex, tokens.join(" "), { limit: PROSE_LOOKUP_LIMIT });
|
|
1183
928
|
if (proseHits.length) {
|
|
@@ -1194,15 +939,8 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1194
939
|
}
|
|
1195
940
|
}
|
|
1196
941
|
}
|
|
1197
|
-
//
|
|
1198
|
-
//
|
|
1199
|
-
// adds a bounded, discounted signal — see the PROSE_LAYER_* comment above. One proseLayerHits call
|
|
1200
|
-
// per DISTINCT query token, ids folded to their containing module via moduleIdOfId (same as the
|
|
1201
|
-
// proseBoost/call-adjacency families). Only tokens NOT already matching a module lexically count
|
|
1202
|
-
// for that module (a layer hit is purely ADDITIVE evidence for otherwise-missed words — never
|
|
1203
|
-
// double-counting a token the base score already saw), weighted by the token's own IDF (so a
|
|
1204
|
-
// ubiquitous word contributes almost nothing) and halved (PROSE_LAYER_DISCOUNT: weaker than a
|
|
1205
|
-
// verbatim match), then the shared FRAC/CAP nudge. Only re-ranks modules already in `scored`.
|
|
942
|
+
// proseLayers: a token that only matches via a normalised layer adds discounted, additive
|
|
943
|
+
// evidence — never double-counted against a token the base score already saw
|
|
1206
944
|
if (proseLayers && scored.length && graph.proseIndex) {
|
|
1207
945
|
const scoredById = new Map(scored.map((s) => [s.ind.id, s]));
|
|
1208
946
|
const modById = new Map(modules.map((m) => [m.ind.id, m]));
|
|
@@ -1217,9 +955,9 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1217
955
|
const modId = moduleIdOfId(graph, id);
|
|
1218
956
|
if (!modId || hitMods.has(modId)) continue;
|
|
1219
957
|
hitMods.add(modId);
|
|
1220
|
-
if (!scoredById.has(modId)) continue;
|
|
958
|
+
if (!scoredById.has(modId)) continue; // never a new zero-match candidate
|
|
1221
959
|
const m = modById.get(modId);
|
|
1222
|
-
if (m && (m.symSet.has(t) || m.symComps.has(t) || m.labelLc.includes(t))) continue; // already matched lexically
|
|
960
|
+
if (m && (m.symSet.has(t) || m.symComps.has(t) || m.labelLc.includes(t))) continue; // already matched lexically
|
|
1223
961
|
layerSignal.set(modId, (layerSignal.get(modId) || 0) + w * PROSE_LAYER_DISCOUNT);
|
|
1224
962
|
}
|
|
1225
963
|
}
|
|
@@ -1229,10 +967,8 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1229
967
|
s.score += Math.min(signal * PROSE_LAYER_FRAC, s.score * PROSE_LAYER_CAP_FRAC);
|
|
1230
968
|
}
|
|
1231
969
|
}
|
|
1232
|
-
//
|
|
1233
|
-
//
|
|
1234
|
-
// this module stays fs-free; absent embedder → no-op with a one-time stderr note, never a
|
|
1235
|
-
// failure (the 30 MB weights are a local opt-in fetch, not a test/CI dependency).
|
|
970
|
+
// embedRank: the embedder is injected so this module stays fs-free; absent -> a one-time
|
|
971
|
+
// stderr note, never a failure
|
|
1236
972
|
if (embedRank) {
|
|
1237
973
|
if (!opts.embedder) {
|
|
1238
974
|
if (!embedWarned) {
|
|
@@ -1261,9 +997,9 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1261
997
|
}
|
|
1262
998
|
}
|
|
1263
999
|
}
|
|
1264
|
-
//
|
|
1000
|
+
// beamSearch (opt-in): multi-ply generalization of the single-hop families above.
|
|
1265
1001
|
if (beamSearch && scored.length > 1) beamExpand(graph, scored, beamWidth);
|
|
1266
|
-
// SPIRAL (opt-in): bounded-radius ego walk that
|
|
1002
|
+
// SPIRAL (opt-in): bounded-radius ego walk that may introduce lexically-invisible modules — runs
|
|
1267
1003
|
// last (after every family has finalised the seed scores) so its seed-relative emit scores and
|
|
1268
1004
|
// hub gate read the settled ranking, and before the sort so surfaced nodes slot into it.
|
|
1269
1005
|
if (spiral && scored.length) spiralExpand(graph, scored, {
|
|
@@ -1276,12 +1012,12 @@ function scoreModules(graph, tokens, opts = {}) {
|
|
|
1276
1012
|
return scored;
|
|
1277
1013
|
}
|
|
1278
1014
|
|
|
1279
|
-
/**
|
|
1015
|
+
/** The ranked module list as plain `{path, score}` (highest-first), using the same
|
|
1280
1016
|
* ranking renderSearch uses (path + symbol + exact-symbol + import-proximity). Lets the rig
|
|
1281
|
-
* read the score
|
|
1017
|
+
* read the score gap between rank-1 and rank-2 (which the text renderer hides) so it can keep
|
|
1282
1018
|
* rank-2 only when it is close. Pure; deterministic.
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
1019
|
+
* Note: scoreModules still ranks (locate always returns modules), but the score-gap top-1
|
|
1020
|
+
* selection that consumes this gap is off by default in run.mjs/selectModules — it over-injected
|
|
1285
1021
|
* on some tasks. The shipped default takes the top-2 instead. */
|
|
1286
1022
|
export function searchModulesRanked(graph, query, opts = {}) {
|
|
1287
1023
|
const raw = String(query || "");
|
|
@@ -1294,28 +1030,15 @@ export function searchModulesRanked(graph, query, opts = {}) {
|
|
|
1294
1030
|
return scoreModules(graph, tokens, effOpts).map((s) => ({ path: String(s.ind.label), score: s.score }));
|
|
1295
1031
|
}
|
|
1296
1032
|
|
|
1297
|
-
//
|
|
1298
|
-
//
|
|
1299
|
-
// ≥50%-vs-otb bar outright on order-service-total). See PLAN_B016.md §6.9. 0.6 is the exact ratio
|
|
1300
|
-
// tested throughout — do not drift it from bench/arms.mjs's arm values or scripts/rank-gate.mjs's
|
|
1301
|
-
// --gap default; all three should read this constant.
|
|
1033
|
+
// 0.6 is the exact ratio tested throughout — do not drift it from bench/arms.mjs's arm values
|
|
1034
|
+
// or scripts/rank-gate.mjs's --gap default; all three should read this constant.
|
|
1302
1035
|
export const DEFAULT_SCORE_GAP = 0.6;
|
|
1303
1036
|
|
|
1304
|
-
/**
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
* (cli.mjs's query-based `digest`) and the bench rig (bench/run.mjs's selectModules).
|
|
1310
|
-
*
|
|
1311
|
-
* DELIBERATELY NEUTRAL BY DEFAULT: `scoreGapK` defaults to `null` (gap-extension OFF, plain
|
|
1312
|
-
* top-`top_k`) here — the SHIPPED default of `DEFAULT_SCORE_GAP` is a product-surface policy
|
|
1313
|
-
* decision, applied explicitly by the caller (cli.mjs's digest query-mode), not baked into this
|
|
1314
|
-
* primitive. A library default of "on" would make every future caller who forgets to pass
|
|
1315
|
-
* `scoreGapK` silently inherit gap-extension — including future bench arms, breaking the
|
|
1316
|
-
* paired-arm "byte-identical when off" comparability this repo's whole measurement methodology
|
|
1317
|
-
* depends on. See test/selectRankedModules.test.mjs's "absent scoreGapK is byte-identical to
|
|
1318
|
-
* plain top-k" case. */
|
|
1037
|
+
/** Take the top_k ranked hits, then extend to ranks (top_k)..2 whose score
|
|
1038
|
+
* sits within `scoreGapK` of rank 1 — the near-tie case where another module
|
|
1039
|
+
* is genuinely as relevant as the top hit. `scoreGapK` defaults to null
|
|
1040
|
+
* (off): the shipped `DEFAULT_SCORE_GAP` is a caller-applied policy, not a
|
|
1041
|
+
* library default, so bench arms stay byte-identical when the flag is off. */
|
|
1319
1042
|
export function selectRankedModules(ranked, { top_k = 2, scoreGapK = null } = {}) {
|
|
1320
1043
|
if (!ranked.length || top_k <= 0) return [];
|
|
1321
1044
|
const picked = ranked.slice(0, top_k).map((r) => r.path);
|
|
@@ -1355,39 +1078,24 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1355
1078
|
return lines.join("\n");
|
|
1356
1079
|
}
|
|
1357
1080
|
|
|
1358
|
-
// ----
|
|
1081
|
+
// ---- read-replacing tools (members / inheritance / architecture / coverage /
|
|
1359
1082
|
// history / call neighbours). Each answers ONE question in one compact call so
|
|
1360
1083
|
// the agent need not Read/Grep. All keep the bounded-output discipline. -------
|
|
1361
1084
|
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
* parseEntities builds it (every refresh constructs a NEW graph object), so caching
|
|
1366
|
-
* keyed on graph object identity is correctness-safe for a graph's whole lifetime —
|
|
1367
|
-
* same invariant qualCache already relies on in production. edgesOfKind is called
|
|
1368
|
-
* repeatedly on the SAME (graph, kind) pair across a single query's traversal
|
|
1369
|
-
* (evalSet/traverse/adjacencyForKinds/renderArchitecture/… all re-derive it), and at
|
|
1370
|
-
* monorepo scale (tens of thousands of modules) that repeated O(relations) scan is a
|
|
1371
|
-
* real latency/GC cost — this collapses every call after the first to an O(1) lookup. */
|
|
1085
|
+
// Per-graph, per-kind memo: a loaded graph's `relations` are never mutated in
|
|
1086
|
+
// place (a refresh builds a new graph object), so caching on graph identity
|
|
1087
|
+
// is correctness-safe and collapses repeated O(relations) scans to O(1).
|
|
1372
1088
|
const edgesOfKindCache = new WeakMap();
|
|
1373
1089
|
|
|
1374
|
-
/** All edges
|
|
1375
|
-
|
|
1376
|
-
* cochange/reexports/callsSymbol/touchesSymbol/contains — see relationKind/PROP_KIND above),
|
|
1377
|
-
* flattened across every raw relation group that classifies to it. Exported for ask.mjs's
|
|
1378
|
-
* mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate.
|
|
1379
|
-
* Memoized per (graph, kind) — see edgesOfKindCache's own doc above (perf lever, HANDOVER
|
|
1380
|
-
* follow-up #8: latency/GC on monorepo-scale graphs, not a correctness fix — the earlier
|
|
1381
|
-
* stack-overflow bug below is already fixed and unrelated). */
|
|
1090
|
+
/** All edges of a classified relation kind, flattened across every raw
|
|
1091
|
+
* relation group that classifies to it. Memoized per (graph, kind). */
|
|
1382
1092
|
export function edgesOfKind(graph, kind) {
|
|
1383
1093
|
let byKind = edgesOfKindCache.get(graph);
|
|
1384
1094
|
if (!byKind) { byKind = new Map(); edgesOfKindCache.set(graph, byKind); }
|
|
1385
1095
|
const cached = byKind.get(kind);
|
|
1386
1096
|
if (cached) return cached;
|
|
1387
1097
|
const out = [];
|
|
1388
|
-
//
|
|
1389
|
-
// element as a call argument and overflows the stack past ~100k edges (live report:
|
|
1390
|
-
// 27,770-module repo, "list modules in <dir>" → "Maximum call stack size exceeded").
|
|
1098
|
+
// plain-loop append, not spread: argument spread overflows the stack past ~100k edges
|
|
1391
1099
|
for (const g of graph.relations) {
|
|
1392
1100
|
if (relationKind(g) !== kind) continue;
|
|
1393
1101
|
for (const e of g.edges) out.push(e);
|
|
@@ -1396,15 +1104,9 @@ export function edgesOfKind(graph, kind) {
|
|
|
1396
1104
|
return out;
|
|
1397
1105
|
}
|
|
1398
1106
|
|
|
1399
|
-
/** A node's "last touched" moment
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
* subject or object — whichever is newer. `""` when nothing carries a timestamp at all. Compares
|
|
1403
|
-
* ISO-8601 strings directly (correct for same-width zero-padded timestamps, no Date parsing).
|
|
1404
|
-
* Tolerates edges with no `createdAt` field (pre-dating `upsertEdge`'s own stamp, or written by
|
|
1405
|
-
* a path that bypasses `upsertEdge` entirely) by simply skipping them, never throwing. Operates
|
|
1406
|
-
* on the shared parsed-graph shape (`graph.relations`/`graph.individuals`), not memory-specific —
|
|
1407
|
-
* same reasoning `edgesOfKind`/`moduleIdOf` already document. */
|
|
1107
|
+
/** A node's "last touched" moment: its own updatedAt/createdAt attribute, or
|
|
1108
|
+
* the max `createdAt` over every edge touching it, whichever is newer. ""
|
|
1109
|
+
* when nothing carries a timestamp. Skips edges with no `createdAt` rather than throwing. */
|
|
1408
1110
|
export function derivedUpdatedAt(graph, ind, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1409
1111
|
if (!ind) return "";
|
|
1410
1112
|
const attrs = ind.attributes || [];
|
|
@@ -1421,17 +1123,10 @@ export function derivedUpdatedAt(graph, ind, { createdAtProp = CREATED_AT_PROP,
|
|
|
1421
1123
|
return best;
|
|
1422
1124
|
}
|
|
1423
1125
|
|
|
1424
|
-
/** Turn a `spiralExpand` walk
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
* directive 2026-07-11) so both paths render byte-identically from the same
|
|
1429
|
-
* logic, never two hand-maintained copies. `nodes` enrich each walked id with
|
|
1430
|
-
* its real label/class/timestamps (`derivedUpdatedAt`, above); `edges` are
|
|
1431
|
-
* every relation-group edge connecting two walked nodes (not just the kinds
|
|
1432
|
-
* the walk itself traversed through — an incidental edge between two reached
|
|
1433
|
-
* nodes still renders), de-duped on (subject, object, predicate) across
|
|
1434
|
-
* relation groups. */
|
|
1126
|
+
/** Turn a `spiralExpand` walk into the `{nodes, edges}` shape `tmct viz`
|
|
1127
|
+
* renders, shared between the CLI and the browser bundle's client-side
|
|
1128
|
+
* re-walk. `edges` includes any relation-group edge connecting two walked
|
|
1129
|
+
* nodes, not just kinds the walk itself traversed, de-duped on (subject, object, predicate). */
|
|
1435
1130
|
export function buildVizNodesAndEdges(graph, walked, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1436
1131
|
const nodeIds = new Set(walked.map((w) => w.id));
|
|
1437
1132
|
const nodes = walked.map(({ id, hop }) => {
|
|
@@ -1457,46 +1152,17 @@ export function buildVizNodesAndEdges(graph, walked, { createdAtProp = CREATED_A
|
|
|
1457
1152
|
return { nodes, edges };
|
|
1458
1153
|
}
|
|
1459
1154
|
|
|
1460
|
-
// PLAN_VIZ_MEMORY.md Bug 2 fix: the FACT_CLASS string, mirrored here rather than
|
|
1461
|
-
// imported (memory/core.mjs's own FACT_CLASS export is a plain "Fact" literal —
|
|
1462
|
-
// importing one more binding across this already-imported module isn't worth
|
|
1463
|
-
// it for a single string every reader of this file can eyeball is exactly what
|
|
1464
|
-
// memory/core.mjs's own appendFact writes).
|
|
1465
1155
|
const MEMORY_FACT_CLASS = "Fact";
|
|
1466
1156
|
const MEMORY_TERM_CLASS = "Term";
|
|
1467
1157
|
|
|
1468
|
-
/** Derive a
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1474
|
-
*
|
|
1475
|
-
*
|
|
1476
|
-
* every edge there connects two INDIVIDUAL ids (Utterance/Session/Fact/
|
|
1477
|
-
* Source), never a concept term. This function materializes the missing
|
|
1478
|
-
* structure as a NEW, derived graph:
|
|
1479
|
-
* - one synthetic `Term` individual per distinct normalized subject/object
|
|
1480
|
-
* string (id `term:<t>`, label `t`);
|
|
1481
|
-
* - one synthetic relation group per DISTINCT fact predicate actually
|
|
1482
|
-
* present in the data (Term -> Term, `subject`/`object` = the two terms'
|
|
1483
|
-
* ids) — no hardcoded predicate vocabulary: a freshly taught "mgx:<verb>"
|
|
1484
|
-
* predicate (generalVerbTeach) becomes walkable automatically, the same
|
|
1485
|
-
* turn it's asserted;
|
|
1486
|
-
* - two FIXED structural link groups, `factSubjectTerm`/`factObjectTerm`
|
|
1487
|
-
* (Fact -> its own subject/object Term) — without these a walk seeded on
|
|
1488
|
-
* a Fact (the default `mostRecentIndividual` seed right after a teach
|
|
1489
|
-
* turn) could never reach the term graph in the first place; a `--term`
|
|
1490
|
-
* seed reaches the SAME facts via the same links, in reverse.
|
|
1491
|
-
* Returns `{ graph: <augmented graph>, factRelationKinds: [<predicate>, …] }`
|
|
1492
|
-
* — `factRelationKinds` is exactly the dynamic `kinds` list a caller passes
|
|
1493
|
-
* to `spiralExpand` for the "concept relation" walk (see MEMORY_SPIRAL_EXPAND_KINDS
|
|
1494
|
-
* for the sibling "provenance/meta" kinds list). A graph with no Fact
|
|
1495
|
-
* individuals (or a code graph passed in by mistake) is a safe no-op:
|
|
1496
|
-
* `{ graph, factRelationKinds: [] }`, the SAME graph object, unchanged.
|
|
1497
|
-
* Pure; deterministic (Map iteration order = insertion order = first-seen
|
|
1498
|
-
* order over `graph.individuals`, so re-running on the same input is
|
|
1499
|
-
* byte-identical). */
|
|
1158
|
+
/** Derive a term-relation view of a memory graph's reified Facts (never
|
|
1159
|
+
* mutates `graph`, viz-only). A Fact stores subject/predicate/object as
|
|
1160
|
+
* plain string attributes, so the concept structure is invisible to a walk
|
|
1161
|
+
* over `graph.relations` as-is; this synthesizes one Term individual per
|
|
1162
|
+
* distinct subject/object string, one relation group per distinct fact
|
|
1163
|
+
* predicate, and two fixed Fact->Term link groups so a walk seeded on a Fact
|
|
1164
|
+
* can reach the term graph at all. Returns `{ graph: <augmented graph>,
|
|
1165
|
+
* factRelationKinds: [<predicate>, …] }`; a graph with no Facts is a no-op. */
|
|
1500
1166
|
export function deriveFactTermGraph(graph) {
|
|
1501
1167
|
const termById = new Map(); // term:<t> -> individual
|
|
1502
1168
|
const groupByPredicate = new Map(); // predicate -> relation group
|
|
@@ -1520,10 +1186,6 @@ export function deriveFactTermGraph(graph) {
|
|
|
1520
1186
|
const objectTermId = ensureTerm(o);
|
|
1521
1187
|
let group = groupByPredicate.get(p);
|
|
1522
1188
|
if (!group) {
|
|
1523
|
-
// `factrel:` namespace: relationKind's own dedicated branch self-classifies
|
|
1524
|
-
// any group with this prefix to its raw predicate, verbatim — see that
|
|
1525
|
-
// function's comment for why (an open-ended, dynamically-discovered kind
|
|
1526
|
-
// set with no PROP_KIND row to add per predicate).
|
|
1527
1189
|
group = { predicate: p, prop: `factrel:${p}`, count: 0, edges: [] };
|
|
1528
1190
|
groupByPredicate.set(p, group);
|
|
1529
1191
|
}
|
|
@@ -1551,28 +1213,16 @@ export function deriveFactTermGraph(graph) {
|
|
|
1551
1213
|
};
|
|
1552
1214
|
}
|
|
1553
1215
|
|
|
1554
|
-
/** The two
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
* the "meta" (provenance) one: a user who toggles to meta-only still gets
|
|
1558
|
-
* today's exact byte-identical provenance-only view (see viz.mjs's edge-kind
|
|
1559
|
-
* toggle). */
|
|
1216
|
+
/** The two fixed structural link kinds `deriveFactTermGraph` always emits.
|
|
1217
|
+
* Bundled into the "relation" (concept) walk, not "meta" (provenance), so a
|
|
1218
|
+
* user toggling to meta-only still gets the provenance-only view. */
|
|
1560
1219
|
export const MEMORY_FACT_LINK_KINDS = ["factSubjectTerm", "factObjectTerm"];
|
|
1561
1220
|
|
|
1562
|
-
/**
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
*
|
|
1567
|
-
* same turn). `factRelationKinds` is the dynamic per-predicate list
|
|
1568
|
-
* `deriveFactTermGraph` discovered in THIS graph — there is no fixed
|
|
1569
|
-
* vocabulary, a freshly taught predicate is walkable the same turn it's
|
|
1570
|
-
* asserted. Lives here (not viz.mjs) and is re-exported through
|
|
1571
|
-
* ask-browser-entry.mjs specifically so BOTH the CLI's own generation-time
|
|
1572
|
-
* walk (viz.mjs's computeVizGraph) AND the browser bundle's client-side
|
|
1573
|
-
* re-walk (a recentre or an edge-kind-toggle change) combine kinds via the
|
|
1574
|
-
* SAME function — viz.mjs itself can't be bundled for the browser (it does
|
|
1575
|
-
* real fs I/O), so this had to live in the shared, browser-safe module. */
|
|
1221
|
+
/** The combined kinds list a memory-graph walk uses for a given edge-kind
|
|
1222
|
+
* mode: "meta" (provenance-only), "relation" (concept view), or "both"
|
|
1223
|
+
* (default). Lives here, not viz.mjs, so the CLI and the browser bundle's
|
|
1224
|
+
* client-side re-walk share the same function (viz.mjs does real fs I/O and
|
|
1225
|
+
* can't be bundled for the browser). */
|
|
1576
1226
|
export function edgeKindsFor(mode, factRelationKinds) {
|
|
1577
1227
|
const relationKinds = [...factRelationKinds, ...MEMORY_FACT_LINK_KINDS];
|
|
1578
1228
|
if (mode === "meta") return [...MEMORY_SPIRAL_EXPAND_KINDS];
|
|
@@ -1580,7 +1230,7 @@ export function edgeKindsFor(mode, factRelationKinds) {
|
|
|
1580
1230
|
return [...MEMORY_SPIRAL_EXPAND_KINDS, ...relationKinds]; // "both" (default)
|
|
1581
1231
|
}
|
|
1582
1232
|
|
|
1583
|
-
const LEGEND_MAX_BUCKETS = 20; //
|
|
1233
|
+
const LEGEND_MAX_BUCKETS = 20; // too many chips to be usable
|
|
1584
1234
|
const LEGEND_MIN_BUCKETS = 2; // nothing to filter with only one bucket
|
|
1585
1235
|
const LEGEND_COLLAPSE_TOP_N = 15; // "top 15 by count, rest grouped as Other" — stays under the max
|
|
1586
1236
|
|
|
@@ -1603,8 +1253,7 @@ function normalizedEntropy(buckets) {
|
|
|
1603
1253
|
|
|
1604
1254
|
/** Collapse a raw {value,count} bucket list down to at most LEGEND_MAX_BUCKETS
|
|
1605
1255
|
* entries: keep the top LEGEND_COLLAPSE_TOP_N by count, fold the rest into a
|
|
1606
|
-
* single "Other" bucket
|
|
1607
|
-
* grouped as Other" escape hatch, applied generically (not predicate-only) so
|
|
1256
|
+
* single "Other" bucket, applied generically (not predicate-only) so
|
|
1608
1257
|
* any dimension that happens to be high-cardinality degrades the same way.
|
|
1609
1258
|
* A no-op (returns `buckets` unchanged, same array) when already <= the cap.
|
|
1610
1259
|
* Exported (not just used internally by pickLegendDimension) so the browser
|
|
@@ -1652,15 +1301,9 @@ function provenanceBucketLabel(rawTag) {
|
|
|
1652
1301
|
return src.kind;
|
|
1653
1302
|
}
|
|
1654
1303
|
|
|
1655
|
-
/** A single walked
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
1658
|
-
* client-side dimension-switcher (a user flipping from "split by predicate"
|
|
1659
|
-
* to "split by trust source" without regenerating the page,
|
|
1660
|
-
* PLAN_VIZ_MEMORY.md's Controls section) filter/color by the SAME derivation,
|
|
1661
|
-
* never a second hand-rolled copy. `"class"` reads every node; `"predicate"`/
|
|
1662
|
-
* `"provenance"` only ever return non-null for a Fact-class node (any other
|
|
1663
|
-
* class simply has no predicate/provenance of its own to bucket on). */
|
|
1304
|
+
/** A single walked node's bucket value under one legend dimension. `"class"`
|
|
1305
|
+
* reads every node; `"predicate"`/`"provenance"` only return non-null for a
|
|
1306
|
+
* Fact-class node. */
|
|
1664
1307
|
export function legendValueFor(graph, node, dimension) {
|
|
1665
1308
|
if (dimension === "class") return node?.class || "(none)";
|
|
1666
1309
|
if (!node || node.class !== MEMORY_FACT_CLASS) return null;
|
|
@@ -1670,31 +1313,12 @@ export function legendValueFor(graph, node, dimension) {
|
|
|
1670
1313
|
return null;
|
|
1671
1314
|
}
|
|
1672
1315
|
|
|
1673
|
-
/** Auto-pick the filter/legend dimension
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1676
|
-
*
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
* filters almost nothing. This scores three candidate dimensions by
|
|
1680
|
-
* normalized Shannon entropy over their bucket-size distribution (rewards an
|
|
1681
|
-
* even-ish split, penalizes one dominant bucket) and picks the best-scoring
|
|
1682
|
-
* QUALIFYING one (`LEGEND_MIN_BUCKETS <= k <= LEGEND_MAX_BUCKETS`, after a
|
|
1683
|
-
* top-15+Other collapse for anything over the cap) as the PRIMARY legend:
|
|
1684
|
-
* 1. `class` — every walked node's own `.class`.
|
|
1685
|
-
* 2. `predicate` — every walked Fact node's `rdf:predicate` attribute (a
|
|
1686
|
-
* relation-shaped split: "show me only IsA facts").
|
|
1687
|
-
* 3. `provenance` — every walked Fact node's provenance prefix, collapsed
|
|
1688
|
-
* (see provenanceBucketLabel) — a TRUST-shaped split.
|
|
1689
|
-
* Pure, one pass over the already-walked `nodes` (no new graph traversal) —
|
|
1690
|
-
* computed ONCE at generation time and embedded into the page's JSON, never
|
|
1691
|
-
* recomputed client-side. `graph` supplies the per-Fact attribute lookups
|
|
1692
|
-
* `nodes` itself doesn't carry (predicate/provenance are Fact ATTRIBUTES,
|
|
1693
|
-
* not part of the {id,hop,label,class,createdAt,updatedAt} viz node shape).
|
|
1694
|
-
* Returns `{ primary, dimensions: { class, predicate, provenance } }`, each
|
|
1695
|
-
* entry `{ score, qualifies, buckets: [{value, count}] }`. When nothing
|
|
1696
|
-
* qualifies (e.g. a tiny 1-2-node walk), `primary` falls back to `"class"` —
|
|
1697
|
-
* today's behavior — so the legend is never simply empty. */
|
|
1316
|
+
/** Auto-pick the filter/legend dimension: since Fact dominates class-based
|
|
1317
|
+
* legends once real memory-graph data is seeded, this scores class/predicate/
|
|
1318
|
+
* provenance by normalized Shannon entropy over their bucket distribution
|
|
1319
|
+
* and picks the best-scoring qualifying one (LEGEND_MIN_BUCKETS..MAX_BUCKETS).
|
|
1320
|
+
* Falls back to "class" when nothing qualifies. Returns `{ primary,
|
|
1321
|
+
* dimensions: { class, predicate, provenance } }`. */
|
|
1698
1322
|
export function pickLegendDimension(graph, nodes) {
|
|
1699
1323
|
const classBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "class")));
|
|
1700
1324
|
const predicateBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "predicate")));
|
|
@@ -1940,11 +1564,7 @@ export function renderHistory(graph, ind) {
|
|
|
1940
1564
|
}
|
|
1941
1565
|
|
|
1942
1566
|
/** Modules that call into the target's module (one hop over `calls`). */
|
|
1943
|
-
//
|
|
1944
|
-
// edge, not the module-coarse `calls`. When the resolved target IS one of these, callers/
|
|
1945
|
-
// callees must read the SYMBOL node's own edges — mapping it to its enclosing module (the
|
|
1946
|
-
// old behaviour) both mislabels the answer with `mod:<path>` and scans the wrong edge set,
|
|
1947
|
-
// so "Widget.render --callsSymbol--> fnAlpha" was reported as "no recorded callers".
|
|
1567
|
+
// classes whose call graph lives on the fn/method-precise callsSymbol edge, not module-coarse calls
|
|
1948
1568
|
const CALL_SYMBOL_CLASSES = new Set(["Function", "Method"]);
|
|
1949
1569
|
|
|
1950
1570
|
export function renderCallers(graph, ind) {
|
|
@@ -2061,11 +1681,10 @@ export function renderClassHistory(graph, ind) {
|
|
|
2061
1681
|
return renderSymbolHistory(graph, ind);
|
|
2062
1682
|
}
|
|
2063
1683
|
|
|
2064
|
-
// ---- author identity
|
|
2065
|
-
//
|
|
2066
|
-
//
|
|
2067
|
-
//
|
|
2068
|
-
// unknown name — the chat lane falls through to the ordinary honest miss. ----
|
|
1684
|
+
// ---- author identity: the Commit "author" attribute answered as a person
|
|
1685
|
+
// ("who is <Name>", "what did <Name> touch"). Author is an attribute, never
|
|
1686
|
+
// an individual, so these renderers read it off every Commit and aggregate.
|
|
1687
|
+
// All return null on an unknown name; the chat lane falls through to an honest miss. ----
|
|
2069
1688
|
|
|
2070
1689
|
const AUTHOR_TOUCH_CAP = 15;
|
|
2071
1690
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
@@ -2167,13 +1786,9 @@ export function renderCommitAuthor(graph, sha) {
|
|
|
2167
1786
|
|
|
2168
1787
|
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute" };
|
|
2169
1788
|
|
|
2170
|
-
/** The structured scorer behind searchSymbols
|
|
2171
|
-
*
|
|
2172
|
-
*
|
|
2173
|
-
* score desc, tie-broken by SHORTER label first (matches searchSymbols's original inline
|
|
2174
|
-
* sort exactly). An unrecognised `kind` yields an empty ranked list — callers that need to
|
|
2175
|
-
* distinguish "unknown kind" from "kind valid, nothing matched" check SYMBOL_CLASSES
|
|
2176
|
-
* themselves (see searchSymbols below). Pure; deterministic.
|
|
1789
|
+
/** The structured scorer behind searchSymbols: filters to `kind`'s class,
|
|
1790
|
+
* scores by token substring hits, sorts score desc then shorter label first.
|
|
1791
|
+
* An unrecognised `kind` yields an empty ranked list.
|
|
2177
1792
|
* @returns {Array<{ind: object, score: number}>} */
|
|
2178
1793
|
export function scoreSymbolsRanked(graph, tokens, { kind, decFilter = "", nameRe = null } = {}) {
|
|
2179
1794
|
const targetClass = SYMBOL_CLASSES[kind];
|
|
@@ -2205,24 +1820,22 @@ function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, n
|
|
|
2205
1820
|
return lines.join("\n");
|
|
2206
1821
|
}
|
|
2207
1822
|
|
|
2208
|
-
// ---- tmct_context: a one-shot "edit bundle" plan (pure; the server adds the
|
|
2209
|
-
//
|
|
2210
|
-
//
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
const
|
|
2214
|
-
const
|
|
2215
|
-
const
|
|
2216
|
-
const
|
|
2217
|
-
|
|
2218
|
-
//
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
const
|
|
2222
|
-
const
|
|
2223
|
-
const
|
|
2224
|
-
const INLINE_CALLEE_CAP = 3; // LARGE: inline at most this many depth-1 in-repo callee bodies …
|
|
2225
|
-
const INLINE_CALLEE_LOC = 120; // … up to this many total lines.
|
|
1823
|
+
// ---- tmct_context: a one-shot "edit bundle" plan (pure; the server adds the
|
|
1824
|
+
// file reads). Returns everything needed to add a sibling to a module in one
|
|
1825
|
+
// call, so the agent need not search->describe->snippet->read x N. ----
|
|
1826
|
+
|
|
1827
|
+
const CONTEXT_SIBLING_CAP = 8; // the bundle is re-billed every turn — keep a few most-relevant siblings, not all
|
|
1828
|
+
const CLASS_MEMBER_CAP = 16; // class-internal members shown when the anchor is a class/method
|
|
1829
|
+
const COCHANGE_MID_CAP = 4;
|
|
1830
|
+
const CONTEXT_TESTS_CAP = 6;
|
|
1831
|
+
const INSERTION_REGION_CAP = 40; // contiguous tail lines shown as the "write your new sibling here" region
|
|
1832
|
+
// task-size thresholds, widened so a common "add a small sibling util" task
|
|
1833
|
+
// lands at the lean TINY default and only genuinely bigger edits top up
|
|
1834
|
+
const TINY_MAX_LOC = 12;
|
|
1835
|
+
const TINY_MAX_ARITY = 2;
|
|
1836
|
+
const LARGE_CLASS_MEMBERS = 8;
|
|
1837
|
+
const INLINE_CALLEE_CAP = 3;
|
|
1838
|
+
const INLINE_CALLEE_LOC = 120;
|
|
2226
1839
|
|
|
2227
1840
|
const splitDecs = (s) => String(s || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
2228
1841
|
const tokenize = (s) =>
|
|
@@ -2276,10 +1889,9 @@ function structuralScore(s, target) {
|
|
|
2276
1889
|
return score + Math.min(shared, 4) * 2;
|
|
2277
1890
|
}
|
|
2278
1891
|
|
|
2279
|
-
/**
|
|
2280
|
-
*
|
|
2281
|
-
*
|
|
2282
|
-
* mutates a transient `_score` only. */
|
|
1892
|
+
/** Rank siblings by relevance to the anchor: shared decorator (the module's
|
|
1893
|
+
* registration pattern) > name-affinity (shared tokens) > nearest source
|
|
1894
|
+
* position. Mutates a transient `_score` only. */
|
|
2283
1895
|
function rankSiblings(siblings, { decorators: anchorDecorators = "", label: anchorLabel = "", site: anchorSite = null } = {}, structuralTarget = null) {
|
|
2284
1896
|
const decCount = new Map();
|
|
2285
1897
|
for (const s of siblings) for (const d of splitDecs(s.decorators)) decCount.set(d, (decCount.get(d) || 0) + 1);
|
|
@@ -2293,9 +1905,7 @@ function rankSiblings(siblings, { decorators: anchorDecorators = "", label: anch
|
|
|
2293
1905
|
for (const s of siblings) {
|
|
2294
1906
|
const decMatch = splitDecs(s.decorators).some((d) => targetDecs.has(d)) ? 1 : 0;
|
|
2295
1907
|
const nameAff = tokenize(s.label).filter((t) => anchorTokens.has(t)).length;
|
|
2296
|
-
|
|
2297
|
-
// callees) sits BELOW name-affinity (max 16 < 50) — a tiebreaker within a name tier.
|
|
2298
|
-
const struct = structuralScore(s, structuralTarget);
|
|
1908
|
+
const struct = structuralScore(s, structuralTarget); // tiebreaker within a name tier
|
|
2299
1909
|
const pos = anchorStart != null && s.site ? 1 / (1 + Math.abs(s.site.start - anchorStart)) : 0;
|
|
2300
1910
|
s._score = decMatch * 1000 + nameAff * 50 + struct + pos;
|
|
2301
1911
|
}
|
|
@@ -2310,8 +1920,7 @@ export function contextPlan(graph, ind) {
|
|
|
2310
1920
|
const modId = moduleIdOf(graph, ind);
|
|
2311
1921
|
const moduleLabel = graph.byId.get(modId)?.label || String(modId || "").replace(/^mod:/, "");
|
|
2312
1922
|
const defEdges = edgesOfKind(graph, "defines").filter((e) => e.subject === modId);
|
|
2313
|
-
//
|
|
2314
|
-
// structural ranking and the sizeBundle cross-module-call check.
|
|
1923
|
+
// index fn→fn in-repo callees once, so siblings/anchor carry their callee set
|
|
2315
1924
|
const calleeMap = new Map();
|
|
2316
1925
|
for (const e of edgesOfKind(graph, "callsSymbol")) {
|
|
2317
1926
|
if (!calleeMap.has(e.subject)) calleeMap.set(e.subject, new Set());
|
|
@@ -2329,9 +1938,8 @@ export function contextPlan(graph, ind) {
|
|
|
2329
1938
|
globals.push({ label: mem.label, value: (mem.attributes || []).find((a) => a.key === "value")?.value || "", site });
|
|
2330
1939
|
if (site) insertion = Math.max(insertion, site.end);
|
|
2331
1940
|
} else if (cls === "Function" || cls === "Class") {
|
|
2332
|
-
//
|
|
2333
|
-
//
|
|
2334
|
-
// structural-similarity ranking.
|
|
1941
|
+
// raises + one-line doc so a validator-style task sees the error-contract
|
|
1942
|
+
// without reading the body; params/returns/callees feed structural-similarity ranking
|
|
2335
1943
|
siblings.push({
|
|
2336
1944
|
id: mem.id, label: mem.label, class: cls, site, decorators: decoratorOf(mem),
|
|
2337
1945
|
raises: attrVal(mem, "raises"), doc: attrVal(mem, "doc"),
|
|
@@ -2350,13 +1958,12 @@ export function contextPlan(graph, ind) {
|
|
|
2350
1958
|
}
|
|
2351
1959
|
: null;
|
|
2352
1960
|
const totalSiblings = siblings.length;
|
|
2353
|
-
//
|
|
2354
|
-
//
|
|
1961
|
+
// the structural target the exemplar should resemble: the anchor's own
|
|
1962
|
+
// shape, or the dominant pattern across siblings for a module anchor
|
|
2355
1963
|
const structuralTarget = anchor ? profileOf(anchor) : dominantProfile(siblings);
|
|
2356
1964
|
siblings = rankSiblings(siblings, anchor || { label: ind.label }, structuralTarget);
|
|
2357
|
-
//
|
|
2358
|
-
//
|
|
2359
|
-
// agent fall back to Read. With a function/class anchor its own body suffices.
|
|
1965
|
+
// when the anchor is a module, surface the closest sibling's full body as
|
|
1966
|
+
// the copy-this exemplar — signatures alone made the agent fall back to Read
|
|
2360
1967
|
const exemplar = !anchor ? siblings.find((s) => s.site && s.label !== ind.label) || null : null;
|
|
2361
1968
|
const tests = [...new Set(edgesOfKind(graph, "tests").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject))].slice(0, CONTEXT_TESTS_CAP);
|
|
2362
1969
|
const cochange = cochangeNeighbours(graph, modId).slice(0, COCHANGE_MID_CAP);
|
|
@@ -2388,16 +1995,15 @@ export function contextPlan(graph, ind) {
|
|
|
2388
1995
|
}).slice(0, CLASS_MEMBER_CAP);
|
|
2389
1996
|
classMembers = { className: owner?.label || String(classOwnerId).replace(/^fn:.*#/, ""), members, total: contains.filter((e) => e.subject === classOwnerId).length };
|
|
2390
1997
|
}
|
|
2391
|
-
//
|
|
2392
|
-
//
|
|
2393
|
-
// the server extends `end` to the real end-of-file (capped) using the lines it reads.
|
|
1998
|
+
// contiguous insertion region from the last top-level definition through
|
|
1999
|
+
// end-of-module; the server extends `end` to the real end-of-file
|
|
2394
2000
|
let lastTop = null;
|
|
2395
2001
|
for (const s of [...siblings, ...globals]) {
|
|
2396
2002
|
if (s.site && (!lastTop || s.site.start > lastTop.start)) lastTop = s.site;
|
|
2397
2003
|
}
|
|
2398
2004
|
const insertionRegion = lastTop ? { start: lastTop.start, end: lastTop.end } : null;
|
|
2399
|
-
//
|
|
2400
|
-
//
|
|
2005
|
+
// the focal symbol (anchor, else the module's exemplar) drives the call
|
|
2006
|
+
// hint and the LARGE-tier inlined-callee bodies
|
|
2401
2007
|
const focal = anchor || exemplar;
|
|
2402
2008
|
const focalInd = focal?.id ? graph.byId.get(focal.id) : null;
|
|
2403
2009
|
const callHintStr = focalInd ? callHint(graph, focalInd) : "";
|
|
@@ -2417,7 +2023,7 @@ export function contextPlan(graph, ind) {
|
|
|
2417
2023
|
};
|
|
2418
2024
|
}
|
|
2419
2025
|
|
|
2420
|
-
// ----
|
|
2026
|
+
// ---- task-size-adaptive bundle (TINY / MID / LARGE) ------------------------------
|
|
2421
2027
|
|
|
2422
2028
|
/** Which bundle sections a tier emits. TINY is genuinely minimal (header + one short
|
|
2423
2029
|
* exemplar body + registration + insertion region + __all__); MID is the full bundle;
|
|
@@ -2432,10 +2038,8 @@ export function bundleMask(tier) {
|
|
|
2432
2038
|
return all; // MID
|
|
2433
2039
|
}
|
|
2434
2040
|
|
|
2435
|
-
/**
|
|
2436
|
-
* cache-stable signal
|
|
2437
|
-
* __all__) but drop the expensive bodies (anchor/exemplar/inlined callees) and the variable
|
|
2438
|
-
* tails (tests/cochange/re-exports/class members). Pure. */
|
|
2041
|
+
/** A trimmed mask for secondary (related-but-not-primary) digest modules: keep
|
|
2042
|
+
* the cheap, cache-stable signal but drop expensive bodies and variable tails. */
|
|
2439
2043
|
export function trimBundleMask(mask) {
|
|
2440
2044
|
return {
|
|
2441
2045
|
...mask,
|
|
@@ -2445,11 +2049,10 @@ export function trimBundleMask(mask) {
|
|
|
2445
2049
|
};
|
|
2446
2050
|
}
|
|
2447
2051
|
|
|
2448
|
-
/** Classify a context plan by task size
|
|
2449
|
-
*
|
|
2450
|
-
*
|
|
2451
|
-
*
|
|
2452
|
-
* records whether auto-sizing escalated above TINY (surfaced in the digest header). Pure. */
|
|
2052
|
+
/** Classify a context plan by task size, {tier, mask, topup}. Lean by default:
|
|
2053
|
+
* start at TINY and escalate one tier only when the lean bundle would omit
|
|
2054
|
+
* something the edit demonstrably needs. `topup` records whether auto-sizing
|
|
2055
|
+
* escalated above TINY. */
|
|
2453
2056
|
export function sizeBundle(plan, graph, { untuned = false } = {}) {
|
|
2454
2057
|
const focal = plan.anchor || plan.exemplar;
|
|
2455
2058
|
let tier = "TINY";
|
|
@@ -2462,17 +2065,11 @@ export function sizeBundle(plan, graph, { untuned = false } = {}) {
|
|
|
2462
2065
|
// (c) a large/complex target (long body, many params, or it raises) → MID.
|
|
2463
2066
|
const loc = focal.site ? focal.site.end - focal.site.start + 1 : Infinity;
|
|
2464
2067
|
const arity = countParams(focal.params);
|
|
2465
|
-
//
|
|
2466
|
-
// an explicit symbol anchor (so a long-exemplar MODULE digest stayed TINY) regressed results,
|
|
2467
|
-
// because the trimmed sibling/test tail was load-bearing scaffolding. The `untuned` param is now
|
|
2468
|
-
// a no-op for sizing (kept so the tmct-b010 control arm's flag still resolves).
|
|
2068
|
+
// `untuned` is a no-op for sizing now (kept so a legacy control-arm flag still resolves)
|
|
2469
2069
|
if (loc > TINY_MAX_LOC || arity > TINY_MAX_ARITY || Boolean(focal.raises)) tier = "MID";
|
|
2470
|
-
//
|
|
2471
|
-
//
|
|
2472
|
-
//
|
|
2473
|
-
// module-EXEMPLAR (the digest/module-anchor case), a cross-module call does NOT force
|
|
2474
|
-
// LARGE — the exemplar body already shows the call, and MID's signatures suffice; this
|
|
2475
|
-
// keeps the common "register a filter" module bundle lean.
|
|
2070
|
+
// LARGE only for an explicit symbol focus: a cross-module call from the
|
|
2071
|
+
// anchor, or an anchor that's a method of a big class. A module-exemplar
|
|
2072
|
+
// focal never forces LARGE — MID's signatures suffice and keeps it lean.
|
|
2476
2073
|
let crossModule = false;
|
|
2477
2074
|
if (plan.anchor) {
|
|
2478
2075
|
for (const cid of focal.callees || []) {
|
|
@@ -2490,10 +2087,9 @@ export function sizeBundle(plan, graph, { untuned = false } = {}) {
|
|
|
2490
2087
|
return { tier, mask: bundleMask(tier), topup: tier !== "TINY" };
|
|
2491
2088
|
}
|
|
2492
2089
|
|
|
2493
|
-
/**
|
|
2494
|
-
* adjacency
|
|
2495
|
-
*
|
|
2496
|
-
* Pure — no fs. Falls back to the input order when the primary can't be mapped to a module. */
|
|
2090
|
+
/** Order secondary digest modules by relevance to the primary module: import
|
|
2091
|
+
* adjacency outranks change-coupling weight; ties keep input order. Falls
|
|
2092
|
+
* back to input order when the primary can't be mapped to a module. */
|
|
2497
2093
|
export function rankModulesByProximity(graph, primaryLabel, candidateLabels) {
|
|
2498
2094
|
const moduleIdFor = (label) => {
|
|
2499
2095
|
const { match } = resolveSymbol(graph, label);
|
|
@@ -2523,7 +2119,7 @@ export function rankModulesByProximity(graph, primaryLabel, candidateLabels) {
|
|
|
2523
2119
|
.map((s) => s.label);
|
|
2524
2120
|
}
|
|
2525
2121
|
|
|
2526
|
-
// ----
|
|
2122
|
+
// ---- tmct_context_more: only the sections a TINY/MID bundle omits ---------------
|
|
2527
2123
|
|
|
2528
2124
|
/** Render ONLY the bundle sections a lean bundle omits (sibling list / class members /
|
|
2529
2125
|
* re-exports / __all__ / tests / cochange) for a symbol's module. Pure (no fs). */
|
|
@@ -2560,14 +2156,10 @@ export function renderContextMore(plan) {
|
|
|
2560
2156
|
|
|
2561
2157
|
// ---- Repository Interface: graph-only context() bundle (no fs) -----------------
|
|
2562
2158
|
|
|
2563
|
-
/** Render a graph-only edit bundle
|
|
2564
|
-
* fs-dependent
|
|
2565
|
-
*
|
|
2566
|
-
*
|
|
2567
|
-
* Used by graph-service.mjs's context() service so a graph-only provider (no working
|
|
2568
|
-
* tree) can still return a real HIT instead of an NO_SOURCE miss — see PLAN item 2d /
|
|
2569
|
-
* INTERFACE_VERSION 1.1.0. A source-capable provider layers the body sections on top
|
|
2570
|
-
* (it has fs access this module deliberately does not). */
|
|
2159
|
+
/** Render a graph-only edit bundle: every contextPlan section except the
|
|
2160
|
+
* fs-dependent body text, gated by `mask`. Lets a graph-only provider (no
|
|
2161
|
+
* working tree) return a real hit instead of a NO_SOURCE miss; a
|
|
2162
|
+
* source-capable provider layers the body sections on top. */
|
|
2571
2163
|
export function renderGraphOnlyBundle(plan, mask) {
|
|
2572
2164
|
const out = [
|
|
2573
2165
|
`Edit context for ${plan.moduleLabel} (graph-only bundle — siblings/registration/tests are real graph truth; ` +
|
|
@@ -2612,7 +2204,7 @@ export function renderGraphOnlyBundle(plan, mask) {
|
|
|
2612
2204
|
return out.join("\n");
|
|
2613
2205
|
}
|
|
2614
2206
|
|
|
2615
|
-
// ----
|
|
2207
|
+
// ---- cold-tool catalog (written to <repo>/.tmct/TOOLS.md by the index step) -----
|
|
2616
2208
|
|
|
2617
2209
|
/** Markdown catalog of the COLD tools (everything except the hot catalog tools): each
|
|
2618
2210
|
* with a one-line purpose and the exact Bash invocation via the CLI `cli <tool>` route.
|