@polycode-projects/the-mechanical-code-talker 2.10.1 → 2.10.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/data/templates/responses.jsonl +1 -0
- package/package.json +1 -1
- package/src/domain/ask.mjs +26 -1
- package/src/domain/domain.mjs +49 -21
- package/src/services/adventure.mjs +73 -1
- package/src/services/chat.mjs +283 -26
- package/src/services/spider-fly-turn.mjs +65 -0
- package/src/surfaces/web/memory-ask-browser.bundle.js +113 -108
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
{"id":"conversational-greeting-good-evening","class":"conversational","register":"friendly","template":"Good evening. Ask me about this codebase, or /help."}
|
|
61
61
|
{"id":"conversational-thanks","class":"conversational","register":"friendly","template":"Any time. Ask another, or /help for what I can do."}
|
|
62
62
|
{"id":"conversational-farewell","class":"conversational","register":"friendly","template":"Bye — flushing the session log. Come back with a question any time."}
|
|
63
|
+
{"id":"conversational-dismissal","class":"conversational","register":"friendly","template":"No worries. Ask another, or /help for what I can do."}
|
|
63
64
|
{"id":"orientation-friendly","class":"orientation","register":"friendly","template":"I'm tmct — a deterministic, offline code-graph assistant (no LLM). I answer questions about THIS codebase's structure — imports, calls, definitions,\nhistory and counts. For example:\n which modules import {example1}\n what calls {example2}\n how many classes are there\n/help for commands, /stats for an overview of the graph."}
|
|
64
65
|
{"id":"miss-no-previous-answer","class":"miss","register":"friendly","template":"No previous answer to expand yet — ask me a question first, then say \"why\" or \"say more\"."}
|
|
65
66
|
{"id":"conversational-greeting-empty","class":"conversational","register":"friendly","template":"Hi. I'm tmct. {vocabHint} Point me at a repo with `--repo <path>` for code-structure questions too (imports, calls, definitions). /help for commands."}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.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; no codebase index of its own.",
|
package/src/domain/ask.mjs
CHANGED
|
@@ -896,7 +896,12 @@ function parseSuperlative(w, lc, nlp) {
|
|
|
896
896
|
}
|
|
897
897
|
const connectivity = lc.includes("connected") || lc.slice(extIdx, extIdx + 2).join(" ") === "most connected"
|
|
898
898
|
|| ["largest", "biggest", "smallest"].includes(lc[extIdx]);
|
|
899
|
-
|
|
899
|
+
// A bare importance superlative ("the most important file") names no explicit
|
|
900
|
+
// edge metric, so it ranks by total connectivity — the sum of an entity's
|
|
901
|
+
// in/out edges, the most defensible deterministic proxy for "important".
|
|
902
|
+
const IMPORTANCE_WORDS = ["important", "significant", "central", "key", "core", "essential", "critical", "principal"];
|
|
903
|
+
const importanceRanked = lc.some((x) => IMPORTANCE_WORDS.includes(x));
|
|
904
|
+
if (!metric && (connectivity || importanceRanked)) { metric = EDGE_NOUN_TO_METRIC.connections; metricNoun = "connections"; }
|
|
900
905
|
// entity noun anywhere (first match, deterministic); else default from a
|
|
901
906
|
// metric that implies exactly one entity class ("test(s)" always ranks
|
|
902
907
|
// Modules, the one declared exception — see METRIC_IMPLIES_ENTITY — so "what
|
|
@@ -4205,6 +4210,26 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
4205
4210
|
content = `${content}\n${lines.join("\n")}`;
|
|
4206
4211
|
}
|
|
4207
4212
|
}
|
|
4213
|
+
// NARROWING DISCLOSURE: the resolver picked ONE entity among several distinct
|
|
4214
|
+
// name-matches (a scored win, not a tie — a tie renders as branches above and
|
|
4215
|
+
// is excluded here). Whether the pick then produced an answer or an empty
|
|
4216
|
+
// result, disclose it and the count of the other matches in one line, so a
|
|
4217
|
+
// silent narrowing (a merged graph's src/store.mjs over src/core/store.mjs, a
|
|
4218
|
+
// directory term landing on one module, a wrong-case pick reporting no members)
|
|
4219
|
+
// is never mistaken for the only reading.
|
|
4220
|
+
if (!rendered.ambiguous && result.objMatch && Array.isArray(result.candidates) && result.candidates.length) {
|
|
4221
|
+
// A ranked shape (entry-point, superlative) carries its runners-up in
|
|
4222
|
+
// `matches` and discloses them its own way; a NAME-narrowing carries the
|
|
4223
|
+
// narrowed-away candidates OUTSIDE `matches`. Only the latter is disclosed.
|
|
4224
|
+
const matchIds = new Set((result.matches || []).map((m) => m && m.id));
|
|
4225
|
+
const others = result.candidates.filter((c) => c && c.id && c.id !== result.objMatch.id && !matchIds.has(c.id));
|
|
4226
|
+
if (others.length) {
|
|
4227
|
+
const shown = others.slice(0, 3).map((c) => c.label).filter(Boolean);
|
|
4228
|
+
const more = others.length - shown.length;
|
|
4229
|
+
const list = shown.join(", ") + (more > 0 ? `, +${more} more` : "");
|
|
4230
|
+
content = `${content}\n(answering for ${result.objMatch.label} — ${others.length} other match${others.length === 1 ? "" : "es"}: ${list})`;
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4208
4233
|
return {
|
|
4209
4234
|
content,
|
|
4210
4235
|
tmct_ask: {
|
package/src/domain/domain.mjs
CHANGED
|
@@ -5,9 +5,18 @@
|
|
|
5
5
|
// arrives as data from the memory store's fact/Rule rows. Plugs into
|
|
6
6
|
// planning.mjs's findActionPath as its applyActions.
|
|
7
7
|
|
|
8
|
+
import { provenanceTagToSource } from "./memory/trust.mjs";
|
|
9
|
+
|
|
8
10
|
const MEMBER_EDGE_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
9
11
|
const SNAPSHOT_RE = /^(.+)@step(\d+)$/;
|
|
10
12
|
|
|
13
|
+
/** The class-member list a grounding step ranges over. The default "all" is
|
|
14
|
+
* every typed member; "taught" restricts to instances a person taught, the
|
|
15
|
+
* scope the plan lane uses so a merged corpus never widens a quantified goal
|
|
16
|
+
* or the movable set. Falls back to nothing for an unknown scope. */
|
|
17
|
+
const membersInScope = (domain, cls, scope) =>
|
|
18
|
+
(scope === "taught" ? domain.taughtClassMembers : domain.classMembers)?.[cls] || [];
|
|
19
|
+
|
|
11
20
|
/** Trim a taught term defensively: some teach frames keep a sentence's
|
|
12
21
|
* trailing punctuation in the captured object. */
|
|
13
22
|
const normTerm = (value) => String(value ?? "").trim().replace(/[.!?]+$/, "");
|
|
@@ -110,18 +119,34 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
110
119
|
|
|
111
120
|
// Class membership from typing edges. A member is a subject with a typing
|
|
112
121
|
// edge into the class and no typing edge pointing at itself (a leaf).
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
const membersFromEdges = (typingEdges) => {
|
|
123
|
+
const hasIncoming = new Set(typingEdges.map((r) => r.object));
|
|
124
|
+
const members = {};
|
|
125
|
+
for (const edge of typingEdges) {
|
|
126
|
+
if (hasIncoming.has(edge.subject)) continue;
|
|
127
|
+
(members[edge.object] ??= []).push(edge.subject);
|
|
128
|
+
}
|
|
129
|
+
for (const list of Object.values(members)) {
|
|
130
|
+
list.sort();
|
|
131
|
+
// de-dup while keeping order
|
|
132
|
+
for (let i = list.length - 1; i > 0; i -= 1) if (list[i] === list[i - 1]) list.splice(i, 1);
|
|
133
|
+
}
|
|
134
|
+
return members;
|
|
135
|
+
};
|
|
136
|
+
const typingRows = (factRows || []).filter((r) => MEMBER_EDGE_PREDICATES.has(normTerm(r.predicate)));
|
|
137
|
+
const edges = typingRows.map(normRow);
|
|
138
|
+
const classMembers = membersFromEdges(edges);
|
|
139
|
+
// The same membership, restricted to instances a person taught (an operator
|
|
140
|
+
// assert or a teach-lane frame). A planner that quantifies over a class —
|
|
141
|
+
// "every X" — ranges over the taught individuals only: a merged corpus can
|
|
142
|
+
// type dozens of unrelated things into that class, and sweeping those into
|
|
143
|
+
// the goal makes it unsatisfiable and the search fruitless. classMembers
|
|
144
|
+
// keeps every member for the readers that want them all.
|
|
145
|
+
const taughtEdges = edges.filter((_, i) => {
|
|
146
|
+
const kind = provenanceTagToSource(typingRows[i].provenance)?.kind;
|
|
147
|
+
return kind === "operator" || kind === "teach";
|
|
148
|
+
});
|
|
149
|
+
const taughtClassMembers = membersFromEdges(taughtEdges);
|
|
125
150
|
|
|
126
151
|
// A class-bound word (an effect role or constraint term that is neither
|
|
127
152
|
// "subject" nor "target") is substituted by its class's sole member at
|
|
@@ -158,7 +183,7 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
158
183
|
.filter((r) => !dynamicPredicates.has(r.predicate) && !MEMBER_EDGE_PREDICATES.has(r.predicate))
|
|
159
184
|
.sort(rowSort);
|
|
160
185
|
|
|
161
|
-
return { actions, classMembers, dynamicPredicates, ordering };
|
|
186
|
+
return { actions, classMembers, taughtClassMembers, dynamicPredicates, ordering };
|
|
162
187
|
}
|
|
163
188
|
|
|
164
189
|
const domainIndividuals = (domain) => {
|
|
@@ -312,13 +337,16 @@ function constraintViolated(action, nextState, domain) {
|
|
|
312
337
|
}
|
|
313
338
|
|
|
314
339
|
/** Every legal grounded action from `state`, with its successor.
|
|
315
|
-
* Deterministic: actions, signatures, and members are walked sorted.
|
|
316
|
-
|
|
340
|
+
* Deterministic: actions, signatures, and members are walked sorted. `scope`
|
|
341
|
+
* ("all" | "taught") picks which class members ground the moves — the plan
|
|
342
|
+
* lane passes "taught" so corpus members of a taught class never enter the
|
|
343
|
+
* movable set. */
|
|
344
|
+
export function movesFromRules(state, domain, { budget = 5000, scope = "all" } = {}) {
|
|
317
345
|
let groundings = 0;
|
|
318
346
|
for (const action of domain.actions) {
|
|
319
347
|
for (const sig of action.signatures) {
|
|
320
|
-
groundings += (domain
|
|
321
|
-
(domain
|
|
348
|
+
groundings += membersInScope(domain, sig.subjectClass, scope).length *
|
|
349
|
+
membersInScope(domain, sig.targetClass, scope).length;
|
|
322
350
|
}
|
|
323
351
|
}
|
|
324
352
|
if (groundings > budget) throw new PlanBudgetError(groundings, budget);
|
|
@@ -327,8 +355,8 @@ export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
|
327
355
|
for (const action of domain.actions) {
|
|
328
356
|
const [verb, particle] = action.name.split(/\s+/);
|
|
329
357
|
for (const sig of action.signatures) {
|
|
330
|
-
for (const subject of domain
|
|
331
|
-
for (const target of domain
|
|
358
|
+
for (const subject of membersInScope(domain, sig.subjectClass, scope)) {
|
|
359
|
+
for (const target of membersInScope(domain, sig.targetClass, scope)) {
|
|
332
360
|
if (subject === target) continue;
|
|
333
361
|
let ok = true;
|
|
334
362
|
for (const precond of action.preconds) {
|
|
@@ -360,7 +388,7 @@ export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
|
360
388
|
* state predicate. Satisfaction is a transitive walk along the goal
|
|
361
389
|
* predicate: a stacked member reaches the goal object through its support
|
|
362
390
|
* chain, which a direct row lookup cannot see. */
|
|
363
|
-
export function compileGoal(goalSpecs, domain) {
|
|
391
|
+
export function compileGoal(goalSpecs, domain, { scope = "all" } = {}) {
|
|
364
392
|
const specs = (goalSpecs || []).map((g) => ({
|
|
365
393
|
universal: Boolean(g.universal),
|
|
366
394
|
term: normTerm(g.term),
|
|
@@ -369,7 +397,7 @@ export function compileGoal(goalSpecs, domain) {
|
|
|
369
397
|
}));
|
|
370
398
|
const checks = [];
|
|
371
399
|
for (const spec of specs) {
|
|
372
|
-
const members = spec.universal ? domain
|
|
400
|
+
const members = spec.universal ? membersInScope(domain, spec.term, scope) : [spec.term];
|
|
373
401
|
if (spec.universal && members.length === 0) {
|
|
374
402
|
throw new Error(`the goal names "${spec.term}" as a class, but it has no known members`);
|
|
375
403
|
}
|
|
@@ -408,9 +408,21 @@ const BACKGROUND_FACT_PHRASES = {
|
|
|
408
408
|
"mgx:atLocation": "is found in",
|
|
409
409
|
};
|
|
410
410
|
|
|
411
|
+
/** True when a fact row is provably owned by a NON-world source (a merged
|
|
412
|
+
* corpus, a reference pack, a taught assert) rather than the loaded world. A
|
|
413
|
+
* row with no provenance is not "non-world" — a hand-built digest view carries
|
|
414
|
+
* none, and the room-look filter keeps those. World facts and their @turn
|
|
415
|
+
* snapshots tag as `world:<name>[:turnN]`. */
|
|
416
|
+
function isNonWorldSourced(row) {
|
|
417
|
+
const prov = String(row.provenance || "").trim();
|
|
418
|
+
return prov !== "" && !prov.startsWith("world:");
|
|
419
|
+
}
|
|
420
|
+
|
|
411
421
|
/** The digest's fact view: current placements (folded), exits, typing and
|
|
412
422
|
* every other surviving fact, with phrase predicates and sentence-cased
|
|
413
|
-
* subjects so the pipeline's sentence splitter sees real sentences.
|
|
423
|
+
* subjects so the pipeline's sentence splitter sees real sentences. Room text
|
|
424
|
+
* is world-sourced only — a merged corpus's overlap on a room's own vocabulary
|
|
425
|
+
* never leaks into the description. Pure. */
|
|
414
426
|
export function worldDigestRows(rows, state) {
|
|
415
427
|
const out = [];
|
|
416
428
|
const seen = new Set();
|
|
@@ -440,6 +452,12 @@ export function worldDigestRows(rows, state) {
|
|
|
440
452
|
}
|
|
441
453
|
for (const row of rows || []) {
|
|
442
454
|
if (SNAPSHOT_RE.test(row.subject)) continue; // folded above
|
|
455
|
+
// Room text comes from the world source only. A merged corpus overlaps a
|
|
456
|
+
// room's own vocabulary ("library rdfs:subClassOf literary study"), and
|
|
457
|
+
// without this those rows leak into the room description as stray sentences.
|
|
458
|
+
// A row with no provenance (a hand-built test view) is kept — the filter
|
|
459
|
+
// only drops rows a non-world source provably owns.
|
|
460
|
+
if (isNonWorldSourced(row)) continue;
|
|
443
461
|
if (PLACEMENT_PREDICATES.has(row.predicate)) continue; // folded above
|
|
444
462
|
if (VIEW_EXCLUDED_PREDICATES.has(row.predicate)) continue;
|
|
445
463
|
const exit = EXIT_PREDICATE_RE.exec(row.predicate);
|
|
@@ -909,6 +927,58 @@ async function worldOpennessAnswer(line, { memoryDir }) {
|
|
|
909
927
|
);
|
|
910
928
|
}
|
|
911
929
|
|
|
930
|
+
// The in-game orientation asides — "where am I", "what can I do", "what is the
|
|
931
|
+
// quest/goal". Without a world-state answer these fall through to the ordinary
|
|
932
|
+
// lanes and misroute: "where am I" reads "I" as a module name, "what can I do"
|
|
933
|
+
// walls, and "what is the goal" answers from corpus vocabulary about the word
|
|
934
|
+
// "goal". A live world answers each from its own fold first.
|
|
935
|
+
const WORLD_WHERE_AM_I_RE = /^where\s+am\s+i(?:\s+now)?[?.!\s]*$/i;
|
|
936
|
+
const WORLD_OPTIONS_RE = /^(?:what\s+can\s+i\s+do(?:\s+(?:here|now))?|what\s+are\s+my\s+options|what\s+(?:should|do)\s+i\s+do(?:\s+(?:here|now))?|what\s+now)[?.!\s]*$/i;
|
|
937
|
+
const WORLD_QUEST_RE = /^(?:what(?:'s|\s+is)\s+(?:the\s+|my\s+)?(?:quest|goal|objective|mission|aim)|what\s+am\s+i\s+(?:trying\s+to\s+do|(?:supposed|meant)\s+to\s+do)|what\s+do\s+i\s+do\s+here)[?.!\s]*$/i;
|
|
938
|
+
|
|
939
|
+
/** The in-game orientation asides, answered from the world fold: the player's
|
|
940
|
+
* room, the room's real affordances, and the world's objective. Null when the
|
|
941
|
+
* line is none of them, so an ordinary question keeps its lane. */
|
|
942
|
+
async function worldContextAnswer(line, { memoryDir }) {
|
|
943
|
+
const l = String(line).trim();
|
|
944
|
+
const asksWhere = WORLD_WHERE_AM_I_RE.test(l);
|
|
945
|
+
const asksOptions = WORLD_OPTIONS_RE.test(l);
|
|
946
|
+
const asksQuest = WORLD_QUEST_RE.test(l);
|
|
947
|
+
if (!asksWhere && !asksOptions && !asksQuest) return null;
|
|
948
|
+
let rows;
|
|
949
|
+
try { rows = readFactRows(await loadMemory(memoryDir)); } catch { return null; }
|
|
950
|
+
const state = foldWorldState(rows);
|
|
951
|
+
const here = state.placements.get("player")?.object ?? null;
|
|
952
|
+
|
|
953
|
+
if (asksWhere) {
|
|
954
|
+
return here
|
|
955
|
+
? answer(`you are in the ${here}.`, "ADVENTURE — where-am-I aside: the player's own room from the current world fold", { goal: "check where you are" })
|
|
956
|
+
: answer("the world has no written player position yet.", "ADVENTURE — where-am-I aside: no player placement", { miss: true, goal: "check where you are" });
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
if (asksOptions) {
|
|
960
|
+
const actions = here ? roomAffordances(rows, state, here) : [];
|
|
961
|
+
return answer(
|
|
962
|
+
actions.length ? `you can: ${actions.join(", ")}.` : `nothing obvious here — say "look" to look around${here ? ` the ${here}` : ""}.`,
|
|
963
|
+
`ADVENTURE — options aside: the ${here}'s roomAffordances, the same list "look" appends`,
|
|
964
|
+
{ goal: "see what you can do here" },
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const objectiveId = rows.find((r) => r.predicate === "mgx:is-objective" && r.object === "true")?.subject ?? null;
|
|
969
|
+
return objectiveId
|
|
970
|
+
? answer(
|
|
971
|
+
`your goal is to find the ${objectiveId} and pick it up.`,
|
|
972
|
+
`ADVENTURE — quest aside: the world's objective (${objectiveId}), named without spoiling where it is`,
|
|
973
|
+
{ goal: `find the ${objectiveId}` },
|
|
974
|
+
)
|
|
975
|
+
: answer(
|
|
976
|
+
`this world sets no explicit goal — explore it, and say "look" to see your options.`,
|
|
977
|
+
"ADVENTURE — quest aside: no objective marker in this world",
|
|
978
|
+
{ goal: "explore the world" },
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
912
982
|
async function inventoryAnswer({ memoryDir, graph }) {
|
|
913
983
|
const memory = await loadMemory(memoryDir);
|
|
914
984
|
const rows = readFactRows(memory);
|
|
@@ -1033,5 +1103,7 @@ export async function adventureTurn(line, { planHolder, memoryDir, sessionId = "
|
|
|
1033
1103
|
if (whereAside) return whereAside;
|
|
1034
1104
|
const opennessAside = await worldOpennessAnswer(line, { memoryDir });
|
|
1035
1105
|
if (opennessAside) return opennessAside;
|
|
1106
|
+
const contextAside = await worldContextAnswer(line, { memoryDir });
|
|
1107
|
+
if (contextAside) return contextAside;
|
|
1036
1108
|
return null; // a mid-game aside — the ordinary lanes answer, world untouched
|
|
1037
1109
|
}
|