@polycode-projects/the-mechanical-code-talker 1.10.4 → 1.10.7

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.4",
3
+ "version": "1.10.7",
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();
@@ -4341,12 +4348,27 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
4341
4348
  // single-hit lookup, same "never a guessed no" discipline).
4342
4349
  const can = q.match(CAN_ASK_RE);
4343
4350
  if (can) {
4351
+ const facts = await memoryFacts(memoryDir);
4344
4352
  const subj = factTermVariants(normFactTerm, can[1]);
4345
4353
  const obj = factTermVariants(normFactTerm, can[2]);
4346
- const hit = (await memoryFacts(memoryDir)).find(
4354
+ const hit = facts.find(
4347
4355
  (f) => f.predicate === "mgx:capableOf" && subj.has(f.subject) && obj.has(f.object),
4348
4356
  );
4349
4357
  if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
4358
+ // A KNOWN subject with capability facts, none matching: an honest,
4359
+ // specific miss citing what it CAN do — the same closer the is-a ladder
4360
+ // answers with, instead of the misleading structural parse wall. An
4361
+ // unknown subject still declines. Never a guessed "no": absence of a
4362
+ // capableOf fact proves nothing.
4363
+ const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
4364
+ if (knownCan.length) {
4365
+ const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
4366
+ return {
4367
+ 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]}".`,
4368
+ replace: true,
4369
+ miss: true,
4370
+ };
4371
+ }
4350
4372
  return null;
4351
4373
  }
4352
4374
 
@@ -5370,7 +5392,45 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
5370
5392
  // to the honest miss below, never a guessed "no".
5371
5393
  const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("./syllogise.mjs");
5372
5394
  const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
5395
+ // NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
5396
+ // subject as "dog not" (the "not" glues onto the subject because the
5397
+ // article anchors the kind), so without this the negated question walks
5398
+ // the positive ladder with a garbage subject and lands on a nonsense
5399
+ // teach hint. Strip the "not", then answer INVERTED: a positive isa fact
5400
+ // refutes it ("no — dog is a kind of cat"), a taught disjointness
5401
+ // confirms it ("yes — dog is not a cat"), anything else is an honest
5402
+ // can't-confirm pointing at the already-supported "no X is a Y" teach
5403
+ // shape. Deliberately shallow — no chain chases on the negated side; a
5404
+ // negative proved through a multi-hop positive chain stays an honest
5405
+ // miss rather than a guess.
5406
+ const negSubject = isaAsk[1].match(/^(.*\S)\s+not$/i);
5407
+ if (negSubject) {
5408
+ const negSubjVariants = factTermVariants(normFactTerm, negSubject[1]);
5409
+ const negObjVariants = objVariants;
5410
+ const posHit = isa
5411
+ .filter((f) => negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
5412
+ .sort(byTrust)[0];
5413
+ if (posHit) return { text: `no — ${renderFactLine(posHit)}`, replace: true };
5414
+ const negDisjoint = disjointRows.find((f) => (negSubjVariants.has(f.subject) && negObjVariants.has(f.object))
5415
+ || (negSubjVariants.has(f.object) && negObjVariants.has(f.subject)));
5416
+ if (negDisjoint) return { text: `yes — ${renderFactLine(negDisjoint)}`, replace: true };
5417
+ const negSubjectWord = negSubject[1].trim();
5418
+ const negKindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
5419
+ return {
5420
+ 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}".`,
5421
+ replace: true,
5422
+ miss: true,
5423
+ };
5424
+ }
5373
5425
  if (disjointRows.length) {
5426
+ // A DIRECT taught disjointness between the asked subject and kind is a
5427
+ // provable "no" on its own — deriveDisjointViolations only ever fires
5428
+ // through a taught rdf:type premise, so without this check "no dog is
5429
+ // a cat" followed by "is a dog a cat" fell through to the can't-confirm
5430
+ // closer instead of the honest no.
5431
+ const directDisjoint = disjointRows.find((f) => (subjCandidates.has(f.subject) && objVariants.has(f.object))
5432
+ || (subjCandidates.has(f.object) && objVariants.has(f.subject)));
5433
+ if (directDisjoint) return { text: `no — ${renderFactLine(directDisjoint)}`, replace: true };
5374
5434
  const disjointEdges = disjointRows.map((f) => [f.subject, f.object]);
5375
5435
  const violations = deriveDisjointViolations(chainTypeEdges, chainSubClassEdges, disjointEdges, { budget: 10 });
5376
5436
  for (const subj of subjCandidates) {
@@ -5503,7 +5563,42 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null, focu
5503
5563
  }
5504
5564
  }
