@polycode-projects/the-mechanical-code-talker 3.1.0 → 3.1.3
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 +2 -0
- package/corpus/seon/relations.jsonl +2 -0
- package/package.json +3 -1
- package/src/adapters/repository-interface.mjs +3 -0
- package/src/domain/ask-vocab.mjs +23 -0
- package/src/domain/ask.mjs +63 -13
- package/src/domain/code-explorer-hints.mjs +4 -0
- package/src/domain/codegraph.mjs +25 -17
- package/src/domain/codeplan/graph-delta.mjs +1 -0
- package/src/domain/concept.mjs +16 -0
- package/src/domain/interpret/normalize.mjs +16 -1
- package/src/domain/module-paths.mjs +6 -4
- package/src/domain/paraphrase-ing8.mjs +195 -0
- package/src/domain/real-word-collisions.json +1 -1
- package/src/domain/router/resolver.mjs +2 -0
- package/src/services/chat-session.mjs +5 -0
- package/src/services/chat.mjs +207 -32
- package/src/services/finish.mjs +6 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +113 -113
- package/src/tools/handlers/kit.mjs +5 -3
- package/src/tools/handlers/tmct-context.mjs +3 -2
- package/src/tools/handlers/tmct-export.mjs +9 -3
- package/src/tools/handlers/tmct-members.mjs +1 -1
- package/src/tools/handlers/tmct-search.mjs +1 -0
- package/src/tools/schema-docs.mjs +10 -0
- package/src/tools/server.mjs +9 -4
package/README.md
CHANGED
|
@@ -1135,6 +1135,8 @@ The `<where-marker>` slot takes any of *defined*, *declared*, *located*, *implem
|
|
|
1135
1135
|
- **touches** — *touched*, *touches*, *changed*, *change*, and more
|
|
1136
1136
|
- **cochange** — *changed with*, *co-changes with*, *co-change with*, *changes alongside*, and more
|
|
1137
1137
|
- **reexports** — *exports*, *export*, *re-exports*, *re-export*, and more
|
|
1138
|
+
- **serves** — *serves*, *serve*, *serving*, and more
|
|
1139
|
+
- **denotes** — *denotes*, *denote*, *denoting*, and more
|
|
1138
1140
|
|
|
1139
1141
|
Every question in that table runs against the example graph:
|
|
1140
1142
|
|
|
@@ -7,3 +7,5 @@
|
|
|
7
7
|
{"relation":"touches","definition":"A touch is a commit changing a file or a symbol in the codebase.","sense":"software"}
|
|
8
8
|
{"relation":"cochange","definition":"Change-coupling is two files that tend to be changed together in the same commits.","sense":"software"}
|
|
9
9
|
{"relation":"reexports","definition":"A re-export is a module passing another module's definition through as part of its own public API.","sense":"software"}
|
|
10
|
+
{"relation":"serves","definition":"A service edge is one part of the codebase providing or backing another, such as a handler serving a route.","sense":"software"}
|
|
11
|
+
{"relation":"denotes","definition":"A denotation is a vocabulary term naming a code entity, linking the word to the thing it refers to.","sense":"software"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
"./generateCompletion": "./src/services/completions.mjs",
|
|
47
47
|
"./createCompletionsGraphAdapter": "./src/services/completions.mjs",
|
|
48
48
|
"./ingest": "./src/services/extract-facts.mjs",
|
|
49
|
+
"./digest": "./src/adapters/corpus/digest-bank.mjs",
|
|
50
|
+
"./memory": "./src/adapters/memory/core.mjs",
|
|
49
51
|
"./repository-interface": "./src/adapters/repository-interface.mjs",
|
|
50
52
|
"./conformance": "./src/tools/conformance.mjs",
|
|
51
53
|
"./ask-browser": "./src/surfaces/web/graph-ask-browser-entry.mjs"
|
|
@@ -46,6 +46,7 @@ export const MISS_REASONS = Object.freeze({
|
|
|
46
46
|
export const EDGE_KINDS = Object.freeze([
|
|
47
47
|
"imports", "calls", "callsSymbol", "defines", "tests",
|
|
48
48
|
"touches", "touchesSymbol", "contains", "inherits", "cochange", "reexports",
|
|
49
|
+
"serves", "denotes",
|
|
49
50
|
]);
|
|
50
51
|
|
|
51
52
|
/** edge-kind → the `tmct:` object property it realizes (the OWL grounding). */
|
|
@@ -61,6 +62,8 @@ export const EDGE_KIND_TO_TMCT = Object.freeze({
|
|
|
61
62
|
inherits: "tmct:extends",
|
|
62
63
|
cochange: "tmct:dependsOn",
|
|
63
64
|
reexports: "tmct:exports",
|
|
65
|
+
serves: "tmct:serves",
|
|
66
|
+
denotes: "tmct:denotes",
|
|
64
67
|
});
|
|
65
68
|
|
|
66
69
|
/** The named services, grouped as in the plan's six-group inventory. Names are
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -164,6 +164,29 @@ export const RELATIONS = {
|
|
|
164
164
|
"exporting",
|
|
165
165
|
],
|
|
166
166
|
},
|
|
167
|
+
// serves/denotes classify edges a PROVIDER declares (mgx:serves / mgx:denotes).
|
|
168
|
+
// tmct's own indexer emits neither, so they are absent from a graph it built
|
|
169
|
+
// and answer honestly empty there; a provider graph that carries them gets the
|
|
170
|
+
// same one-hop traversal every other kind gets, instead of only /describe's
|
|
171
|
+
// kind-agnostic edge walk.
|
|
172
|
+
serves: {
|
|
173
|
+
bare: "serve",
|
|
174
|
+
comment: "subject provides or backs the object — a handler serving a route, a module serving a surface (mgx:serves).",
|
|
175
|
+
verbs: [
|
|
176
|
+
"serves", "serve",
|
|
177
|
+
// gerund (g-drop normalization)
|
|
178
|
+
"serving",
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
denotes: {
|
|
182
|
+
bare: "denote",
|
|
183
|
+
comment: "subject names the object — a glossary/lexicon term denoting a code entity (mgx:denotes).",
|
|
184
|
+
verbs: [
|
|
185
|
+
"denotes", "denote",
|
|
186
|
+
// gerund (g-drop normalization)
|
|
187
|
+
"denoting",
|
|
188
|
+
],
|
|
189
|
+
},
|
|
167
190
|
};
|
|
168
191
|
|
|
169
192
|
/** The closed set of reverse `inherits` verb phrasings a strategy checks to
|
package/src/domain/ask.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { parseKeywordSpot, findPhrase } from "./interpret/strategies/keywords.mj
|
|
|
35
35
|
import { runStrategiesSync } from "./interpret/pipeline.mjs";
|
|
36
36
|
import { mergeStrategyResults, alternateLines } from "./interpret/merge.mjs";
|
|
37
37
|
import { lookupByProseTokens, splitIdentifierWords } from "./prose.mjs";
|
|
38
|
+
import { articleFor } from "./digest/words.mjs";
|
|
38
39
|
import { pickPhrase } from "./answer-variants.mjs";
|
|
39
40
|
|
|
40
41
|
// Normalization stays importable from its original site (tests + chat surface).
|
|
@@ -137,7 +138,7 @@ function verbFor(kind) {
|
|
|
137
138
|
const PLURAL_SUBJECT_VERB = {
|
|
138
139
|
imports: "import", calls: "call", callsSymbol: "call", inherits: "inherit from",
|
|
139
140
|
contains: "contain", tests: "test", touches: "touch", cochange: "cochange",
|
|
140
|
-
reexports: "export", uses: "use",
|
|
141
|
+
reexports: "export", uses: "use", serves: "serve", denotes: "denote",
|
|
141
142
|
};
|
|
142
143
|
const pluralVerbFor = (kind) => PLURAL_SUBJECT_VERB[kind] || verbFor(kind);
|
|
143
144
|
|
|
@@ -2187,7 +2188,28 @@ function describeFindHit(ind) {
|
|
|
2187
2188
|
const label = ["Function", "Method"].includes(ind.class) ? `${ind.label}()` : ind.label;
|
|
2188
2189
|
if (ind.class === "Module") return label;
|
|
2189
2190
|
const mod = moduleLabelOf(ind);
|
|
2190
|
-
return mod
|
|
2191
|
+
return mod ? `${label} in ${mod}` : label;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
/** Does any branch of a compositional AST filter on test coverage? Keyed off
|
|
2195
|
+
* QUALIFIERS' own `via` field, so a new coverage adjective in the vocabulary
|
|
2196
|
+
* is picked up here without a second list to keep in step. */
|
|
2197
|
+
function filtersOnCoverage(node) {
|
|
2198
|
+
if (!node || typeof node !== "object") return false;
|
|
2199
|
+
if (Array.isArray(node)) return node.some(filtersOnCoverage);
|
|
2200
|
+
if (Array.isArray(node.filters)
|
|
2201
|
+
&& node.filters.some((f) => QUALIFIERS[String(f).toLowerCase()]?.via === "tested")) return true;
|
|
2202
|
+
return Object.values(node).some(filtersOnCoverage);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
/** An empty coverage-filtered set over symbols is a grain mismatch, not an
|
|
2206
|
+
* absent answer: `tests` edges are recorded module to module, so no
|
|
2207
|
+
* function-grain coverage exists to filter on. Say that instead of the
|
|
2208
|
+
* generic rephrase nudge, which would send the reader somewhere unrelated. */
|
|
2209
|
+
function coverageGrainNote(parsed, entityType) {
|
|
2210
|
+
if (!["Function", "Method"].includes(entityType)) return null;
|
|
2211
|
+
if (!filtersOnCoverage(parsed)) return null;
|
|
2212
|
+
return "This index records tests edges module to module, so it holds no function-grain coverage to filter on — ask whether the module a function lives in is tested instead.";
|
|
2191
2213
|
}
|
|
2192
2214
|
|
|
2193
2215
|
function renderComposite(parsed, result, graph) {
|
|
@@ -2210,7 +2232,7 @@ function renderComposite(parsed, result, graph) {
|
|
|
2210
2232
|
}
|
|
2211
2233
|
const hit = result.matches[0];
|
|
2212
2234
|
const modLabel = moduleLabelOf(hit);
|
|
2213
|
-
const definedIn = hit.class === "Module" ? "" : (modLabel
|
|
2235
|
+
const definedIn = hit.class === "Module" ? "" : (modLabel ? `, ${pickPhrase("defined-in", hit.id, "defined in")} ${modLabel}` : "");
|
|
2214
2236
|
return { content: `Yes — ${hit.label} is a ${kindSingular}${definedIn}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
2215
2237
|
}
|
|
2216
2238
|
if (!result.matches.length) {
|
|
@@ -2386,7 +2408,8 @@ function renderComposite(parsed, result, graph) {
|
|
|
2386
2408
|
}
|
|
2387
2409
|
// set-producing
|
|
2388
2410
|
if (!result.matches.length) {
|
|
2389
|
-
|
|
2411
|
+
const hint = coverageGrainNote(parsed, result.entityType) || touchesRephraseHint(graph);
|
|
2412
|
+
return { content: `nothing in the index matches that${result.entityType ? ` (${nounFor(result.entityType, 2)})` : ""}. ${hint}`, miss: true, ambiguous: false, matches: [] };
|
|
2390
2413
|
}
|
|
2391
2414
|
return { content: `${compositeList(result.matches)}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
2392
2415
|
}
|
|
@@ -2963,6 +2986,25 @@ function modifierIsWired(shape, kind, entityType) {
|
|
|
2963
2986
|
}
|
|
2964
2987
|
const TRANSITIVE_MAX_DEPTH = 8; // matches renderImpact's own default (codegraph.mjs)
|
|
2965
2988
|
|
|
2989
|
+
/** A graph's own vocabulary nodes carry their definition text under this
|
|
2990
|
+
* property. A code entity's docstring rides `seon:hasDoc` and shares the
|
|
2991
|
+
* plain `doc` key, so the PROP is what separates the two. */
|
|
2992
|
+
const SCHEMA_DOC_PROP = "mgx:schemaDoc";
|
|
2993
|
+
|
|
2994
|
+
/** The definition text an individual publishes about itself, or null. Reading
|
|
2995
|
+
* the meta lane off this attribute rather than a fixed class-name list lets a
|
|
2996
|
+
* graph declare its own documented individual class (a glossary term, say)
|
|
2997
|
+
* and have "what is X" answer for it like any other vocabulary node. */
|
|
2998
|
+
function schemaDefinitionOf(ind) {
|
|
2999
|
+
return (ind?.attributes || []).find((a) => a.prop === SCHEMA_DOC_PROP)?.value || null;
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
function schemaKindWordFor(cls) {
|
|
3003
|
+
if (cls === "SchemaClass") return "a class in the graph's schema";
|
|
3004
|
+
if (cls === "SchemaPredicate") return "a predicate (relation) in the graph's schema";
|
|
3005
|
+
return `${articleFor(cls)} ${cls} in this graph's vocabulary`;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2966
3008
|
/** Compile a parsed query into a graph lookup. Pure given (graph, parsed, opts).
|
|
2967
3009
|
* `opts.contextId` resolves a context pronoun ("this"/"it"/…) when the parse
|
|
2968
3010
|
* needed one. Returns {matches, objMatch, candidates, traversal, ambiguous,
|
|
@@ -2992,7 +3034,7 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2992
3034
|
const term = String(parsed.object || "").trim();
|
|
2993
3035
|
const termLc = term.toLowerCase();
|
|
2994
3036
|
const match = (graph.individuals || []).find((i) => {
|
|
2995
|
-
if (i
|
|
3037
|
+
if (!schemaDefinitionOf(i)) return false;
|
|
2996
3038
|
if (String(i.label).toLowerCase() === termLc) return true;
|
|
2997
3039
|
const token = (i.attributes || []).find((a) => a.key === "token")?.value;
|
|
2998
3040
|
return token && String(token).toLowerCase() === termLc;
|
|
@@ -3435,12 +3477,15 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
3435
3477
|
// ---- templated renderer: string interpolation + grouping/pluralization/
|
|
3436
3478
|
// overflow rules, never generation. ----
|
|
3437
3479
|
|
|
3480
|
+
/** The module an individual lives in, or null when the index places it in
|
|
3481
|
+
* none — a graph can carry individuals that are not code (a glossary term,
|
|
3482
|
+
* a schema node), and naming a module for those would be a fabrication. */
|
|
3438
3483
|
function moduleLabelOf(ind) {
|
|
3439
3484
|
if (ind.class === "Module") return ind.label;
|
|
3440
3485
|
const site = (ind.attributes || []).find((a) => a.key === "site")?.value;
|
|
3441
3486
|
if (site) return String(site).split(":")[0];
|
|
3442
3487
|
const m = String(ind.id || "").match(/^fn:(.+)#/);
|
|
3443
|
-
return m ? m[1] :
|
|
3488
|
+
return m ? m[1] : null;
|
|
3444
3489
|
}
|
|
3445
3490
|
|
|
3446
3491
|
function symbolLabelOf(ind) {
|
|
@@ -3653,9 +3698,8 @@ function renderCore(parsed, result, graph) {
|
|
|
3653
3698
|
if (result.metaCodeClass) {
|
|
3654
3699
|
return { content: result.metaFallbackText, miss: false, ambiguous: false, matches: result.matches };
|
|
3655
3700
|
}
|
|
3656
|
-
const doc = (result.objMatch
|
|
3657
|
-
|
|
3658
|
-
return { content: `${result.objMatch.label} is ${kindWord}: ${doc}`, miss: false, ambiguous: false, matches: result.matches };
|
|
3701
|
+
const doc = schemaDefinitionOf(result.objMatch) || "";
|
|
3702
|
+
return { content: `${result.objMatch.label} is ${schemaKindWordFor(result.objMatch.class)}: ${doc}`, miss: false, ambiguous: false, matches: result.matches };
|
|
3659
3703
|
}
|
|
3660
3704
|
// mentions: the prose surface — checked before the generic objMatch-null miss
|
|
3661
3705
|
// below, because a mentions result deliberately carries no resolved object
|
|
@@ -3810,8 +3854,15 @@ function renderCore(parsed, result, graph) {
|
|
|
3810
3854
|
const lines = m[3] && m[3] !== m[2] ? `lines ${m[2]}-${m[3]}` : `line ${m[2]}`;
|
|
3811
3855
|
return { content: `${symbolLabelOf(ind)} is defined in ${m[1]} at ${lines}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
3812
3856
|
}
|
|
3857
|
+
const mod = moduleLabelOf(ind);
|
|
3858
|
+
if (!mod) {
|
|
3859
|
+
return {
|
|
3860
|
+
content: `${ind.label} has no recorded code location in this index — it carries no source site, and nothing places it in a module.`,
|
|
3861
|
+
miss: true, ambiguous: false,
|
|
3862
|
+
};
|
|
3863
|
+
}
|
|
3813
3864
|
return {
|
|
3814
|
-
content: `${symbolLabelOf(ind)} is defined in ${
|
|
3865
|
+
content: `${symbolLabelOf(ind)} is defined in ${mod} (no line span recorded in this index).`,
|
|
3815
3866
|
miss: false, ambiguous: false, matches: result.matches,
|
|
3816
3867
|
};
|
|
3817
3868
|
}
|
|
@@ -3974,7 +4025,7 @@ function renderCore(parsed, result, graph) {
|
|
|
3974
4025
|
// "there is … in {module}" (module trails, not leads).
|
|
3975
4026
|
const byModule = new Map();
|
|
3976
4027
|
for (const m of result.matches.slice(0, OVERFLOW_CAP)) {
|
|
3977
|
-
const mod = moduleLabelOf(m);
|
|
4028
|
+
const mod = moduleLabelOf(m) || "no recorded module";
|
|
3978
4029
|
if (!byModule.has(mod)) byModule.set(mod, []);
|
|
3979
4030
|
byModule.get(mod).push(symbolLabelOf(m));
|
|
3980
4031
|
}
|
|
@@ -4066,8 +4117,7 @@ function fuzzyCascadeWord(w) {
|
|
|
4066
4117
|
* question. Exact/substring/prose matches are untouched. */
|
|
4067
4118
|
function schemaTypoTrap(resolution, term) {
|
|
4068
4119
|
if (!resolution?.match || resolution.matchedVia !== "fuzzy" || resolution.ambiguous) return false;
|
|
4069
|
-
|
|
4070
|
-
if (cls !== "SchemaClass" && cls !== "SchemaPredicate") return false;
|
|
4120
|
+
if (!schemaDefinitionOf(resolution.match)) return false;
|
|
4071
4121
|
const lc = String(term || "").trim().toLowerCase();
|
|
4072
4122
|
const kindNoun = fuzzyCascadeWord(lc);
|
|
4073
4123
|
return !!kindNoun && kindNoun !== lc && !!ENTITY_TO_TYPE[kindNoun];
|
|
@@ -42,6 +42,8 @@ const FORWARD_TEMPLATE = Object.freeze({
|
|
|
42
42
|
contains: (f) => `what is in ${f}`,
|
|
43
43
|
defines: (f) => `what does ${f} define`,
|
|
44
44
|
inherits: (f) => `what does ${f} inherit from`,
|
|
45
|
+
serves: (f) => `what does ${f} serve`,
|
|
46
|
+
denotes: (f) => `what does ${f} denote`,
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
// Reverse reading (focus is the object): "what <kind> <focus>".
|
|
@@ -51,6 +53,8 @@ const REVERSE_TEMPLATE = Object.freeze({
|
|
|
51
53
|
tests: (f) => `what tests ${f}`,
|
|
52
54
|
inherits: (f) => `what inherits from ${f}`,
|
|
53
55
|
contains: (f) => `what contains ${f}`,
|
|
56
|
+
serves: (f) => `what serves ${f}`,
|
|
57
|
+
denotes: (f) => `what denotes ${f}`,
|
|
54
58
|
});
|
|
55
59
|
|
|
56
60
|
/** Index a payload into the counts and adjacency the hints read: class →
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -75,6 +75,10 @@ const PROP_KIND = {
|
|
|
75
75
|
"seon:hassupertype": "inherits",
|
|
76
76
|
"mgx:changecoupledwith": "cochange",
|
|
77
77
|
"mgx:reexports": "reexports",
|
|
78
|
+
// provider-declared edges: tmct's own indexer emits neither, so these classify
|
|
79
|
+
// only in a graph supplied through the provider seam
|
|
80
|
+
"mgx:serves": "serves",
|
|
81
|
+
"mgx:denotes": "denotes",
|
|
78
82
|
// symbol-level edges stay separate kinds so the module-coarse impact closure is unchanged
|
|
79
83
|
"mgx:touchessymbol": "touchesSymbol",
|
|
80
84
|
"mgx:callssymbol": "callsSymbol",
|
|
@@ -152,6 +156,11 @@ export function resolveSymbol(graph, symbol) {
|
|
|
152
156
|
const s = normPath(symbol);
|
|
153
157
|
if (!s) return { match: null, candidates: [] };
|
|
154
158
|
const sBase = basename(s);
|
|
159
|
+
// A bare filename (no "/") legitimately fuzzy-matches on basename alone. A
|
|
160
|
+
// multi-segment input reads as a repo-relative path the caller believes exists;
|
|
161
|
+
// matching it against an unrelated file that merely shares a basename would
|
|
162
|
+
// fabricate a hit, so those only match via an exact path suffix, never basename alone.
|
|
163
|
+
const isBareFilename = !s.includes("/");
|
|
155
164
|
const scored = [];
|
|
156
165
|
for (const ind of graph.individuals) {
|
|
157
166
|
const label = normPath(ind.label);
|
|
@@ -160,8 +169,7 @@ export function resolveSymbol(graph, symbol) {
|
|
|
160
169
|
if (label === s || id === s) score = 100;
|
|
161
170
|
else if (
|
|
162
171
|
label.endsWith(`/${s}`) ||
|
|
163
|
-
basename(label) === sBase ||
|
|
164
|
-
basename(label).replace(/\.[a-z]+$/, "") === sBase
|
|
172
|
+
(isBareFilename && (basename(label) === sBase || basename(label).replace(/\.[a-z]+$/, "") === sBase))
|
|
165
173
|
)
|
|
166
174
|
score = 80;
|
|
167
175
|
else if (label.includes(s)) score = Math.max(10, 50 - (label.length - s.length));
|
|
@@ -1090,7 +1098,7 @@ export function compileNameFilter(name, { now = Date.now } = {}) {
|
|
|
1090
1098
|
};
|
|
1091
1099
|
}
|
|
1092
1100
|
|
|
1093
|
-
export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", decorator = "", name = "" } = {}) {
|
|
1101
|
+
export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", decorator = "", name = "", toolNamePrefix = "tmct_" } = {}) {
|
|
1094
1102
|
const tokens = String(query || "").toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean);
|
|
1095
1103
|
const wantKind = String(kind || "").trim().toLowerCase();
|
|
1096
1104
|
const decFilter = String(decorator || "").trim().toLowerCase();
|
|
@@ -1104,7 +1112,7 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1104
1112
|
// default (no kind) keeps the module "where does this live" search unchanged.
|
|
1105
1113
|
if (wantKind && wantKind !== "module") {
|
|
1106
1114
|
try {
|
|
1107
|
-
return searchSymbols(graph, tokens, { limit, kind: wantKind, decFilter, nameRe });
|
|
1115
|
+
return searchSymbols(graph, tokens, { limit, kind: wantKind, decFilter, nameRe, toolNamePrefix });
|
|
1108
1116
|
} catch (e) {
|
|
1109
1117
|
if (e instanceof NameFilterBudgetExceeded) return e.message;
|
|
1110
1118
|
throw e;
|
|
@@ -1113,7 +1121,7 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1113
1121
|
if (!tokens.length && !nameRe && !decFilter) return "empty query";
|
|
1114
1122
|
const scored = scoreModules(graph, tokens);
|
|
1115
1123
|
if (!scored.length) {
|
|
1116
|
-
return `no module matches "${query}". Try broader keywords, or
|
|
1124
|
+
return `no module matches "${query}". Try broader keywords, or ${toolNamePrefix}describe <path> if you know where it lives.`;
|
|
1117
1125
|
}
|
|
1118
1126
|
const hits = scored.slice(0, limit);
|
|
1119
1127
|
const lines = [`${scored.length} module(s) match "${query}" (top ${hits.length}):`];
|
|
@@ -1121,7 +1129,7 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1121
1129
|
const m = matching.length ? ` — matching: ${capJoin([...new Set(matching)], SEARCH_SYMBOLS_SHOWN)}` : "";
|
|
1122
1130
|
lines.push(`- ${ind.label} (defines ${defineCount} symbol(s))${m}`);
|
|
1123
1131
|
}
|
|
1124
|
-
lines.push(
|
|
1132
|
+
lines.push(`Then ${toolNamePrefix}describe <path> for the full sibling list + typed edges, or ${toolNamePrefix}impact <path> for dependents.`);
|
|
1125
1133
|
return lines.join("\n");
|
|
1126
1134
|
}
|
|
1127
1135
|
|
|
@@ -1187,11 +1195,11 @@ const CALL_CAP = 30;
|
|
|
1187
1195
|
|
|
1188
1196
|
/** A class's methods + attributes (with sites/decorators) in one slice — replaces
|
|
1189
1197
|
* reading the class body. Uses the `contains` (seon:containsCodeEntity) relation. */
|
|
1190
|
-
export function renderMembers(graph, ind) {
|
|
1198
|
+
export function renderMembers(graph, ind, { toolNamePrefix = "tmct_" } = {}) {
|
|
1191
1199
|
const lines = [`${ind.label} — ${classHeading(ind.class)} (id: ${ind.id})`];
|
|
1192
1200
|
const contains = edgesOfKind(graph, "contains").filter((e) => e.subject === ind.id);
|
|
1193
1201
|
if (!contains.length) {
|
|
1194
|
-
lines.push(
|
|
1202
|
+
lines.push(`members: none recorded (empty class, or members not in the extracted graph). Use ${toolNamePrefix}describe for its edges.`);
|
|
1195
1203
|
return lines.join("\n");
|
|
1196
1204
|
}
|
|
1197
1205
|
const methods = [];
|
|
@@ -1205,7 +1213,7 @@ export function renderMembers(graph, ind) {
|
|
|
1205
1213
|
}
|
|
1206
1214
|
if (methods.length) lines.push(`methods (${methods.length}): ${capJoin(methods, MEMBERS_CAP)}`);
|
|
1207
1215
|
if (attrs.length) lines.push(`attributes (${attrs.length}): ${capJoin(attrs, MEMBERS_CAP)}`);
|
|
1208
|
-
lines.push(
|
|
1216
|
+
lines.push(`Use ${toolNamePrefix}snippet <Class.member> for an exact body.`);
|
|
1209
1217
|
return lines.join("\n");
|
|
1210
1218
|
}
|
|
1211
1219
|
|
|
@@ -1215,7 +1223,7 @@ const attrVal = (ind, key) => (ind?.attributes || []).find((a) => a.key === key)
|
|
|
1215
1223
|
* return annotation, raises/catches, self-fields, flags, decorators, one-line doc —
|
|
1216
1224
|
* so the agent gets the API surface without reading the body. Deterministic ast facts
|
|
1217
1225
|
* (kept OUT of tmct_context's lean bundle; this is the targeted tool for them). */
|
|
1218
|
-
export function renderSignature(graph, ind) {
|
|
1226
|
+
export function renderSignature(graph, ind, { toolNamePrefix = "tmct_" } = {}) {
|
|
1219
1227
|
const site = siteOf(ind);
|
|
1220
1228
|
const lines = [`${ind.label} — ${classHeading(ind.class)}${spanTag(site)}`];
|
|
1221
1229
|
const params = attrVal(ind, "params");
|
|
@@ -1242,8 +1250,8 @@ export function renderSignature(graph, ind) {
|
|
|
1242
1250
|
if (value) lines.push(`value: ${value}`);
|
|
1243
1251
|
const doc = attrVal(ind, "doc");
|
|
1244
1252
|
if (doc) lines.push(`doc: ${doc}`);
|
|
1245
|
-
if (lines.length === 1) lines.push(
|
|
1246
|
-
lines.push(
|
|
1253
|
+
if (lines.length === 1) lines.push(`(no signature detail recorded for this symbol — likely a module or attribute; use ${toolNamePrefix}snippet for its source.)`);
|
|
1254
|
+
lines.push(`Use ${toolNamePrefix}snippet for the exact body.`);
|
|
1247
1255
|
return lines.join("\n");
|
|
1248
1256
|
}
|
|
1249
1257
|
|
|
@@ -1391,18 +1399,18 @@ export function renderHistory(graph, ind) {
|
|
|
1391
1399
|
// classes whose call graph lives on the fn/method-precise callsSymbol edge, not module-coarse calls
|
|
1392
1400
|
const CALL_SYMBOL_CLASSES = new Set(["Function", "Method"]);
|
|
1393
1401
|
|
|
1394
|
-
export function renderCallers(graph, ind) {
|
|
1402
|
+
export function renderCallers(graph, ind, { toolNamePrefix = "tmct_" } = {}) {
|
|
1395
1403
|
// symbol grain: a fine symbol's callers are the SUBJECTS of callsSymbol edges into it.
|
|
1396
1404
|
if (CALL_SYMBOL_CLASSES.has(ind.class)) {
|
|
1397
1405
|
const callers = [...new Set(edgesOfKind(graph, "callsSymbol").filter((e) => e.object === ind.id).map((e) => e.subjectLabel || e.subject))];
|
|
1398
|
-
if (!callers.length) return `${ind.label}: no recorded callers (fine-grained call edges are conservative — absence is not proof). Try
|
|
1406
|
+
if (!callers.length) return `${ind.label}: no recorded callers (fine-grained call edges are conservative — absence is not proof). Try ${toolNamePrefix}impact for the full reverse closure.`;
|
|
1399
1407
|
return `${ind.label} — called by ${callers.length} symbol(s):\n ${capJoin(callers, CALL_CAP, "\n ")}`;
|
|
1400
1408
|
}
|
|
1401
1409
|
const modId = moduleIdOf(graph, ind);
|
|
1402
1410
|
if (!modId) return `cannot map ${ind.label} to a module.`;
|
|
1403
1411
|
const modLabel = graph.byId.get(modId)?.label || modId;
|
|
1404
1412
|
const callers = [...new Set(edgesOfKind(graph, "calls").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject))];
|
|
1405
|
-
if (!callers.length) return `${modLabel}: no recorded callers (calls are coarse/import-backed — absence is not proof). Try
|
|
1413
|
+
if (!callers.length) return `${modLabel}: no recorded callers (calls are coarse/import-backed — absence is not proof). Try ${toolNamePrefix}impact for the full reverse closure.`;
|
|
1406
1414
|
return `${modLabel} — called by ${callers.length} module(s):\n ${capJoin(callers, CALL_CAP, "\n ")}`;
|
|
1407
1415
|
}
|
|
1408
1416
|
|
|
@@ -1632,7 +1640,7 @@ export function scoreSymbolsRanked(graph, tokens, { kind, decFilter = "", nameRe
|
|
|
1632
1640
|
return hits;
|
|
1633
1641
|
}
|
|
1634
1642
|
|
|
1635
|
-
function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, nameRe }) {
|
|
1643
|
+
function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, nameRe, toolNamePrefix = "tmct_" }) {
|
|
1636
1644
|
const targetClass = SYMBOL_CLASSES[kind];
|
|
1637
1645
|
if (!targetClass) return `unknown kind "${kind}" (use function, class, method, attribute, or module).`;
|
|
1638
1646
|
const hits = scoreSymbolsRanked(graph, tokens, { kind, decFilter, nameRe });
|
|
@@ -1640,7 +1648,7 @@ function searchSymbols(graph, tokens, { limit = SEARCH_LIMIT, kind, decFilter, n
|
|
|
1640
1648
|
const top = hits.slice(0, limit);
|
|
1641
1649
|
const lines = [`${hits.length} ${kind}(s) match (top ${top.length}):`];
|
|
1642
1650
|
for (const { ind } of top) lines.push(`- ${ind.label}${spanTag(siteOf(ind))}`);
|
|
1643
|
-
lines.push(
|
|
1651
|
+
lines.push(`Then ${toolNamePrefix}snippet <name> for the exact body, or ${toolNamePrefix}describe for its edges.`);
|
|
1644
1652
|
return lines.join("\n");
|
|
1645
1653
|
}
|
|
1646
1654
|
|
|
@@ -30,6 +30,7 @@ export const ENTITY_CLASSES = Object.freeze([
|
|
|
30
30
|
export const EDGE_PREDICATES = Object.freeze([
|
|
31
31
|
"imports", "calls", "callsSymbol", "defines", "tests",
|
|
32
32
|
"touches", "touchesSymbol", "contains", "inherits", "cochange", "reexports",
|
|
33
|
+
"serves", "denotes",
|
|
33
34
|
]);
|
|
34
35
|
|
|
35
36
|
/** The closed effect vocabulary — every way an operator's declared delta can
|
package/src/domain/concept.mjs
CHANGED
|
@@ -219,6 +219,10 @@ export const RELATION_TERM = Object.freeze({
|
|
|
219
219
|
export: "reexports", exports: "reexports", exporting: "reexports", exported: "reexports",
|
|
220
220
|
reexport: "reexports", reexports: "reexports", reexporting: "reexports",
|
|
221
221
|
"re-export": "reexports", "re-exports": "reexports", "re-exporting": "reexports",
|
|
222
|
+
// provider-declared kinds: tmct's own indexer emits neither, so a graph it
|
|
223
|
+
// built degrades to the honest two-band "no edges" answer rather than a miss.
|
|
224
|
+
serve: "serves", serves: "serves", serving: "serves", served: "serves",
|
|
225
|
+
denote: "denotes", denotes: "denotes", denoting: "denotes", denoted: "denotes",
|
|
222
226
|
});
|
|
223
227
|
|
|
224
228
|
/** concept key → the relationKind()s whose edges it enumerates. A concept can span
|
|
@@ -234,6 +238,8 @@ const RELATION_KINDS = Object.freeze({
|
|
|
234
238
|
touches: ["touches", "touchesSymbol"],
|
|
235
239
|
cochange: ["cochange"],
|
|
236
240
|
reexports: ["reexports"],
|
|
241
|
+
serves: ["serves"],
|
|
242
|
+
denotes: ["denotes"],
|
|
237
243
|
});
|
|
238
244
|
|
|
239
245
|
/** concept key → the verb phrase that renders an edge as an English sentence
|
|
@@ -248,6 +254,8 @@ const RELATION_RENDER = Object.freeze({
|
|
|
248
254
|
touches: { verb: "touches", edgeNoun: "touch" },
|
|
249
255
|
cochange: { verb: "changes together with", edgeNoun: "change-coupling" },
|
|
250
256
|
reexports: { verb: "re-exports", edgeNoun: "re-export" },
|
|
257
|
+
serves: { verb: "serves", edgeNoun: "service" },
|
|
258
|
+
denotes: { verb: "denotes", edgeNoun: "denotation" },
|
|
251
259
|
});
|
|
252
260
|
|
|
253
261
|
/** Per concept key, the candidate follow-up shapes in priority order — each
|
|
@@ -290,6 +298,14 @@ const RELATION_FOLLOWUP_SHAPES = Object.freeze({
|
|
|
290
298
|
{ side: "subj", make: (x) => `what does ${x} export` },
|
|
291
299
|
{ side: "obj", make: (x) => `where is ${x} defined` },
|
|
292
300
|
],
|
|
301
|
+
serves: [
|
|
302
|
+
{ side: "subj", make: (x) => `what does ${x} serve` },
|
|
303
|
+
{ side: "obj", make: (x) => `what serves ${x}` },
|
|
304
|
+
],
|
|
305
|
+
denotes: [
|
|
306
|
+
{ side: "subj", make: (x) => `what does ${x} denote` },
|
|
307
|
+
{ side: "obj", make: (x) => `what denotes ${x}` },
|
|
308
|
+
],
|
|
293
309
|
});
|
|
294
310
|
|
|
295
311
|
/** How many example edges the relation force shows before the remainder is held for
|
|
@@ -186,6 +186,12 @@ const EMBEDDED_MEANS_RE = /^what\s+((?:an?\s+|the\s+)?[\w'-]+(?:\s+[\w'-]+){0,2}
|
|
|
186
186
|
* untouched, a relation/interrogative remainder unwraps to itself, anything
|
|
187
187
|
* else bridges to "describe <thing>". */
|
|
188
188
|
const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
189
|
+
/** An "everything I need [to <verb>|for X]" remainder always bridges to
|
|
190
|
+
* "describe <thing>", even when its purpose clause happens to contain a
|
|
191
|
+
* relation verb ("everything I need to change X") — RELATION_VERB_RE's
|
|
192
|
+
* bag-of-words probe would otherwise misread that verb as making the
|
|
193
|
+
* remainder itself an already-relational clause. */
|
|
194
|
+
const EVERYTHING_I_NEED_RE = /^everything\s+i(?:'d|\s+would)?\s+need\b/i;
|
|
189
195
|
|
|
190
196
|
/** Leading STACCATO connective before an ALREADY well-formed question ("and
|
|
191
197
|
* what imports it") -> the question alone. Gated on the remainder starting
|
|
@@ -196,6 +202,12 @@ const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
|
196
202
|
* noise-strip layer never drops. */
|
|
197
203
|
const LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
|
|
198
204
|
const QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
|
|
205
|
+
/** A bare relative-clause remainder ("the tests that cover it"), the shape an
|
|
206
|
+
* anaphoric follow-up takes when it names a thing instead of asking outright.
|
|
207
|
+
* Anchored on a determiner and a short head noun so a mid-clause boolean
|
|
208
|
+
* branch ("and are tested", "and call Y") never reaches it. */
|
|
209
|
+
const RELATIVE_CLAUSE_LEAD_RE =
|
|
210
|
+
/^(?:the|its|their|his|her|our|my)\s+[\w'-]+(?:\s+[\w'-]+){0,2}\s+(?:that|which|who)\s+\S/i;
|
|
199
211
|
|
|
200
212
|
/** A topic-switch/self-interruption preamble ("actually never mind, <Q>"),
|
|
201
213
|
* repeating so a stack of markers peels in one pass. Distinct from
|
|
@@ -248,7 +260,9 @@ export function applyPreambleFrames(text) {
|
|
|
248
260
|
if (m) {
|
|
249
261
|
const rest = m[1].trim();
|
|
250
262
|
if (!isListingRemainder(rest)) {
|
|
251
|
-
|
|
263
|
+
const isRelationClause = !EVERYTHING_I_NEED_RE.test(rest)
|
|
264
|
+
&& (RELATION_VERB_RE.test(rest) || INTERROGATIVE_LEAD_RE.test(rest));
|
|
265
|
+
q = isRelationClause ? rest : `describe ${rest}`;
|
|
252
266
|
}
|
|
253
267
|
}
|
|
254
268
|
m = q.match(LEADING_CONNECTIVE_RE);
|
|
@@ -261,6 +275,7 @@ export function applyPreambleFrames(text) {
|
|
|
261
275
|
INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)
|
|
262
276
|
|| TOPIC_SWITCH_PREAMBLE_RE.test(rest) || ACK_PREAMBLE_RE.test(rest)
|
|
263
277
|
|| HEDGE_ADVERB_PREAMBLE_RE.test(rest) || BROWSING_PREAMBLE_RE.test(rest)
|
|
278
|
+
|| RELATIVE_CLAUSE_LEAD_RE.test(rest)
|
|
264
279
|
) q = rest;
|
|
265
280
|
}
|
|
266
281
|
if (q === before) break;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// the rankers that read what they build.
|
|
3
3
|
|
|
4
4
|
/** Does this module path (or a lowercased path-shaped label) belong to test code?
|
|
5
|
-
* Covers `test/` and `tests/` segments,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Covers `test/` and `tests/` segments, a hyphen/underscore-prefixed test directory
|
|
6
|
+
* (`behaviour-tests/`, `unit_test/`), Python's `test_*.py` convention, .NET's
|
|
7
|
+
* `*.Tests` assembly suffix, and a `.test`/`.spec` file anywhere in the tree.
|
|
8
|
+
* Case-sensitive: callers holding mixed-case paths lowercase first. */
|
|
8
9
|
export const isTestPath = (p) =>
|
|
9
|
-
/(^|\/)tests?\//.test(p) || /(^|\/)test_[^/]*\.py$/.test(p) || /\.tests(\.|$)/.test(p)
|
|
10
|
+
/(^|\/)tests?\//.test(p) || /(^|\/)test_[^/]*\.py$/.test(p) || /\.tests(\.|$)/.test(p)
|
|
11
|
+
|| /(^|\/)[^/]*[-_]tests?\//.test(p) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(p);
|