@polycode-projects/the-mechanical-code-talker 1.10.6 → 1.10.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polycode-projects/the-mechanical-code-talker",
3
- "version": "1.10.6",
3
+ "version": "1.10.8",
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/chat.mjs CHANGED
@@ -81,7 +81,7 @@ function note(trace, text) { if (trace) trace.push(text); }
81
81
 
82
82
  /** relation `kind` (ask-vocab.mjs RELATIONS) -> a short, deterministic
83
83
  * statement of what a person asking that KIND of question is probably after.
84
- * Deliberately a small, honest bucket lookup over the query SHAPE the engine
84
+ * A small bucket lookup over the query SHAPE the engine
85
85
  * already computed — tmct is no-LLM, so goal deduction is table-driven, never
86
86
  * free-text generation. A kind/shape this table doesn't recognise falls
87
87
  * through to a generic line in deduceGoalFromParsed, never a fabricated guess. */
@@ -417,8 +417,7 @@ const ANAPHORA_COUNT_RE = /\b(?:how many|how much|count|number of)\s+(?:of\s+)?(
417
417
  * filter on) is untouched. */
418
418
  const IMPLICIT_ANAPHORA_COUNT_RE = /^(?:(?:and|so|then|also)\s+)?how many (?:are|is|were|was)\s+(?!there\b)(\S.*)$/i;
419
419
 