5505
5565
  }
5506
- return null; // no remembered fact the honest miss stands (never a guessed "no")
5566
+ // Every yes-chase and the disjoint "no" above missed. The old
5567
+ // unconditional decline here fell through to the structural "couldn't
5568
+ // parse this as a graph question" wall — actively misleading for a KNOWN
5569
+ // subject, twice over: the question DID parse, and the wall's hint
5570
+ // suggests the exact shape the user just typed. When the subject has
5571
+ // remembered isa-family facts, answer with an honest, specific miss that
5572
+ // cites what IS remembered instead. An unknown subject still declines to
5573
+ // the standing wall/teach-hint path — and this never guesses a "no".
5574
+ // The SUBJECT'S OWN variants only — subjCandidates was augmented above
5575
+ // with the graph entity's class noun (the CLASS↔INSTANCE bridge), and
5576
+ // filtering on it here would cite facts about that noun ("class ⊑
5577
+ // component") as if they were facts about the asked subject ("Widget").
5578
+ const directSubjVariants = factTermVariants(normFactTerm, isaAsk[1]);
5579
+ const knownSubjectIsa = isa.filter((f) => directSubjVariants.has(f.subject)).sort(byTrust);
5580
+ const subjectWord = isaAsk[1].trim();
5581
+ const kindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
5582
+ if (knownSubjectIsa.length) {
5583
+ const shown = knownSubjectIsa.slice(0, 3).map(renderFactLine).join("; ");
5584
+ return {
5585
+ text: `I can't confirm that — nothing I remember says ${subjectWord} is a ${kindWord}. I do know: ${shown}. If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`,
5586
+ replace: true,
5587
+ miss: true, // still a MISS in the turn record — honest wording, not an answer
5588
+ };
5589
+ }
5590
+ // Subject with NO isa facts: only divert when it's mentioned NOWHERE at
5591
+ // all (no fact row on either side, no code entity by id OR class noun) —
5592
+ // a subject known via OTHER predicates ("ahab is male") or the code graph
5593
+ // keeps the old decline, so nothing downstream is ever shadowed.
5594
+ if (!ent && !noun && !rows.some((f) => subjCandidates.has(f.subject) || subjCandidates.has(f.object))) {
5595
+ return {
5596
+ text: `I can't confirm that — I don't know "${subjectWord}" at all yet. If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`,
5597
+ replace: true,
5598
+ miss: true,
5599
+ };
5600
+ }
5601
+ return null; // the honest miss stands (never a guessed "no")
5507
5602
  }
5508
5603
 
5509
5604
  // (a1c-i) CARDINALITY MONOTONICITY — "does every X have at least N Y" over
@@ -7105,6 +7200,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
7105
7200
  if (memoryDir) {
7106
7201
  bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
7107
7202
  ?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
7203
+ if (bareMetaHit?.miss) bareMetaHit = null; // an honest-miss return never diverts the gate
7108
7204
  // A bare "what is X" with NO taught fact but a KNOWN curated corpus term
7109
7205
  // ("what is cache", no article) needs the same "only diverts on a REAL
7110
7206
  // hit" treatment — curatedDefinitionAnswer otherwise only ever runs once
@@ -7175,8 +7271,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
7175
7271
  ?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
7176
7272
  if (fact) {
7177
7273
  answer = fact.replace ? fact.text : `${answer}\n${fact.text}`;
7178
- via = "fact";
7179
- recordMiss = false;
7274
+ // A fact-lane return flagged `miss` is an HONEST MISS in better words
7275
+ // (the isa ladder's "I can't confirm that" closers) — the turn record
7276
+ // keeps miss=true and via stays untouched, so miss-rate metrics and
7277
+ // recall's own miss-gated lanes see it exactly like the wall it replaced.
7278
+ if (!fact.miss) {
7279
+ via = "fact";
7280
+ recordMiss = false;
7281
+ }
7180
7282
  if (fact.pending) factPending = fact.pending; // a truncated fact list → paginable remainder
7181
7283
  if (typeof fact.trust === "number") entailedTrust = fact.trust; // live-chase trust (see the `entailedTrust` declaration above)
7182
7284
  note(trace, `lane: (3) memory facts — factAnswer/factReadBack matched (memoryDir=${memoryDir})`);
@@ -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