@polycode-projects/the-mechanical-code-talker 3.1.0 → 3.1.2
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/package.json +3 -1
- package/src/domain/ask-vocab.mjs +23 -0
- package/src/domain/ask.mjs +63 -13
- package/src/domain/codegraph.mjs +25 -17
- package/src/domain/interpret/normalize.mjs +7 -0
- 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 +183 -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/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
|
|
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.2",
|
|
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"
|
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];
|
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
|
|
|
@@ -196,6 +196,12 @@ const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
|
196
196
|
* noise-strip layer never drops. */
|
|
197
197
|
const LEADING_CONNECTIVE_RE = /^(?:and|also|so|then|now|but)\s+(.+)$/i;
|
|
198
198
|
const QUESTION_AUX_LEAD_RE = /^(?:does|do|did|is|are|was|were|has|have|had|can|could|will|would|should)\b/i;
|
|
199
|
+
/** A bare relative-clause remainder ("the tests that cover it"), the shape an
|
|
200
|
+
* anaphoric follow-up takes when it names a thing instead of asking outright.
|
|
201
|
+
* Anchored on a determiner and a short head noun so a mid-clause boolean
|
|
202
|
+
* branch ("and are tested", "and call Y") never reaches it. */
|
|
203
|
+
const RELATIVE_CLAUSE_LEAD_RE =
|
|
204
|
+
/^(?:the|its|their|his|her|our|my)\s+[\w'-]+(?:\s+[\w'-]+){0,2}\s+(?:that|which|who)\s+\S/i;
|
|
199
205
|
|
|
200
206
|
/** A topic-switch/self-interruption preamble ("actually never mind, <Q>"),
|
|
201
207
|
* repeating so a stack of markers peels in one pass. Distinct from
|
|
@@ -261,6 +267,7 @@ export function applyPreambleFrames(text) {
|
|
|
261
267
|
INTERROGATIVE_LEAD_RE.test(rest) || QUESTION_AUX_LEAD_RE.test(rest)
|
|
262
268
|
|| TOPIC_SWITCH_PREAMBLE_RE.test(rest) || ACK_PREAMBLE_RE.test(rest)
|
|
263
269
|
|| HEDGE_ADVERB_PREAMBLE_RE.test(rest) || BROWSING_PREAMBLE_RE.test(rest)
|
|
270
|
+
|| RELATIVE_CLAUSE_LEAD_RE.test(rest)
|
|
264
271
|
) q = rest;
|
|
265
272
|
}
|
|
266
273
|
if (q === before) break;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// paraphrase-ing8.mjs — the deterministic ING-8 equivalence checker: the harder
|
|
2
|
+
// whole-document paraphrase shapes ING-7's own checker (verifySubClassParaphrase,
|
|
3
|
+
// ./paraphrase.mjs) doesn't cover — non-isa relations (has/creates/capableOf),
|
|
4
|
+
// multi-sentence documents, and synonym substitution over that closed relation
|
|
5
|
+
// vocabulary.
|
|
6
|
+
//
|
|
7
|
+
// Strategy, mirroring verifySubClassParaphrase's own pattern: wherever the
|
|
8
|
+
// predicate space allows exact re-derivation (a single isa fact on both sides),
|
|
9
|
+
// reuse ING-7's own closure re-derivation directly. Everywhere else — every
|
|
10
|
+
// non-isa relation, and every multi-sentence document — there is no entailment
|
|
11
|
+
// closure to re-derive over, so the fallback is normalization + a closed
|
|
12
|
+
// synonym-template table per relation, recognized and compared as an exact set.
|
|
13
|
+
// A document holding a sentence outside the closed template set is reported
|
|
14
|
+
// unverified, never guessed — the same "verified, never instead of the
|
|
15
|
+
// original" discipline paraphrase.mjs itself states for the isa case.
|
|
16
|
+
|
|
17
|
+
import { recoverSubClassTriple, verifySubClassParaphrase } from "./paraphrase.mjs";
|
|
18
|
+
import { normFactTerm, fnv1aHex } from "./hash.mjs";
|
|
19
|
+
|
|
20
|
+
const articleFor = (word) => (/^[aeiou]/i.test(String(word || "")) ? "an" : "a");
|
|
21
|
+
|
|
22
|
+
// Every template reads "SUBJECT verb OBJECT" left to right, same discipline as
|
|
23
|
+
// paraphrase.mjs's SUBCLASS_TEMPLATES — no passive/reordered form, since that's
|
|
24
|
+
// the shape most likely to invert subject/object under a naive recognizer.
|
|
25
|
+
const HAS_TEMPLATES = [
|
|
26
|
+
(s, o) => `${s} has ${articleFor(o)} ${o}`,
|
|
27
|
+
(s, o) => `${s} possesses ${articleFor(o)} ${o}`,
|
|
28
|
+
(s, o) => `${s} owns ${articleFor(o)} ${o}`,
|
|
29
|
+
(s, o) => `${s} carries ${articleFor(o)} ${o}`,
|
|
30
|
+
];
|
|
31
|
+
const HAS_RECOGNIZERS = [
|
|
32
|
+
/^(.+?)\s+has\s+an?\s+(.+)$/i,
|
|
33
|
+
/^(.+?)\s+possesses\s+an?\s+(.+)$/i,
|
|
34
|
+
/^(.+?)\s+owns\s+an?\s+(.+)$/i,
|
|
35
|
+
/^(.+?)\s+carries\s+an?\s+(.+)$/i,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const CREATES_TEMPLATES = [
|
|
39
|
+
(s, o) => `${s} creates ${o}`,
|
|
40
|
+
(s, o) => `${s} produces ${o}`,
|
|
41
|
+
(s, o) => `${s} generates ${o}`,
|
|
42
|
+
(s, o) => `${s} causes ${o}`,
|
|
43
|
+
];
|
|
44
|
+
const CREATES_RECOGNIZERS = [
|
|
45
|
+
/^(.+?)\s+creates\s+(.+)$/i,
|
|
46
|
+
/^(.+?)\s+produces\s+(.+)$/i,
|
|
47
|
+
/^(.+?)\s+generates\s+(.+)$/i,
|
|
48
|
+
/^(.+?)\s+causes\s+(.+)$/i,
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const CAPABLEOF_TEMPLATES = [
|
|
52
|
+
(s, o) => `${s} can ${o}`,
|
|
53
|
+
(s, o) => `${s} is able to ${o}`,
|
|
54
|
+
(s, o) => `${s} knows how to ${o}`,
|
|
55
|
+
];
|
|
56
|
+
const CAPABLEOF_RECOGNIZERS = [
|
|
57
|
+
/^(.+?)\s+can\s+(.+)$/i,
|
|
58
|
+
/^(.+?)\s+is\s+able\s+to\s+(.+)$/i,
|
|
59
|
+
/^(.+?)\s+knows\s+how\s+to\s+(.+)$/i,
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// The closed non-isa relation vocabulary this checker recognizes. Each family
|
|
63
|
+
// pairs its templates and recognizers by index, same convention as isa.
|
|
64
|
+
const RELATION_FAMILIES = {
|
|
65
|
+
has: { templates: HAS_TEMPLATES, recognizers: HAS_RECOGNIZERS },
|
|
66
|
+
creates: { templates: CREATES_TEMPLATES, recognizers: CREATES_RECOGNIZERS },
|
|
67
|
+
capableOf: { templates: CAPABLEOF_TEMPLATES, recognizers: CAPABLEOF_RECOGNIZERS },
|
|
68
|
+
};
|
|
69
|
+
export const RELATION_FAMILY_IDS = Object.keys(RELATION_FAMILIES);
|
|
70
|
+
|
|
71
|
+
/** Deterministic template pick — same (subject, object) always picks the same
|
|
72
|
+
* template, spread across the table by a pure hash (paraphrase.mjs's own
|
|
73
|
+
* pickTemplateIndex pattern). `variantIndex`, when given, overrides the hash
|
|
74
|
+
* pick (a corpus generator wanting two DIFFERENT closed phrasings for the same
|
|
75
|
+
* fact passes distinct indices explicitly rather than relying on the hash). */
|
|
76
|
+
function pickTemplateIndex(subject, object, templateCount, variantIndex) {
|
|
77
|
+
if (variantIndex !== null && variantIndex !== undefined) {
|
|
78
|
+
return ((variantIndex % templateCount) + templateCount) % templateCount;
|
|
79
|
+
}
|
|
80
|
+
const h = fnv1aHex(`${subject}\0${object}`);
|
|
81
|
+
return parseInt(h.slice(0, 8), 16) % templateCount;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Generate a closed-template phrasing of `subject <family> object` — never
|
|
85
|
+
* null, always one of RELATION_FAMILIES[family]'s templates. Rule/template-
|
|
86
|
+
* based only, no LLM, matching paraphrase.mjs's paraphraseSubClass. */
|
|
87
|
+
export function paraphraseRelation(family, subject, object, variantIndex = null) {
|
|
88
|
+
const def = RELATION_FAMILIES[family];
|
|
89
|
+
if (!def) throw new Error(`paraphrase-ing8: unknown relation family "${family}"`);
|
|
90
|
+
const idx = pickTemplateIndex(subject, object, def.templates.length, variantIndex);
|
|
91
|
+
return def.templates[idx](subject, object);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Recover {subject, object} from text matching one of `family`'s closed
|
|
95
|
+
* recognizers — null if it matches none of them. A plain closed-set regex
|
|
96
|
+
* match, never a fuzzy/NLP parse, the exact inverse of paraphraseRelation. */
|
|
97
|
+
export function recoverRelationTriple(family, text) {
|
|
98
|
+
const def = RELATION_FAMILIES[family];
|
|
99
|
+
if (!def) return null;
|
|
100
|
+
const s = String(text || "").trim();
|
|
101
|
+
for (const re of def.recognizers) {
|
|
102
|
+
const m = s.match(re);
|
|
103
|
+
if (m) return { subject: m[1].trim(), object: m[2].trim() };
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Verify one relation-family paraphrase (mirrors verifySubClassParaphrase's
|
|
109
|
+
* single-fact contract, but for a non-isa relation): the paraphrase text must
|
|
110
|
+
* recover to the SAME (subject, object) under `family`'s closed recognizers.
|
|
111
|
+
* No closure to re-derive over for a non-isa relation, so this is a direct
|
|
112
|
+
* normalized compare, never a fuzzy one. */
|
|
113
|
+
export function verifyRelationParaphrase(family, subject, object, paraphraseText) {
|
|
114
|
+
const recovered = recoverRelationTriple(family, paraphraseText);
|
|
115
|
+
if (!recovered) return { verified: false };
|
|
116
|
+
const verified = normFactTerm(recovered.subject) === normFactTerm(subject)
|
|
117
|
+
&& normFactTerm(recovered.object) === normFactTerm(object);
|
|
118
|
+
return { verified };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- whole-document recognition: isa + every relation family above ----
|
|
122
|
+
|
|
123
|
+
/** Recover a single (family, subject, object) triple from one sentence: isa
|
|
124
|
+
* first (paraphrase.mjs's closed subclass templates), then every relation
|
|
125
|
+
* family in RELATION_FAMILY_IDS order — null if the sentence matches none of
|
|
126
|
+
* the closed templates. Family keywords ("is a kind of" vs "has" vs "creates"
|
|
127
|
+
* vs "can" …) don't overlap, so recognition order never resolves a genuine
|
|
128
|
+
* ambiguity, only which closed family a sentence belongs to. */
|
|
129
|
+
export function recoverAnyTriple(sentence) {
|
|
130
|
+
const isaHit = recoverSubClassTriple(sentence);
|
|
131
|
+
if (isaHit) return { family: "isa", subject: isaHit.subject, object: isaHit.object };
|
|
132
|
+
for (const family of RELATION_FAMILY_IDS) {
|
|
133
|
+
const hit = recoverRelationTriple(family, sentence);
|
|
134
|
+
if (hit) return { family, subject: hit.subject, object: hit.object };
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function splitSentences(text) {
|
|
140
|
+
return String(text || "")
|
|
141
|
+
.split(/(?<=[.!?])\s+/)
|
|
142
|
+
.map((s) => s.trim())
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tripleKey = (t) => `${t.family}\0${normFactTerm(t.subject)}\0${normFactTerm(t.object)}`;
|
|
147
|
+
|
|
148
|
+
/** Recover EVERY sentence of a document as a closed-template triple — null (the
|
|
149
|
+
* whole document unrecognized) the moment any sentence fails to match, never a
|
|
150
|
+
* partial list standing in for the whole. */
|
|
151
|
+
export function recoverDocumentTriples(text) {
|
|
152
|
+
const sentences = splitSentences(text);
|
|
153
|
+
if (!sentences.length) return null;
|
|
154
|
+
const triples = [];
|
|
155
|
+
for (const sentence of sentences) {
|
|
156
|
+
const t = recoverAnyTriple(sentence.replace(/\.+$/, ""));
|
|
157
|
+
if (!t) return null;
|
|
158
|
+
triples.push(t);
|
|
159
|
+
}
|
|
160
|
+
return triples;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Verify a whole-document ING-8 paraphrase pair: does `restatementText` say
|
|
164
|
+
* exactly what `inputText` says, no more and no less? A single isa fact on
|
|
165
|
+
* both sides re-derives the subclass closure directly through
|
|
166
|
+
* verifySubClassParaphrase (ING-7's own exact-re-derivation strategy).
|
|
167
|
+
* Everything else — a non-isa relation, or more than one sentence on either
|
|
168
|
+
* side — recovers every sentence of both texts against the closed template
|
|
169
|
+
* set above and requires the exact same (family, subject, object) SET, order
|
|
170
|
+
* irrelevant. Never verified when either side holds a sentence outside the
|
|
171
|
+
* closed templates, or when the two recovered sets differ in size or content
|
|
172
|
+
* (a dropped fact and an invented fact both fail this the same way — the
|
|
173
|
+
* checker declines rather than guesses which). */
|
|
174
|
+
export function verifyIng8Paraphrase(inputText, restatementText) {
|
|
175
|
+
const inputTriples = recoverDocumentTriples(inputText);
|
|
176
|
+
const restatementTriples = recoverDocumentTriples(restatementText);
|
|
177
|
+
if (!inputTriples || !restatementTriples) {
|
|
178
|
+
return { verified: false, reason: "unrecognized", inputTriples, restatementTriples };
|
|
179
|
+
}
|
|
180
|
+
const bothSingleIsa = inputTriples.length === 1 && restatementTriples.length === 1
|
|
181
|
+
&& inputTriples[0].family === "isa" && restatementTriples[0].family === "isa";
|
|
182
|
+
if (bothSingleIsa) {
|
|
183
|
+
const { subject, object } = inputTriples[0];
|
|
184
|
+
const restatementSentence = splitSentences(restatementText)[0].replace(/\.+$/, "");
|
|
185
|
+
const { verified } = verifySubClassParaphrase(subject, object, restatementSentence);
|
|
186
|
+
return { verified, method: "isa-closure", inputTriples, restatementTriples };
|
|
187
|
+
}
|
|
188
|
+
if (inputTriples.length !== restatementTriples.length) {
|
|
189
|
+
return { verified: false, reason: "fact count mismatch", inputTriples, restatementTriples };
|
|
190
|
+
}
|
|
191
|
+
const inputKeys = inputTriples.map(tripleKey).sort();
|
|
192
|
+
const restatementKeys = restatementTriples.map(tripleKey).sort();
|
|
193
|
+
const verified = inputKeys.every((k, i) => k === restatementKeys[i]);
|
|
194
|
+
return { verified, method: "closed-template-set", inputTriples, restatementTriples };
|
|
195
|
+
}
|