420
- /** "have"/"has"/"holds"/"hold" are excluded from RESTRICTOR_VERB_RE below
421
- * deliberately treated as non-restrictor cues, not a bug fix skipped.
420
+ /** "have"/"has"/"holds"/"hold" are excluded from RESTRICTOR_VERB_RE below.
422
421
  * Ask-vocab's VERB_TO_KIND maps them to "defines" unconditionally, but the
423
422
  * graph's actual "have" semantics are subject-type-dependent (a Module "has"
424
423
  * things it defines; a Class "has" things it contains) — ask.mjs's own
@@ -797,8 +796,8 @@ const AI_IDENTITY_PHRASES = [
797
796
  * two-sentence turn like "are you an AI? like chatgpt?" could never match
798
797
  * the whole raw string even though its first clause alone is an exact "are
799
798
  * you an AI" hit. Used ONLY by aiIdentityMatch below — every OTHER
800
- * closed-set match in this file stays whole-string, on purpose (deliberately
801
- * narrow to the one family that's shown up broken this way). */
799
+ * closed-set match in this file stays whole-string (scoped to the one
800
+ * family that's shown up broken this way). */
802
801
  function splitClauses(text) {
803
802
  return String(text).split(/[?!.]+\s*/).map((c) => c.trim()).filter(Boolean);
804
803
  }
@@ -1848,11 +1847,10 @@ const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|a\s+|an\s+)?([\w-]+(?:\s+[
1848
1847
  const MINT_ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
1849
1848
 
1850
1849
  /** Small CLOSED set of generic English root nouns that count as always-
1851
- * grounded anchor terms for the mint-fallbacks below (operator refinement,
1852
- * 2026-07-09) deliberately NOT added to lexicon-core.json itself (that
1853
- * file stays the curated ~180-word CODE vocabulary; these are ordinary-
1854
- * English root nouns with no code meaning at all, confirmed absent from it
1855
- * today). Their only job is to give a user who hits the "both sides
1850
+ * grounded anchor terms for the mint-fallbacks below not in
1851
+ * lexicon-core.json (these are ordinary-English root nouns with no code
1852
+ * meaning, absent from that code vocabulary). Their only job is to give a
1853
+ * user who hits the "both sides
1856
1854
  * ungrounded" decline (groundingSuggestionMiss, below) an honest, guessable
1857
1855
  * way in: ground one brand-new term via one of THESE words first ("every
1858
1856
  * zorp is a thing"), then chain the other new term off the now-grounded one. */
@@ -2314,6 +2312,11 @@ const GENERAL_VERB_NOT_A_VERB_RE = new RegExp(
2314
2312
  async function generalVerbPredicate(verb) {
2315
2313
  const v = String(verb || "").toLowerCase();
2316
2314
  if (v === "has" || v === "have") return HAS_A_PREDICATE;
2315
+ // The modal maps onto the corpus's own capability predicate — "dog can
2316
+ // swim" is a capability claim, not a transitive "to can" — so a taught
2317
+ // capability reads back interoperably with /r/CapableOf data and the
2318
+ // "can a X <verb>" reader finds it (same reasoning as HAS_A above).
2319
+ if (v === "can") return "mgx:capableOf";
2317
2320
  try {
2318
2321
  const { proseLemma } = await import("./prose-nlp.mjs");
2319
2322
  const lemma = proseLemma();
@@ -2345,6 +2348,10 @@ async function generalVerbTeach(payload) {
2345
2348
  const verb = verbRaw.toLowerCase();
2346
2349
  if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
2347
2350
  if (GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null; // a closed-class word can never be the real verb
2351
+ // "cannot" would mint a nonsense mgx:cannot fact whose read-back silently
2352
+ // INVERTS the taught meaning — the vocabulary has no negative-capability
2353
+ // predicate, so an honest decline is the only correct move.
2354
+ if (verb === "cannot") return null;
2348
2355
  if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
2349
2356
  const subject = subjectRaw.trim();
2350
2357
  const object = objectRaw.replace(/^an?\s+/i, "").trim();
@@ -4159,6 +4166,42 @@ const REVERSE_PREDICATE_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
4159
4166
  re: new RegExp(`^what\\s+${escapeRegex(phrase)}\\s+(.+?)[?.!\\s]*$`, "i"),
4160
4167
  }))
4161
4168
  .sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
4169
+
4170
+ // The FORWARD yes/no mirror of REVERSE_PREDICATE_MARKERS: one derived
4171
+ // "is/are X <phrase> Y" (copula phrases), "can X be Y" (receivesAction), or
4172
+ // "does/do X <base-verb> Y" (verb phrases, naive de-3sg fold) reader per
4173
+ // FACT_PREDICATE_PHRASES entry — so every relation the table can RENDER can
4174
+ // also be ASKED as a forward yes/no, instead of each one needing its own
4175
+ // hand-written lane. Excluded: the isa family and hasProperty (ISA_ASK_RE /
4176
+ // IS_ADJECTIVE_YESNO_RE territory), hasA and capableOf (their dedicated
4177
+ // readers above carry teach hints these derived ones deliberately don't —
4178
+ // no derived hint is emitted because no teach phrasing for these relations
4179
+ // is verified to round-trip).
4180
+ const FORWARD_YESNO_EXCLUDE = new Set([
4181
+ "rdfs:subClassOf", "rdf:type", "owl:disjointWith", "mgx:hasProperty",
4182
+ "mgx:hasA", "mgx:capableOf",
4183
+ // ownership's dedicated reader (OWNS_YESNO_RE) answers a confident
4184
+ // closed-world "no" — a stronger contract than the derived "can't
4185
+ // confirm", so the derived reader must never intercept it.
4186
+ "mgx:ownedBy",
4187
+ ]);
4188
+ const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
4189
+ .filter(([predicate]) => !FORWARD_YESNO_EXCLUDE.has(predicate))
4190
+ .map(([predicate, phrase]) => {
4191
+ let re;
4192
+ if (phrase === "can be") {
4193
+ re = new RegExp("^can\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+be\\s+(.+?)[?.!\\s]*$", "i");
4194
+ } else if (phrase.startsWith("is ")) {
4195
+ const rest = escapeRegex(phrase.slice(3));
4196
+ re = new RegExp(`^(?:is|are)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${rest}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
4197
+ } else {
4198
+ const [head, ...tail] = phrase.split(" ");
4199
+ const base = [head.replace(/s$/, ""), ...tail].map(escapeRegex).join("\\s+");
4200
+ re = new RegExp(`^(?:does|do)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${base}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
4201
+ }
4202
+ return { predicate, phrase, re };
4203
+ })
4204
+ .sort((a, b) => b.re.source.length - a.re.source.length); // longest phrase first
4162
4205
  // On the FIRST turn of a graph-less session, `envelope` stays null for the
4163
4206
  // whole turn (dispatchTool's loadGraph() throws its own documented empty-graph
4164
4207
  // ToolError, self-correcting from turn 2 on), so this regex is the ONLY path
@@ -4323,6 +4366,42 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
4323
4366
  }
4324
4367
  if (!miss) return null;
4325
4368
 
4369
+ // (b0) Derived forward yes/no readers — FORWARD_YESNO_MARKERS, one per
4370
+ // renderable relation. Runs BEFORE the isa lane because ISA_ASK_RE's lazy
4371
+ // subject otherwise swallows these shapes whole ("is a wheel part of a
4372
+ // car" reads as subject "wheel part of") and ends the cascade. A real fact
4373
+ // answers yes; a subject known under the SAME relation gets an honest miss
4374
+ // citing those facts; a subject known at all (with no structural parse
4375
+ // standing) gets a bare honest miss; anything else leaves the standing
4376
+ // miss text alone — so a code-shaped query with a real parse is never
4377
+ // hijacked.
4378
+ for (const { predicate, phrase, re } of FORWARD_YESNO_MARKERS) {
4379
+ const m = q.match(re);
4380
+ if (!m) continue;
4381
+ const facts = await memoryFacts(memoryDir);
4382
+ const subj = factTermVariants(normFactTerm, m[1]);
4383
+ const obj = factTermVariants(normFactTerm, m[2]);
4384
+ const hit = facts.find((f) => f.predicate === predicate && subj.has(f.subject) && obj.has(f.object));
4385
+ if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
4386
+ const sameRelation = facts.filter((f) => f.predicate === predicate && subj.has(f.subject));
4387
+ if (sameRelation.length) {
4388
+ const shown = sameRelation.slice(0, 3).map(renderFactLine).join("; ");
4389
+ return {
4390
+ text: `I can't confirm that — nothing I remember says ${m[1]} ${phrase} ${m[2]}. I do know: ${shown}.`,
4391
+ replace: true,
4392
+ miss: true,
4393
+ };
4394
+ }
4395
+ if (!envelope?.parsed && facts.some((f) => subj.has(f.subject))) {
4396
+ return {
4397
+ text: `I can't confirm that — nothing I remember says ${m[1]} ${phrase} ${m[2]}.`,
4398
+ replace: true,
4399
+ miss: true,
4400
+ };
4401
+ }
4402
+ break; // shape matched, nothing honest to add — the standing miss stands
4403
+ }
4404
+
4326
4405
  // (b) "is a module a component" — yes iff a remembered isa-family fact says so.
4327
4406
  // Also accepts "why is X a Y" / "explain how you know X is Y" — see matchWhyIsa.
4328
4407
  const isa = q.match(ISA_ASK_RE) || matchWhyIsa(q);
@@ -4341,12 +4420,27 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
4341
4420
  // single-hit lookup, same "never a guessed no" discipline).
4342
4421
  const can = q.match(CAN_ASK_RE);
4343
4422
  if (can) {
4423
+ const facts = await memoryFacts(memoryDir);
4344
4424
  const subj = factTermVariants(normFactTerm, can[1]);
4345
4425
  const obj = factTermVariants(normFactTerm, can[2]);
4346
- const hit = (await memoryFacts(memoryDir)).find(
4426
+ const hit = facts.find(
4347
4427
  (f) => f.predicate === "mgx:capableOf" && subj.has(f.subject) && obj.has(f.object),
4348
4428
  );
4349
4429
  if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
4430
+ // A KNOWN subject with capability facts, none matching: an honest,
4431
+ // specific miss citing what it CAN do — the same closer the is-a ladder
4432
+ // answers with, instead of the misleading structural parse wall. An
4433
+ // unknown subject still declines. Never a guessed "no": absence of a
4434
+ // capableOf fact proves nothing.
4435
+ const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
4436
+ if (knownCan.length) {
4437
+ const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
4438
+ return {
4439
+ text: `I can't confirm that — nothing I remember says ${can[1]} can ${can[2]}. I do know: ${shown}. If it's true, teach me: "a ${can[1]} can ${can[2]}".`,
4440
+ replace: true,
4441
+ miss: true,
4442
+ };
4443
+ }
4350
4444
  return null;
4351
4445
  }
4352
4446
 
@@ -5370,7 +5464,45 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
5370
5464
  // to the honest miss below, never a guessed "no".
5371
5465
  const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("./syllogise.mjs");
5372
5466
  const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
5467
+ // NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
5468
+ // subject as "dog not" (the "not" glues onto the subject because the
5469
+ // article anchors the kind), so without this the negated question walks
5470
+ // the positive ladder with a garbage subject and lands on a nonsense
5471
+ // teach hint. Strip the "not", then answer INVERTED: a positive isa fact
5472
+ // refutes it ("no — dog is a kind of cat"), a taught disjointness
5473
+ // confirms it ("yes — dog is not a cat"), anything else is an honest
5474
+ // can't-confirm pointing at the already-supported "no X is a Y" teach
5475
+ // shape. Deliberately shallow — no chain chases on the negated side; a
5476
+ // negative proved through a multi-hop positive chain stays an honest
5477
+ // miss rather than a guess.
5478
+ const negSubject = isaAsk[1].match(/^(.*\S)\s+not$/i);
5479
+ if (negSubject) {
5480
+ const negSubjVariants = factTermVariants(normFactTerm, negSubject[1]);
5481
+ const negObjVariants = objVariants;
5482
+ const posHit = isa
5483
+ .filter((f) => negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
5484
+ .sort(byTrust)[0];
5485
+ if (posHit) return { text: `no — ${renderFactLine(posHit)}`, replace: true };
5486
+ const negDisjoint = disjointRows.find((f) => (negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
5487
+ || (negSubjVariants.has(f.object) && negObjVariants.has(f.subject)));
5488
+ if (negDisjoint) return { text: `yes — ${renderFactLine(negDisjoint)}`, replace: true };
5489
+ const negSubjectWord = negSubject[1].trim();
5490
+ const negKindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
5491
+ return {
5492
+ text: `I can't confirm that either way — nothing I remember links ${negSubjectWord} and ${negKindWord}. If no ${negSubjectWord} is a ${negKindWord}, teach me: "no ${negSubjectWord} is a ${negKindWord}".`,
5493
+ replace: true,
5494
+ miss: true,
5495
+ };
5496
+ }
5373
5497
  if (disjointRows.length) {
5498
+ // A DIRECT taught disjointness between the asked subject and kind is a
5499
+ // provable "no" on its own — deriveDisjointViolations only ever fires
5500
+ // through a taught rdf:type premise, so without this check "no dog is
5501
+ // a cat" followed by "is a dog a cat" fell through to the can't-confirm
5502
+ // closer instead of the honest no.
5503
+ const directDisjoint = disjointRows.find((f) => (subjCandidates.has(f.subject) && objVariants.has(f.object))
5504
+ || (subjCandidates.has(f.object) && objVariants.has(f.subject)));
5505
+ if (directDisjoint) return { text: `no — ${renderFactLine(directDisjoint)}`, replace: true };
5374
5506
  const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
5375
5507
  const violations = deriveDisjointViolations(chainTypeEdges, chainSubClassEdges, disjointEdges, { budget: 10 });
5376
5508
  for (const subj of subjCandidates) {
@@ -19,7 +19,7 @@ function contentTokens(text) {
19
19
  }
20
20
 
21
21
  /** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
22
- * hit count), so this is deliberately the simplest correct structure, not a fancy one. */
22
+ * hit count). */
23
23
  function unionFind(n) {
24
24
  const parent = Array.from({ length: n }, (_, i) => i);
25
25
  function find(x) {
@@ -2,7 +2,7 @@
2
2
  // within a group.mjs group. Reuses memory/blocks.mjs's rankBlocks() (PageRank) and degreeOf()
3
3
  // (hub dampening) verbatim at sentence granularity, combined with group-scoped IDF the same
4
4
  // way retrieveBlocks() fuses relevance/centrality/hub-dampening into one score.
5
- // splitSentences() is a simple regex splitter — deliberately not an NLP dependency.
5
+ // splitSentences() is a simple regex splitter.
6
6
 
7
7
  import { degreeOf, rankBlocks, tokenizeBlock, OVERLAP_MIN } from "../memory/blocks.mjs";
8
8
  import { STOPWORDS } from "../prose.mjs";
@@ -15,8 +15,8 @@ function contentTokens(text) {
15
15
  return tokenizeBlock(text).filter(isContentToken);
16
16
  }
17
17
 
18
- // Sentence boundary: a run of [.!?] followed by whitespace and an uppercase letter or digit
19
- // deliberately simple (no abbreviation dictionary, no NLP dependency).
18
+ // Sentence boundary: a run of [.!?] followed by whitespace and an uppercase letter or digit
19
+ // (no abbreviation dictionary).
20
20
  const SENTENCE_SPLIT_RE = /(?<=[.!?])\s+(?=[A-Z0-9])/;
21
21
 
22
22
  /**
@@ -9,7 +9,7 @@
9
9
  // `mgx:contextPassage` fact (the passage it was found in — ConceptNet's own surfaceText,
10
10
  // or the map's `surface` template) and `mgx:coOccursWith` links to its row's other
11
11
  // endpoint plus any already-known terms recognizable in the same passage (bounded) —
12
- // deliberately closed-vocabulary co-occurrence, not distributional/embedding inference.
12
+ // closed-vocabulary co-occurrence.
13
13
  //
14
14
  // Wired as an opt-in `captureUnknownContext: true` on seedMemory (conceptnet.mjs),
15
15
  // dynamically imported to avoid a load-time cycle with conceptnet.mjs.
package/src/finish.mjs CHANGED
@@ -251,7 +251,7 @@ function ruleArticle(segments, rule) {
251
251
  return out;
252
252
  }
253
253
 
254
- // Rule 2 — subject–verb agreement. STRUCTURE-DRIVEN and deliberately narrow:
254
+ // Rule 2 — subject–verb agreement. STRUCTURE-DRIVEN:
255
255
  // (i) an existential "there is/are/was/were" agrees with the FOLLOWING number
256
256
  // span's value (the number is genuinely the subject there);
257
257
  // (ii) any listed copula agrees with a following span that carries an explicit
@@ -1,5 +1,5 @@
1
1
  // grammar/ace.mjs — tmct's deterministic ACE-OWL sub-fragment parser.
2
- // Implements the 8 controlled-English sentence patterns of
2
+ // Implements the 9 controlled-English sentence patterns of
3
3
  // docs/references/schemas/ace-owl-fragment.md and nothing more: fitting the
4
4
  // grammar is a strong signal, missing it is a FEATURE — parseAce returns null
5
5
  // (or an empty-triples result carrying the unknown words as `residue`) and the
@@ -44,11 +44,13 @@ export const PATTERN_CARDINALITY = "cardinality";
44
44
  export const PATTERN_DISJOINT_WITH = "disjointWith";
45
45
  export const PATTERN_POSSESSIVE = "possessive";
46
46
  export const PATTERN_ADJECTIVE = "adjective";
47
+ export const PATTERN_CAPABILITY = "capability";
47
48
 
48
49
  /** The pattern field's full domain, in the README's table order. */
49
50
  export const PATTERNS = Object.freeze([
50
51
  PATTERN_SUB_CLASS_OF, PATTERN_TYPE_ASSERTION, PATTERN_RELATION, PATTERN_SOME_VALUES_FROM,
51
52
  PATTERN_CARDINALITY, PATTERN_DISJOINT_WITH, PATTERN_POSSESSIVE, PATTERN_ADJECTIVE,
53
+ PATTERN_CAPABILITY,
52
54
  ]);
53
55
 
54
56
  const DET = new Set(["a", "an", "the"]);
@@ -413,7 +415,7 @@ function parseCopula(lexicon, toks, lower, isIdx) {
413
415
  ]);
414
416
  }
415
417
 
416
- /** Parse one sentence against the 8-pattern ACE-OWL sub-fragment. See the file
418
+ /** Parse one sentence against the 9-pattern ACE-OWL sub-fragment. See the file
417
419
  * header for the result contract; `lexicon` defaults to the committed core
418
420
  * under the library's own neutral DEFAULT_NS ("ex:") when the caller doesn't
419
421
  * supply one. */
@@ -429,5 +431,32 @@ export function parseAce(sentence, lexicon = loadLexicon()) {
429
431
  }
430
432
  const isIdx = lower.indexOf("is");
431
433
  if (isIdx > 0) return parseCopula(lexicon, toks, lower, isIdx);
434
+ const canIdx = lower.indexOf("can");
435
+ if (canIdx > 0 && canIdx < toks.length - 1) {
436
+ const cap = parseCapability(lexicon, toks, canIdx);
437
+ if (cap) return cap;
438
+ }
432
439
  return parseRelation(lexicon, toks, lower);
433
440
  }
441
+
442
+ /** Pattern 9 — "N can VERB" → mgx:capableOf. The modal is not a relation
443
+ * verb: without this, parseRelation reads "can" through lookupVerb and
444
+ * asserts a generic object property ("dog cans swim") that no capability
445
+ * reader ever finds. Returns null (never a miss record) unless BOTH sides
446
+ * resolve, so a noun "can" ("trash can holds garbage") still falls through
447
+ * to parseRelation. "cannot"/"can't" stays unparsed — the fact vocabulary
448
+ * has no negative-capability predicate, and a silently dropped negation
449
+ * would invert the taught meaning. */
450
+ function parseCapability(lexicon, toks, canIdx) {
451
+ const np1 = resolveNP(lexicon, toks.slice(0, canIdx));
452
+ if (np1.term == null) return null;
453
+ // The capability's object is a VERB ("swim"), not a lexicon noun, so
454
+ // resolveNP is the wrong resolver for it: accept exactly one bare word,
455
+ // stored as a plain term — the same grain the corpus's own CapableOf
456
+ // objects ("bark", "run") already use.
457
+ const rest = toks.slice(canIdx + 1);
458
+ if (rest.length !== 1 || !/^[a-z][a-z-]*$/i.test(rest[0])) return null;
459
+ return hit(PATTERN_CAPABILITY, [np1], [
460
+ { subject: np1.term, predicate: "mgx:capableOf", object: `${lexicon.ns}${rest[0].toLowerCase()}`, kind: "mgx:capableOf" },
461
+ ]);
462
+ }
@@ -14,7 +14,7 @@
14
14
  // Namespace: every lexicon carries a `.ns` field (the CURIE prefix ace.mjs
15
15
  // stamps onto every term it mints) — always "tmct:" here (DEFAULT_NS).
16
16
  //
17
- // Morphology is deliberately tiny and deterministic (no NLP dependency): a
17
+ // Morphology is deterministic (no NLP dependency): a
18
18
  // suffix-fold for plurals/3rd-person-singular ("repositories"→repository,
19
19
  // "relies"→rely, "classes"→class, "uses"→use) plus an optional declared
20
20
  // irregular `plural` ("indices"). Anything the fold can't reach is simply not
package/src/init.mjs CHANGED
@@ -47,8 +47,7 @@ export function defaultConfig() {
47
47
  /** `tmct init --with-persona <name>` presets: a named bundle of `extensions`/`bias`
48
48
  * overrides written into tmct.toml. `human` makes the implicit default explicit; `code`
49
49
  * re-activates the software-domain `seon`+`conceptnet` bundles; `empty` deactivates
50
- * `human`, leaving a repo genuinely empty of corpus facts. Kept minimal on purpose —
51
- * the persona seam, not a curated library of presets. */
50
+ * `human`, leaving a repo genuinely empty of corpus facts. */
52
51
  export const PERSONA_PRESETS = Object.freeze({
53
52
  human: { extensions: {}, bias: { human: 1.0 } },
54
53
  code: { extensions: { seon: { active: true }, conceptnet: { active: true } }, bias: { seon: 1.0, conceptnet: 1.0 } },
@@ -239,7 +239,7 @@ export function applySubordinationFrames(text) {
239
239
  * delimiter are both required — an ordinary em-dash aside is common prose,
240
240
  * not a restart, and treating every dash as a delimiter would be a guess.
241
241
  * The trailing delimiter also means an object-only restart with no comma
242
- * after "i mean" isn't rescued here; that's a narrower, accepted ceiling. */
242
+ * after "i mean" isn't rescued here. */
243
243
  const SELF_CORRECTION_RE =
244
244
  /^.+?(?:\s*(?:--|—|-)\s*)?\b(?:sorry|i\s+mean)\b\s*(?:--|—|-|,|:)\s*(.+)$/i;
245
245
 
@@ -1,6 +1,5 @@
1
1
  // src/router/guardrail.mjs — the guardrail. Validate an
2
- // EXTERNALLY-proposed `tool_use` against the registry's declared preconditions, and
3
- // DEFAULT-DENY anything outside the declared, registered envelope.
2
+ // EXTERNALLY-proposed `tool_use` against the registry's declared preconditions.
4
3
  //
5
4
  // Proves RESOLVABILITY (the tool is registered/declared, args are well-formed, every
6
5
  // `resolves(param, as)` precondition binds to a real graph entity) — NOT antecedent
@@ -14,8 +13,8 @@ import { capabilityByName, preconditionsOf, PRECOND } from "./registry.mjs";
14
13
  import { hallucinationsIn } from "./call-validator.mjs";
15
14
 
16
15
  /** The same read-only breadth-first enrichment as resolver.mjs's `dispatchEachCandidate`:
17
- * every registered capability is `readOnly:true` with an empty delete-list, so dispatching
18
- * the SAME tool once per tied candidate is safe. Returns `[{candidate, result}, ...]`, or
16
+ * dispatching the SAME tool once per tied candidate is safe for `readOnly` capabilities
17
+ * (dispatch performs no writes). Returns `[{candidate, result}, ...]`, or
19
18
  * undefined when there is no dispatcher to run it with. */
20
19
  async function dispatchEachCandidate(pool, capName, arg, ctx) {
21
20
  if (!ctx.dispatch) return undefined;
@@ -2,11 +2,10 @@
2
2
  //
3
3
  // Each tmct tool is modelled as a STRIPS/PDDL operator declared as DATA: a `Capability` with
4
4
  // typed `Parameter`s, `Precondition`s, and `Effect`s (add-list/delete-list). Preconditions are
5
- // the safety gate guardrail.mjs checks before a call fires; effects are epistemic
6
- // (a read-only query "knows" a topic, never mutates) resolver.mjs backward-chains
7
- // from a goal `(knows <topic> ?x)` to the capability whose add-list achieves it.
5
+ // the safety gate guardrail.mjs checks before a call fires; resolver.mjs backward-chains from
6
+ // a goal to a capability whose add-list achieves it.
8
7
  //
9
- // Pure: plain frozen data + pure accessors, no I/O. Tool names + parameter arg keys are the
8
+ // Plain data + pure accessors, no I/O. Tool names + parameter arg keys are the
10
9
  // exact ones src/server.mjs `dispatchTool` reads, so a bound call this registry validates is
11
10
  // directly dispatchable.
12
11
 
@@ -45,7 +44,7 @@ export const PRECOND = Object.freeze({
45
44
 
46
45
  // ---- capability builder (returns PLAIN FROZEN data) -------------------------
47
46
 
48
- /** A parameter slot. `arg` is the EXACT dispatchTool key (never invented). */
47
+ /** A parameter slot. `arg` is the exact key src/server.mjs `dispatchTool` reads. */
49
48
  const param = (name, kind, { arg = name, required = true, note = "" } = {}) =>
50
49
  Object.freeze({ type: VOCAB.Parameter, name, kind, arg, required, note });
51
50
 
@@ -58,31 +57,29 @@ const resolves = (paramName, as) =>
58
57
  const anyPresent = (params) =>
59
58
  Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
60
59
 
61
- /** An epistemic add-effect: after the call the agent KNOWS `topic` about `?of`. */
60
+ /** Add-effect: after the call the agent knows `topic` about `?of`. */
62
61
  const knows = (topic, ofParam = null) =>
63
62
  Object.freeze({ type: VOCAB.Effect, pred: "cap:knows", topic, of: ofParam ? `?${ofParam}` : null });
64
63
 
65
- /** Declare one capability as frozen STRIPS data. Read-only query tools pass an
66
- * empty delete-list (`del: []`) — the closed-world "queries mutate nothing". */
64
+ /** Declare one capability as STRIPS data. */
67
65
  function capability({ name, label, question, params = [], preconditions = [], add = [], del = [] }) {
68
66
  return Object.freeze({
69
67
  type: VOCAB.Capability,
70
68
  name, // the dispatchTool tool name — directly callable
71
69
  label, // human label (the slash-command verb)
72
70
  question, // one-line "what question does this answer"
73
- readOnly: true, // every capability here is query-only
71
+ readOnly: true, // dispatching this capability performs no writes
74
72
  parameters: Object.freeze(params),
75
73
  preconditions: Object.freeze(preconditions),
76
74
  effects: Object.freeze({ add: Object.freeze(add), del: Object.freeze(del) }),
77
75
  });
78
76
  }
79
77
 
80
- // ---- the registry the read-only graph-query tools as operators ------------
81
- // Enumerated from src/server.mjs `dispatchTool` (the query-only, bounded-output
82
- // slice). Arg keys verified against the switch: describe/callers/callees/tests/
83
- // history/… take `symbol`; impact/exports take `module`; members/subclasses take
84
- // `class`; search takes `query` (+ optional kind/name/decorator); architecture
85
- // takes an optional `package`; untested takes nothing.
78
+ // ---- the declared capabilities -----------------------------------------------
79
+ // Arg keys verified against src/server.mjs `dispatchTool`'s switch: describe/callers/
80
+ // callees/tests/history/… take `symbol`; impact/exports take `module`; members/
81
+ // subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
82
+ // architecture takes an optional `package`; untested takes nothing.
86
83
 
87
84
  const CAPABILITIES = Object.freeze([
88
85
  capability({
@@ -187,12 +184,8 @@ const BY_NAME = Object.freeze(
187
184
  CAPABILITIES.reduce((m, c) => { m[c.name] = c; return m; }, Object.create(null)),
188
185
  );
189
186
 
190
- // ---- closed-world / DEFAULT-DENY --------------------------------------------
191
- // The registry is a strict subset of src/server.mjs's `dispatchTool` switch: a tool name
192
- // not registered here is treated as unknown/hallucinated, never dispatchable.
193
- //
194
- // The following dispatch tools are INTENTIONALLY UNREGISTERED — they emit unbounded raw
195
- // output, the most hallucination-prone surface, not a clean bounded-epistemic-effect query.
187
+ // ---- unregistered dispatch tools ---------------------------------------------
188
+ // Dispatch tools not yet registered; each names the precondition work it needs first.
196
189
  export const EXCLUDED_FROM_REGISTRY = Object.freeze({
197
190
  tmct_context: "unbounded edit-context bundle (multi-file); needs a size/budget precondition",
198
191
  tmct_context_more: "unbounded context continuation; same as tmct_context",
@@ -45,7 +45,7 @@ export const NL_INTENTS = Object.freeze({
45
45
  "reverse:cochange": { topic: "cochanges", arg: "symbol" },
46
46
  });
47
47
 
48
- // ---- ask-vocab RELATION kinds with NO capability — the HONEST ceiling --------
48
+ // ---- ask-vocab RELATION kinds with NO capability ------------------------------
49
49
  // Every ask-vocab.mjs RELATIONS key must be either mapped (above) or listed here with a
50
50
  // reason (the conformance test enforces the partition). Refuse rather than mis-route.
51
51
  export const UNMAPPED_KINDS = Object.freeze({
@@ -1,7 +1,7 @@
1
1
  // Shared, safe source-span slicing for the tool layer (src/server.mjs) and the
2
2
  // source-capable Repository Interface provider (src/providers/graph-service.mjs).
3
3
  //
4
- // Two halves, deliberately split:
4
+ // Two halves:
5
5
  // - sliceSpan — PURE. Given an in-memory `lines` array, extracts + line-numbers
6
6
  // one span. No fs, no path logic.
7
7
  // - readSpanSafe — the fs-touching half. Resolves `join(repoRoot, path)` with Node's