@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.
@@ -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.1",
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.",
@@ -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
- if (!metric && connectivity) { metric = EDGE_NOUN_TO_METRIC.connections; metricNoun = "connections"; }
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: {
@@ -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 edges = (factRows || []).map(normRow).filter((r) => MEMBER_EDGE_PREDICATES.has(r.predicate));
114
- const hasIncoming = new Set(edges.map((r) => r.object));
115
- const classMembers = {};
116
- for (const edge of edges) {
117
- if (hasIncoming.has(edge.subject)) continue;
118
- (classMembers[edge.object] ??= []).push(edge.subject);
119
- }
120
- for (const members of Object.values(classMembers)) {
121
- members.sort();
122
- // de-dup while keeping order
123
- for (let i = members.length - 1; i > 0; i -= 1) if (members[i] === members[i - 1]) members.splice(i, 1);
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
- export function movesFromRules(state, domain, { budget = 5000 } = {}) {
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.classMembers[sig.subjectClass] || []).length *
321
- (domain.classMembers[sig.targetClass] || []).length;
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.classMembers[sig.subjectClass] || []) {
331
- for (const target of domain.classMembers[sig.targetClass] || []) {
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.classMembers[spec.term] || [] : [spec.term];
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. Pure. */
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
  }