@polycode-projects/the-mechanical-code-talker 2.5.4 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/data/templates/responses.jsonl +1 -1
- package/package.json +1 -1
- package/src/domain/ask.mjs +66 -10
- package/src/domain/codegraph.mjs +6 -2
- package/src/domain/interpret/normalize.mjs +1 -1
- package/src/domain/router/registry.mjs +17 -4
- package/src/domain/router/resolver.mjs +4 -2
- package/src/services/chat.mjs +626 -43
- package/src/services/sessions.mjs +10 -2
- package/src/surfaces/web/memory-ask-browser.bundle.js +88 -19
package/README.md
CHANGED
|
@@ -285,8 +285,8 @@ steps:
|
|
|
285
285
|
- src/handlers/tasks.mjs (imports it) — tests: test/tasks.test.mjs
|
|
286
286
|
- src/server/router.mjs (imports it) — tests: none recorded
|
|
287
287
|
depth 2 (2):
|
|
288
|
-
- src/handlers/users.mjs (
|
|
289
|
-
- src/server/app.mjs (
|
|
288
|
+
- src/handlers/users.mjs (reaches it through an intermediary) — tests: none recorded
|
|
289
|
+
- src/server/app.mjs (reaches it through an intermediary) — tests: none recorded
|
|
290
290
|
2. tmct_untested {}
|
|
291
291
|
7 source module(s) with no covering test module:
|
|
292
292
|
src/core/validate.mjs
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
{"id":"nudge-commands","class":"nudge","register":"friendly","template":"If prose fails you, the slash commands always work — try {command}."}
|
|
55
55
|
{"id":"nudge-narrower","class":"nudge","register":"friendly","template":"That matched {count} things — too many to be useful. Narrow it with a module or class name."}
|
|
56
56
|
{"id":"conversational-greeting","class":"conversational","register":"friendly","template":"Hi. Ask me about this codebase — imports, calls, definitions, history — or /help."}
|
|
57
|
-
{"id":"conversational-greeting-hello-there","class":"conversational","register":"friendly","template":"Hello there.
|
|
57
|
+
{"id":"conversational-greeting-hello-there","class":"conversational","register":"friendly","template":"Hello there. Ask me about this codebase, or /help."}
|
|
58
58
|
{"id":"conversational-greeting-good-morning","class":"conversational","register":"friendly","template":"Good morning. Ask me about this codebase, or /help."}
|
|
59
59
|
{"id":"conversational-greeting-good-afternoon","class":"conversational","register":"friendly","template":"Good afternoon. Ask me about this codebase, or /help."}
|
|
60
60
|
{"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
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; no codebase index of its own.",
|
package/src/domain/ask.mjs
CHANGED
|
@@ -225,7 +225,7 @@ function parseComposite(text, nlp) {
|
|
|
225
225
|
|| parseQualifierCheck(w, lc)
|
|
226
226
|
|| parseUniversal(w, lc, nlp)
|
|
227
227
|
|| parseNegation(text, nlp, 0)
|
|
228
|
-
|| parseNegatedAsk(w, lc)
|
|
228
|
+
|| parseNegatedAsk(w, lc, nlp)
|
|
229
229
|
|| parseForwardNegation(w, lc, nlp)
|
|
230
230
|
|| parseTemporal(w, lc, nlp, 0)
|
|
231
231
|
|| parseCommitFilter(w, lc)
|
|
@@ -718,11 +718,18 @@ function parseQualifierCheck(w, lc) {
|
|
|
718
718
|
* the same sentence at positive polarity ("not" is no stopword) and the merge
|
|
719
719
|
* would call the two readings an ambiguity. Declining to a composite production
|
|
720
720
|
* is what keeps the sentence out of that merge. */
|
|
721
|
-
function parseNegatedAsk(w, lc) {
|
|
722
|
-
|
|
721
|
+
function parseNegatedAsk(w, lc, nlp) {
|
|
722
|
+
// The copular leads carry the PASSIVE twin ("is X not imported by Y") —
|
|
723
|
+
// without them the strategies read the sentence at positive polarity (the
|
|
724
|
+
// "not" drops as noise) and the bare "Yes" answers the un-negated question.
|
|
725
|
+
// parseQualifierCheck ran first, so "is X not deprecated" keeps its lane.
|
|
726
|
+
// The positive re-parse runs the SAME simple-clause pair the composer's
|
|
727
|
+
// fragments use — the passive lives in the keyword strategy, which the
|
|
728
|
+
// anchored grammar alone never reaches.
|
|
729
|
+
if (!["do", "does", "did", "is", "are", "was", "were"].includes(lc[0])) return null;
|
|
723
730
|
const notIdx = lc.indexOf("not", 1);
|
|
724
731
|
if (notIdx < 0) return null;
|
|
725
|
-
const positive =
|
|
732
|
+
const positive = parseSimpleClause(w.filter((_, i) => i !== notIdx).join(" "), nlp);
|
|
726
733
|
if (!positive || positive.shape !== "ask") return null;
|
|
727
734
|
return { ...positive, negated: true };
|
|
728
735
|
}
|
|
@@ -1784,7 +1791,15 @@ function evalTemporal(graph, ast, opts) {
|
|
|
1784
1791
|
}
|
|
1785
1792
|
|
|
1786
1793
|
function evalSuperlative(graph, ast) {
|
|
1787
|
-
|
|
1794
|
+
let pool = graph.individuals.filter((i) => i.class === ast.entityType);
|
|
1795
|
+
// A tests-metric ranking over Modules surveys COVERAGE, and a test module
|
|
1796
|
+
// is never a coverage target — the same exclusion renderUntested (the
|
|
1797
|
+
// /untested surface) applies, so the two surveys can't disagree about the
|
|
1798
|
+
// same set ("what most needs a test" used to name b.test.mjs).
|
|
1799
|
+
if (ast.metric?.kind === "tests" && ast.entityType === "Module") {
|
|
1800
|
+
const testSubjects = new Set(edgesOfKind(graph, "tests").map((e) => e.subject));
|
|
1801
|
+
pool = pool.filter((i) => !testSubjects.has(i.id) && !isTestPath(String(i.label).toLowerCase()));
|
|
1802
|
+
}
|
|
1788
1803
|
const scored = pool.map((ind) => ({ ind, score: degreeMetric(graph, ind, ast.metric) }))
|
|
1789
1804
|
.sort((a, z) => (ast.extreme === "most" ? z.score - a.score : a.score - z.score));
|
|
1790
1805
|
if (!scored.length) return { compositeKind: "superlative", entityType: ast.entityType, matches: [] };
|
|
@@ -2608,7 +2623,10 @@ export function resolveObject(graph, term, opts = {}) {
|
|
|
2608
2623
|
}
|
|
2609
2624
|
return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
|
|
2610
2625
|
}
|
|
2611
|
-
|
|
2626
|
+
// The pinned-class branch honors the same contract — a term carrying words
|
|
2627
|
+
// the index has no reading for declines instead of resolving past them
|
|
2628
|
+
// ("the old Task" pinned to Class must not silently swallow "old").
|
|
2629
|
+
return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
|
|
2612
2630
|
}
|
|
2613
2631
|
|
|
2614
2632
|
/** Resolve a term that may be a context pronoun ("this"/"it"/"that"/"here") —
|
|
@@ -2979,7 +2997,10 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
|
|
|
2979
2997
|
if (entityType && entityType !== "Change") {
|
|
2980
2998
|
const wantClasses = classesForKinds(graph, fwdKinds);
|
|
2981
2999
|
const siblingClass = FINE_CLASS_SIBLING[entityType];
|
|
2982
|
-
|
|
3000
|
+
// An edge-less kind (wantClasses empty) is not a grain mismatch — there
|
|
3001
|
+
// is nothing to name after "only …", and the render leaked a literal
|
|
3002
|
+
// "undefined" there. Fall through to the plain empty-edges answer.
|
|
3003
|
+
if (wantClasses.size && !wantClasses.has(entityType) && !(siblingClass && wantClasses.has(siblingClass))) {
|
|
2983
3004
|
return {
|
|
2984
3005
|
matches: [], objMatch, candidates, ambiguous, matchedVia,
|
|
2985
3006
|
forwardGrainMiss: true, wantClasses: [...wantClasses],
|
|
@@ -3451,13 +3472,48 @@ function renderCore(parsed, result, graph) {
|
|
|
3451
3472
|
if (result.ambiguous) {
|
|
3452
3473
|
// Name the actual candidates in the prose, not just the structured field
|
|
3453
3474
|
// — "narrow the term" isn't actionable if the reader can't see the options.
|
|
3454
|
-
|
|
3475
|
+
let pool = [result.objMatch, ...(result.candidates || [])].filter(Boolean);
|
|
3476
|
+
let branches = result.branches;
|
|
3477
|
+
// A relation question's object slot is a code entity, so a Commit/Session
|
|
3478
|
+
// candidate (a prose-tier hit on a commit MESSAGE) is grain noise in the
|
|
3479
|
+
// did-you-mean — dropped, along with its branch, unless the question is
|
|
3480
|
+
// itself about touches/history where a commit genuinely fits.
|
|
3481
|
+
if (parsed.kind && parsed.kind !== "touches" && !pool.every((i) => i.class === "Commit")) {
|
|
3482
|
+
const grainOk = (i) => !["Commit", "Session", "Source", "Utterance"].includes(i?.class);
|
|
3483
|
+
const kept = pool.filter(grainOk);
|
|
3484
|
+
if (kept.length) {
|
|
3485
|
+
pool = kept;
|
|
3486
|
+
if (branches?.length) branches = branches.filter((b) => grainOk(b.candidate));
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
// The nearest REAL neighbour joins the list: a misremembered symbol
|
|
3490
|
+
// ("saveTask") shares an identifier word with the real one ("saveStore"),
|
|
3491
|
+
// which no containment/prose tier ever surfaces. Ranked by shared
|
|
3492
|
+
// camelCase-split words (≥3 chars), edit distance breaking ties.
|
|
3493
|
+
if (graph && !/[/.]/.test(String(parsed.object || ""))) {
|
|
3494
|
+
const tLc = String(parsed.object || "").toLowerCase();
|
|
3495
|
+
const termWords = new Set(splitIdentifierWords(String(parsed.object || "")).filter((w) => w.length >= 3));
|
|
3496
|
+
const already = new Set(pool.map((i) => i.id));
|
|
3497
|
+
let nearest = null;
|
|
3498
|
+
let bestShared = 0;
|
|
3499
|
+
let bestD = Infinity;
|
|
3500
|
+
if (termWords.size) {
|
|
3501
|
+
for (const i of graph.individuals) {
|
|
3502
|
+
if (!["Function", "Method", "Class", "GlobalVariable", "Attribute"].includes(i.class) || already.has(i.id)) continue;
|
|
3503
|
+
const shared = splitIdentifierWords(String(i.label)).filter((w) => termWords.has(w)).length;
|
|
3504
|
+
if (!shared || shared < bestShared) continue;
|
|
3505
|
+
const d = editDistance(String(i.label).toLowerCase(), tLc, 8);
|
|
3506
|
+
if (shared > bestShared || d < bestD) { nearest = i; bestShared = shared; bestD = d; }
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (nearest) pool = [...pool, nearest];
|
|
3510
|
+
}
|
|
3455
3511
|
const noun = pool.length && pool.every((i) => i.class === "Commit") ? "commit" : "module";
|
|
3456
3512
|
const shown = pool.slice(0, OVERFLOW_CAP).map((i) => i.label);
|
|
3457
3513
|
const extra = pool.length > OVERFLOW_CAP ? `, …and ${pool.length - OVERFLOW_CAP} more` : "";
|
|
3458
3514
|
const lead = `"${parsed.object}" matches more than one ${noun} ambiguously — did you mean ${listJoin(shown)}${extra}? Try one of those. If you're not sure, narrow it to one name.`;
|
|
3459
|
-
const content = (
|
|
3460
|
-
? `${lead}\n${
|
|
3515
|
+
const content = (branches && branches.length)
|
|
3516
|
+
? `${lead}\n${branches.map((b, i) => `${i + 1}) ${b.candidate.label}: ${b.rendered.content}`).join("\n")}`
|
|
3461
3517
|
: lead;
|
|
3462
3518
|
return {
|
|
3463
3519
|
content, miss: false, ambiguous: true, candidates: pool.map((i) => i.label),
|
package/src/domain/codegraph.mjs
CHANGED
|
@@ -413,7 +413,11 @@ export function renderImpact(graph, ind, { maxDepth = 8 } = {}) {
|
|
|
413
413
|
const tests = dep.tests.length
|
|
414
414
|
? `tests: ${capJoin(dep.tests, IMPACT_TESTS_PER_DEP)}`
|
|
415
415
|
: "tests: none recorded";
|
|
416
|
-
|
|
416
|
+
// Past depth 1 the via verb describes the hop to the dependent's own
|
|
417
|
+
// parent, not an edge to the changed module — saying "imports it" there
|
|
418
|
+
// would claim a direct edge that does not exist.
|
|
419
|
+
const receipt = i === 0 ? `${dep.via} it` : "reaches it through an intermediary";
|
|
420
|
+
lines.push(` - ${dep.label} (${receipt}) — ${tests}`);
|
|
417
421
|
}
|
|
418
422
|
if (level.length > IMPACT_PER_DEPTH) lines.push(` …+${level.length - IMPACT_PER_DEPTH} more at depth ${i + 1}`);
|
|
419
423
|
});
|
|
@@ -425,7 +429,7 @@ export function renderImpact(graph, ind, { maxDepth = 8 } = {}) {
|
|
|
425
429
|
lines.push(
|
|
426
430
|
"warning: partial edge lists (" +
|
|
427
431
|
truncatedStructural.map((t) => `${t.predicate}: ${t.shown}/${t.count}`).join(", ") +
|
|
428
|
-
") — this closure may be missing edges. Cross-check critical results with
|
|
432
|
+
") — this closure may be missing edges. Cross-check critical results with a lexical search (/find <term>).",
|
|
429
433
|
);
|
|
430
434
|
}
|
|
431
435
|
return lines.join("\n");
|
|
@@ -132,7 +132,7 @@ const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(
|
|
|
132
132
|
const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|great|nice|got it|gotcha|sounds good|no worries|no problem)[\s,]+)+(.+)$/i;
|
|
133
133
|
/** Self-orientation lead-in with a delimiter — "just poking around, <Q>",
|
|
134
134
|
* "first time using this, <Q>". */
|
|
135
|
-
const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here))\s*[,.—–-]\s*(.+)$/i;
|
|
135
|
+
const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here)|i'?m\s+new\s+(?:here|around\s+here|to\s+(?:this|all\s+this)(?:\s+(?:repo|codebase|project|app|tool|thing))?))\s*[,.—–-]\s*(.+)$/i;
|
|
136
136
|
/** Repeated leading hedge adverb before a polite request verb ("maybe
|
|
137
137
|
* possibly tell me <Q>"). No delimiter required, unlike ACK_PREAMBLE_RE. */
|
|
138
138
|
const HEDGE_ADVERB_PREAMBLE_RE = /^(?:(?:maybe|possibly|perhaps)\s+)+(.+)$/i;
|
|
@@ -41,6 +41,7 @@ export const PRECOND = Object.freeze({
|
|
|
41
41
|
graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
|
|
42
42
|
resolves: "cap:resolves", // { param, as } — the slot binds to an entity of kind `as`
|
|
43
43
|
anyPresent: "cap:any-present", // { params } — at least one of these slots is provided
|
|
44
|
+
memoryFacts: "cap:memory-facts", // the conversational-memory store holds relation facts for the term
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
// ---- capability builder (returns PLAIN FROZEN data) -------------------------
|
|
@@ -57,6 +58,11 @@ const resolves = (paramName, as) =>
|
|
|
57
58
|
/** any-present(params) — search-style disjunction (query OR kind must be given). */
|
|
58
59
|
const anyPresent = (params) =>
|
|
59
60
|
Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
|
|
61
|
+
/** memory-facts — the memory graph holds mgx:synonym / mgx:relatedTo / mgx:similarTo
|
|
62
|
+
* facts for the term. The memory-graph sibling of graph-loaded: the SKOS view
|
|
63
|
+
* answers from the conversational-memory store and misses honestly without it,
|
|
64
|
+
* with or without a code-map graph. */
|
|
65
|
+
const memoryFacts = () => Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.memoryFacts });
|
|
60
66
|
|
|
61
67
|
/** Add-effect: after the call the agent knows `topic` about `?of`. */
|
|
62
68
|
const knows = (topic, ofParam = null) =>
|
|
@@ -80,7 +86,8 @@ function capability({ name, label, question, params = [], preconditions = [], ad
|
|
|
80
86
|
// Arg keys verified against src/tools/server.mjs `dispatchTool`'s switch: describe/callers/
|
|
81
87
|
// callees/tests/history/… take `symbol`; impact/exports take `module`; members/
|
|
82
88
|
// subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
|
|
83
|
-
// architecture takes an optional `package`; untested takes nothing
|
|
89
|
+
// architecture takes an optional `package`; untested takes nothing; related
|
|
90
|
+
// takes `term` (a memory-graph concept term).
|
|
84
91
|
|
|
85
92
|
const CAPABILITIES = Object.freeze([
|
|
86
93
|
capability({
|
|
@@ -107,9 +114,9 @@ const CAPABILITIES = Object.freeze([
|
|
|
107
114
|
add: [knows("signature", "symbol")],
|
|
108
115
|
}),
|
|
109
116
|
capability({
|
|
110
|
-
name: "tmct_impact", label: "impact", question: "what a change to this module reaches (impact closure)",
|
|
111
|
-
params: [param("module", KINDS.Module)],
|
|
112
|
-
preconditions: [graphLoaded(), resolves("module", KINDS.
|
|
117
|
+
name: "tmct_impact", label: "impact", question: "what a change to this module or symbol reaches (impact closure)",
|
|
118
|
+
params: [param("module", KINDS.Symbol, { note: "a Module, or any sited symbol — a fine-grained seed walks callsSymbol dependents and coarsens them to module grain" })],
|
|
119
|
+
preconditions: [graphLoaded(), resolves("module", KINDS.Symbol)],
|
|
113
120
|
add: [knows("impact", "module")],
|
|
114
121
|
}),
|
|
115
122
|
capability({
|
|
@@ -178,6 +185,12 @@ const CAPABILITIES = Object.freeze([
|
|
|
178
185
|
preconditions: [graphLoaded()],
|
|
179
186
|
add: [knows("architecture", "package")],
|
|
180
187
|
}),
|
|
188
|
+
capability({
|
|
189
|
+
name: "tmct_related", label: "related", question: "a term's synonyms and related concepts (the SKOS view over the conversational-memory graph)",
|
|
190
|
+
params: [param("term", KINDS.Query, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
|
|
191
|
+
preconditions: [memoryFacts()],
|
|
192
|
+
add: [knows("related", "term")],
|
|
193
|
+
}),
|
|
181
194
|
]);
|
|
182
195
|
|
|
183
196
|
// The live capability set: the built-in frozen array is the seed; registration
|
|
@@ -56,8 +56,10 @@ export const UNMAPPED_KINDS = Object.freeze({
|
|
|
56
56
|
|
|
57
57
|
// ---- capabilities the NL surface cannot reach today (named, not accidental) ---
|
|
58
58
|
// A declared capability with no NL/command/frame path is a routing gap and must be tagged
|
|
59
|
-
// here with the reason.
|
|
60
|
-
export const NOT_NL_REACHABLE = Object.freeze({
|
|
59
|
+
// here with the reason.
|
|
60
|
+
export const NOT_NL_REACHABLE = Object.freeze({
|
|
61
|
+
tmct_related: "the SKOS synonym/related surface is served by the chat lane's own recogniser over the memory graph; a router frame for it needs memory-term binding, which resolveObject (code-graph-only) does not prove yet",
|
|
62
|
+
});
|
|
61
63
|
|
|
62
64
|
// ---- imperative intent FRAMES (fills what the relational grammar and command register
|
|
63
65
|
// both miss). regex -> { topic, arg | noArg }. Ordered: first match wins.
|