@polycode-projects/the-mechanical-code-talker 3.0.10 → 3.1.2

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.
@@ -257,7 +257,12 @@ function renderNarration(trace, { record, detail, fallbackGoal }) {
257
257
  * renderVerbose) see the exact same text a narrate:false run would have
258
258
  * produced; narrate is purely additive to what's PRINTED, never to what's
259
259
  * REMEMBERED. No-op (returns `result` unchanged, by reference) when `trace`
260
- * is null (narrate off) or empty (nothing was traced). */
260
+ * is null (narrate off) or empty (nothing was traced).
261
+ *
262
+ * The same trace block also rides the result as its own `narration` field, so
263
+ * an embedding caller reads the answer and the trace apart without splitting
264
+ * the printed text on NARRATE_MARKER. `answer` keeps the glued form it always
265
+ * had — the field is additive, and absent on a turn that traced nothing. */
261
266
  function withNarration(result, trace, fallbackGoal) {
262
267
  if (!trace || !trace.length) return result;
263
268
  const narrative = renderNarration(trace, { record: result.record, detail: result.detail, fallbackGoal });
@@ -265,7 +270,7 @@ function withNarration(result, trace, fallbackGoal) {
265
270
  const logLines = Array.isArray(result.logLines)
266
271
  ? result.logLines.map((l) => (l === result.answer ? answer : l))
267
272
  : result.logLines;
268
- return { ...result, answer, logLines };
273
+ return { ...result, answer, narration: narrative, logLines };
269
274
  }
270
275
 
271
276
  /** A short "Goal (inferred): …" line appended (never prepended) after every
@@ -1111,14 +1116,26 @@ const CAPABILITY_PHRASES = [
1111
1116
  // anything nameable. Answered the same as any other vague opener: sure,
1112
1117
  // here's what I can help with.
1113
1118
  /^can i ask (?:you\s+)?something(?:\s+random)?\??$/i,
1119
+ // The same orientation request with the SUBJECT flipped to "I" — "what can I
1120
+ // ask you" rather than "what can you do". Every entry above reads "you" as
1121
+ // the subject, so the flipped phrasings matched nothing and the bare "I"
1122
+ // token went to the code-graph lane as a module name. The optional
1123
+ // about-tail reuses META_ORIENT_RE's own closed self-referential noun list,
1124
+ // so "what questions can I ask about <a real module>" is untouched.
1125
+ /^what can (?:i|we) ask(?:\s+(?:you|u))?(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
1126
+ /^what questions can (?:i|we) ask(?:\s+(?:you|u))?(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
1127
+ /^what (?:kind|kinds|sort|sorts|type|types) of questions can (?:i|we) ask(?:\s+(?:you|u))?(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
1114
1128
  ];
1115
1129
  /** IDENTITY questions — "who/what are you", by name, in plain or ESL-ish phrasing.
1116
1130
  * Routed to a self-description (identity-self) that works regardless of graph
1117
1131
  * state, never the code-graph deflection. */
1118
1132
  const IDENTITY_PHRASES = [
1119
- /^who are you\??$/i, /^what (is|are|r) (this|you)\??$/i,
1133
+ // The optional adverb ("what EVEN is this thing", "what REALLY is this") is
1134
+ // emphasis on the same question — without it the casual spelling missed the
1135
+ // closed set entirely and the bare "this"/"I" token went to the graph lane.
1136
+ /^who are you\??$/i, /^what (?:even |really |actually )?(is|are|r) (this|you)\??$/i,
1120
1137
  /^what('?s| is) your name\??$/i, /^what exactly are you\??$/i,
1121
- /^(tell me about|introduce) yourself\??$/i, /^what is this thing\??$/i,
1138
+ /^(tell me about|introduce) yourself\??$/i, /^what (?:even |really |actually )?is this thing\??$/i,
1122
1139
  /^what am i (talking|speaking|chatting) (to|with)\??$/i,
1123
1140
  /^you are what\??$/i, /^what thing (are|is) you\??$/i,
1124
1141
  // "explain [to me|please]* what (you are|this is)" in EITHER word order — a
@@ -1248,6 +1265,34 @@ function aiIdentityMatch(raw) {
1248
1265
  return splitClauses(text).some((clause) => AI_IDENTITY_PHRASES.some((re) => re.test(clause)));
1249
1266
  }
1250
1267
 
1268
+ /** The self-referential tail a casual identity question trails after a comma
1269
+ * ("what even is this thing, like what does it do?"). It names no term at all
1270
+ * — "it"/"this"/"you" is the thing already asked about in the first clause —
1271
+ * so it adds nothing to answer and only has to be RECOGNIZED, not routed. */
1272
+ const SELF_REFERENTIAL_TAIL_RE = /^(?:like\s+|so\s+|and\s+)?what (?:does|do|can) (?:it|this|you|u) do\??$/i;
1273
+
1274
+ /** IDENTITY_PHRASES matched against the raw turn, its preamble-peeled twin, or
1275
+ * a comma-joined identity clause trailed only by self-referential filler.
1276
+ *
1277
+ * The peeled check mirrors what CAPABILITY_PHRASES' own dispatch already does
1278
+ * ("hello there, what am I talking to?" is an exact identity question behind a
1279
+ * greeting frame, and every entry is anchored, so the frame alone sank it).
1280
+ *
1281
+ * The clause check is deliberately narrower than aiIdentityMatch's: it splits
1282
+ * on commas/"and" (conversationalClauses), which a real graph question can sit
1283
+ * on the far side of, so a match requires the FIRST clause to be an identity
1284
+ * question AND every later clause to be self-referential filler. "what is
1285
+ * this, and which modules import walk.mjs" therefore stays a graph query. */
1286
+ function identityPhraseMatch(raw) {
1287
+ const text = String(raw);
1288
+ const anchored = (s) => IDENTITY_PHRASES.some((re) => re.test(s));
1289
+ if (anchored(text) || anchored(applyPreambleFrames(text))) return true;
1290
+ const clauses = conversationalClauses(applyPreambleFrames(text).trim());
1291
+ if (clauses.length < 2) return false;
1292
+ return anchored(clauses[0].replace(/[?.!]+$/, ""))
1293
+ && clauses.slice(1).every((c) => SELF_REFERENTIAL_TAIL_RE.test(c));
1294
+ }
1295
+
1251
1296
  /** "Do you have feelings/emotions" — with no closed-set match, this would
1252
1297
  * otherwise misfire into a literal module-name lookup for the bare noun
1253
1298
  * ("no module matching 'feelings' found in the index") — a wrong-flavor
@@ -2009,7 +2054,7 @@ function conversationalTurn(line, ctx) {
2009
2054
  { lane: "help" },
2010
2055
  );
2011
2056
  }
2012
- if (IDENTITY_PHRASES.some((re) => re.test(raw))) {
2057
+ if (identityPhraseMatch(raw)) {
2013
2058
  note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
2014
2059
  note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
2015
2060
  return mk(t(T_IDENTITY_SELF), { lane: "help" });
@@ -2126,7 +2171,12 @@ const ORIENTATION_EMPTY_FALLBACK = "I'm tmct — a deterministic, offline chat a
2126
2171
  * branch uses, so there is exactly one copy of that wording to keep in sync, not
2127
2172
  * two hand-duplicated strings. */
2128
2173
  function orientationText(graph, templates, vocabHint) {
2129
- if (noCodeGraph(graph)) {
2174
+ // A null (never-loaded) graph reads as "unknown, not empty" to noCodeGraph,
2175
+ // which is the right call for the greeting/orientation CARD — but there is no
2176
+ // entity count to render from one either, so the empty wording is the only
2177
+ // truthful thing left to say. Without this the entity tally below threw on a
2178
+ // graph-less runTurn.
2179
+ if (!graph || noCodeGraph(graph)) {
2130
2180
  return tRender(templates, T_ORIENTATION_EMPTY, { vocabHint }) ?? ORIENTATION_EMPTY_FALLBACK;
2131
2181
  }
2132
2182
  const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
@@ -2245,7 +2295,14 @@ const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+
2245
2295
  // stayed null) before ever trying the unknown-subject/object mint fallbacks
2246
2296
  // below — the SAME sentence typed without the period worked. Mirrors
2247
2297
  // UNKNOWN_SUBJECT_RE's own identical tolerance, added for the same reason.
2248
- const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+(?: too)?[.!?]*$/i;
2298
+ // This is the gate that decides whether a bare declarative becomes a teach
2299
+ // payload at all, so a complement narrower than UNKNOWN_SUBJECT_RE's own object
2300
+ // capture kept a multi-word class name out of the mint fallbacks downstream no
2301
+ // matter how wide their captures were. A multi-word complement is admitted ONLY
2302
+ // behind an article — "is A unit of work" names a class, while the article-less
2303
+ // "is nice today" / "are fast animals" is ordinary prose that happens to carry a
2304
+ // copula, and admitting THAT grounds filler sentences as facts.
2305
+ const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:(?:a |an )[\w-]+(?: [\w-]+){0,2}|[\w-]+)(?: too)?[.!?]*$/i;
2249
2306
  /** "X is <comparative> than Y" — the comparative teach/ask surface. The
2250
2307
  * comparative slot is closed by SHAPE (-er word, better/worse, or a
2251
2308
  * more/less + adjective pair), never a hand-list of adjectives. */
@@ -2842,7 +2899,11 @@ export { singularizeSurface };
2842
2899
  * and unknownObjectFallback's own docblocks) — a proper noun that happens to
2843
2900
  * end in "s" ("redis") naively strips to "redi" if singularized on an "is"
2844
2901
  * sentence, where no such fold is ever needed.
2845
- * Y (the object) is a single token, same as parseAce's own copula fragments;
2902
+ * Y (the object) is ONE TO THREE tokens, so a natural multi-word class name
2903
+ * ("every Function is a unit of work") reaches the mint instead of failing
2904
+ * the match outright and falling to the grammar wall; the greedy quantifier
2905
+ * plus the end anchor keep a longer trailing phrase ("rex is a dog in the
2906
+ * garden") rejected exactly as before.
2846
2907
  * X (the subject) is ONE OR TWO tokens, to cover a natural 2-word noun
2847
2908
  * phrase ("vulcan gizmo is a tool"), the same class of gap OWNS_TEACH_RE's
2848
2909
  * own object needed widening for, above. The greedy quantifier tries the
@@ -2867,7 +2928,7 @@ export { singularizeSurface };
2867
2928
  * captures themselves are unaffected (`[\w-]+` never included the period in
2868
2929
  * the first place), so this only widens WHICH sentences reach the match,
2869
2930
  * never what gets captured out of one that already did. */
2870
- const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|any\s+|a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(is|are)\s+(?:an?\s+)?([\w-]+)[.!?]*$/i;
2931
+ const UNKNOWN_SUBJECT_RE = /^(every\s+|each\s+|all\s+|any\s+|a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(is|are)\s+(?:an?\s+)?([\w-]+(?:\s+[\w-]+){0,2})[.!?]*$/i;
2871
2932
 
2872
2933
  /** ISA-family predicates (mirrors the private ISA_PREDICATES set defined near
2873
2934
  * memoryFacts, below, at module scope — both are simple top-level consts
@@ -3124,6 +3185,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
3124
3185
  * tagging surprise, degrades to a null tag treated as "no signal" (never a
3125
3186
  * decline) — matching every other optional-adapter path in this file. */
3126
3187
  async function objectReadsAsNonNoun(word) {
3188
+ // A MULTI-WORD complement only ever reaches here from behind an article, so
3189
+ // it is a noun phrase by construction and the adjective question doesn't
3190
+ // arise. Asking wink anyway tags the joined string as one opaque token and
3191
+ // reads back nonsense ("key value store" → ADJ), which then diverted a plain
3192
+ // class claim into the property lane.
3193
+ if (/\s/.test(String(word || "").trim())) return false;
3127
3194
  try {
3128
3195
  const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
3129
3196
  const adapter = nlpAdapter();
@@ -4766,12 +4833,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
4766
4833
  if (stored) return stored;
4767
4834
  }
4768
4835
  // PASSIVE ownership — "<X> is owned by <Name>". Same bare-form gate as the
4769
- // active shape just above: a Capitalized owner
4770
- // name AND no interrogative lead, so "is TaskController owned by anyone"
4771
- // (a genuine yes/no QUESTION, handled by factReadBack instead) never lands
4772
- // a bogus fact here.
4836
+ // active shape just above, reading EITHER side and requiring no
4837
+ // interrogative lead, so "is TaskController owned by anyone" (a genuine
4838
+ // yes/no QUESTION, handled by factReadBack instead) never lands a bogus
4839
+ // fact here. Reading both sides matters as much as it does for the active
4840
+ // shape, and the SUBJECT side gets the path test too: "src/domain/
4841
+ // lexicon.mjs is owned by antony" states ownership just as plainly as a
4842
+ // capitalized owner does, while "the car is owned by john" still declines.
4773
4843
  const ownPassive = ownSrc.match(OWNS_PASSIVE_TEACH_RE);
4774
- if (ownPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion && (wrapped || /^[A-Z]/.test(ownPassive[2]))) {
4844
+ const ownPassiveSubject = ownPassive?.[1]?.trim() ?? "";
4845
+ if (ownPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion
4846
+ && (wrapped || /^[A-Z]/.test(ownPassive[2]) || /^[A-Z]/.test(ownPassiveSubject)
4847
+ || MODULE_PATH_RE.test(ownPassiveSubject))) {
4775
4848
  const stored = await teachFact(memoryDir, sessionId, {
4776
4849
  subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2],
4777
4850
  });
@@ -5469,7 +5542,7 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
5469
5542
  // do" needs the noun OPTIONAL after "this" (kept REQUIRED after "the") or it
5470
5543
  // falls through to MODULE_ORIENT_RE, which fails to resolve "this" as an
5471
5544
  // entity and hits the raw grammar wall.
5472
- const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+this(?:\s+(?:app|code|codebase|project|repo))?\s+do|what\s+does\s+the\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin)(?:\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)?|what\s+should\s+i\s+(?:read|look\s+at)\s+first(?:\s+to\s+understand\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+should\s+i\s+start\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+do\s+i\s+begin\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)$/;
5545
+ const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+this(?:\s+(?:app|code|codebase|project|repo))?\s+do|what\s+does\s+the\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:(?:start|begin|get\s+started|get\s+going)(?:\s+(?:with|on)\s+(?:this|it))?|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin)(?:\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)?|what\s+should\s+i\s+(?:read|look\s+at)\s+first(?:\s+to\s+understand\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+should\s+i\s+start\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?|where\s+do\s+i\s+begin\s+reading(?:\s+(?:this\s+)?(?:codebase|code|repo|repository|project))?)$/;
5473
5546
  /** A bare "what is in here"/"what's in here"/"whats in here" — the SAME
5474
5547
  * orientation intent as META_ORIENT_RE's own
5475
5548
  * "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
@@ -5517,17 +5590,19 @@ async function memorySummary(memoryDir, graph) {
5517
5590
  // #2(e) MODULE-GRAIN OVERVIEW. META_ORIENT_RE (above) can't match a module
5518
5591
  // path/symbol name, so "what does app/lib/a.mjs do" needs its own lane.
5519
5592
  // CASE-PRESERVING: reads the ORIGINAL query text, never metaLane's lowercased `q`.
5520
- /** A trailing intensifier/filler adverb tacked onto "do"/"does" ("what does the
5521
- * store module do exactly?", "...do exactly", "what X does really") the
5522
- * closed-form anchor below requires "do"/"does" to be the LAST word before
5523
- * the optional "?", so this one extra word past it would otherwise hit the
5524
- * raw grammar wall even though the shared FILLER_WORDS/normalizeQuery pass
5525
- * (used elsewhere in the file) never sees this lane's case-preserving text
5526
- * at all. Mirrors MODULE_ORIENT_POLITENESS_RE
5527
- * just below: closed, optional, single-lane blast radius — a bare "what does
5528
- * X do" still matches with this suffix empty. */
5529
- const TRAILING_ADVERB_RE = "(?:\\s+(?:exactly|really|actually|anyway))?";
5530
- const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVERB_RE}\\??$`, "i");
5593
+ /** One intensifier/filler adverb sitting either side of "do"/"does" ("what does
5594
+ * the store module do exactly?", "what does tasks.mjs actually do
5595
+ * internally", "what X does really") the closed-form anchors below pin
5596
+ * "do"/"does" against the term on one side and the optional "?" on the
5597
+ * other, so either extra word would hit the raw grammar wall even though the
5598
+ * shared FILLER_WORDS/normalizeQuery pass (used elsewhere in the file) never
5599
+ * sees this lane's case-preserving text at all. Mirrors
5600
+ * MODULE_ORIENT_POLITENESS_RE just below: closed, optional, single-lane blast
5601
+ * radius — a bare "what does X do" still matches with both slots empty. The
5602
+ * PRE-verb slot also keeps an adverb out of the term capture, so
5603
+ * "tasks.mjs actually" never reaches entity resolution as one name. */
5604
+ const ORIENT_ADVERB_SLOT_RE = "(?:\\s+(?:exactly|really|actually|truly|anyway|internally|properly))?";
5605
+ const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)${ORIENT_ADVERB_SLOT_RE}\\s+do${ORIENT_ADVERB_SLOT_RE}\\??$`, "i");
5531
5606
  /** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
5532
5607
  * "what does saveStore do") — a perfectly natural alternate phrasing of an
5533
5608
  * ALREADY-recognized intent that would otherwise hit the raw grammar wall
@@ -5537,7 +5612,7 @@ const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVE
5537
5612
  * UNIQUE graph entity or this lane declines) is what keeps this loose an
5538
5613
  * ending safe — a syntactic match against a term that isn't a real entity
5539
5614
  * simply falls through unchanged, same as every other lane in this file. */
5540
- const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB_RE}\\??$`, "i");
5615
+ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)${ORIENT_ADVERB_SLOT_RE}\\s+does${ORIENT_ADVERB_SLOT_RE}\\??$`, "i");
5541
5616
  /** "whats X do" / "what's X do" / "what is X do" — the CONTRACTED phrasing of
5542
5617
  * "what does X do", where the auxiliary collapses into the "what's"/"whats"
5543
5618
  * opener and "do" trails the term. MODULE_ORIENT_RE's own "does BEFORE the
@@ -5548,7 +5623,7 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
5548
5623
  * that is not a real unique entity (a pronoun subject "whats it do", a
5549
5624
  * non-word) simply declines. The "what(?:'s|s|\s+is)" opener mirrors
5550
5625
  * MODULE_PURPOSE_RE's tolerance for the apostrophe-less "whats" contraction. */
5551
- const MODULE_ORIENT_IS_DO_RE = new RegExp(`^what(?:'s|s|\\s+is)\\s+(.+?)\\s+do${TRAILING_ADVERB_RE}\\??$`, "i");
5626
+ const MODULE_ORIENT_IS_DO_RE = new RegExp(`^what(?:'s|s|\\s+is)\\s+(.+?)${ORIENT_ADVERB_SLOT_RE}\\s+do${ORIENT_ADVERB_SLOT_RE}\\??$`, "i");
5552
5627
  // Purpose/identity phrasing: "whats X for"/"what's X
5553
5628
  // about"/"what is X for", the sibling of "what does X do" that asks for the
5554
5629
  // SAME module-grain overview. Deliberately does NOT claim the literal noun
@@ -10927,9 +11002,24 @@ const STACCATO_PRONOUN_RE = /^(?:and|also|so|then|now)\s+(it|that|this|those|the
10927
11002
  const DESCRIBE_GRAIN_WORD_RE = new RegExp(
10928
11003
  `^(?:(?:the|a|an)\\s+)?(.+?)\\s+(${Object.keys(ENTITY_TO_TYPE).join("|")})$`, "i",
10929
11004
  );
11005
+ /** A LEADING noise phrase glued onto the captured term by an outer bridge, the
11006
+ * mirror of the trailing grain word DESCRIBE_GRAIN_WORD_RE strips. "give me
11007
+ * the context for src/core/store.mjs" reaches this lane as "describe context
11008
+ * for src/core/store.mjs" (normalize.mjs's show/give-me frame rewrites the
11009
+ * whole tail into the term), and every resolver downstream then reads
11010
+ * "context" as part of the name. Closed list, and the term still has to
11011
+ * resolve afterwards, so a phrase that leaves nothing resolvable simply
11012
+ * declines to the ordinary miss. */
11013
+ const DESCRIBE_LEADING_NOISE_RE =
11014
+ /^(?:the\s+)?(?:context|details|detail|info|information|background)\s+(?:for|on|about|of|around)\s+/i;
11015
+ function stripDescribeLeadingNoise(term) {
11016
+ const stripped = String(term || "").replace(DESCRIBE_LEADING_NOISE_RE, "").trim();
11017
+ return stripped || String(term || "").trim();
11018
+ }
11019
+
10930
11020
  async function describeGrainRescue(graph, term) {
10931
11021
  if (!graph) return null;
10932
- const m = String(term || "").trim().match(DESCRIBE_GRAIN_WORD_RE);
11022
+ const m = stripDescribeLeadingNoise(term).match(DESCRIBE_GRAIN_WORD_RE);
10933
11023
  if (!m) return null;
10934
11024
  const [, head, grainWord] = m;
10935
11025
  const expectedClass = ENTITY_TO_TYPE[grainWord.toLowerCase()];
@@ -10959,6 +11049,11 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
10959
11049
  // "describe about X" (a doubled verb) leaves a redundant leading "about "
10960
11050
  // glued to the captured term.
10961
11051
  term = term.replace(/^about\s+/i, "");
11052
+ // "give me the context for X" arrives bridged into "describe context for X".
11053
+ // Cleaned once here so the grain rescue, the /describe dispatch and the
11054
+ // focus-setting resolveEntity below all read the same clean term — the
11055
+ // answer was already right without this, but the focus never updated.
11056
+ term = stripDescribeLeadingNoise(term);
10962
11057
  // A trailing bare discourse tag ("describe Record then") glued onto the
10963
11058
  // captured term, same class of gap stripTrailingDiscourseTag (ask-vocab.mjs)
10964
11059
  // already fixes for the meta-whatis vocab lane.
@@ -14785,7 +14880,60 @@ function vocabAntecedentFrom(last) {
14785
14880
  return m[1];
14786
14881
  }
14787
14882
 
14788
- export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, discourse = null, _noSplit = false } = {}) {
14883
+ /** Commands whose answer is SERVED SOURCE TEXT lines read byte-for-byte off a
14884
+ * file on disk, which a caller may paste straight into an edit. The prose
14885
+ * grammar rules rewrite real code into code that no longer parses: a spread
14886
+ * (`...base`) and an optional chain (`?.`) both read as runs of terminal
14887
+ * punctuation and collapse to a single character. So the whole answer goes to
14888
+ * finish() as ONE protected `code` span and comes back untouched. */
14889
+ const SOURCE_RENDERING_COMMANDS = new Set(["context", "snippet"]);
14890
+
14891
+ /** finish() a dispatched turn, byte-protecting a served-source answer. A
14892
+ * producer that segmented its own answer keeps its own segments; everything
14893
+ * else is masked and grammar-corrected exactly as before. */
14894
+ function finishTurn(result, ctx) {
14895
+ const preSegmented = Array.isArray(result?.segments) && result.segments.length > 0;
14896
+ if (!preSegmented && typeof result?.answer === "string"
14897
+ && SOURCE_RENDERING_COMMANDS.has(result?.record?.command)) {
14898
+ return finish({ ...result, segments: [{ type: "code", text: result.answer }] }, ctx);
14899
+ }
14900
+ return finish(result, ctx);
14901
+ }
14902
+
14903
+ /** An uncached readFactRows() snapshot of the memory store — one half of the
14904
+ * before/after pair a turn's `factsTouched` diff is taken over. Deliberately
14905
+ * bypasses the turn's factRowsCache: the cache is invalidated by only some
14906
+ * write paths, and a diff read through it would miss the very writes it exists
14907
+ * to report. Null (rather than []) when there is no store or it won't load, so
14908
+ * the caller can tell "nothing to diff" from "diffed, nothing moved". */
14909
+ async function factRowSnapshot(memoryDir) {
14910
+ if (!memoryDir) return null;
14911
+ try { return readStoredFactRows(await loadMemoryStore(memoryDir)); } catch { return null; }
14912
+ }
14913
+
14914
+ /** The Fact rows this turn wrote, diffed against the snapshot taken before it.
14915
+ * Empty when the turn had no store to write to, or wrote nothing. */
14916
+ async function factsTouchedSince(memoryDir, before) {
14917
+ if (!before) return [];
14918
+ const after = await factRowSnapshot(memoryDir);
14919
+ if (!after) return [];
14920
+ const { touchedFactRows } = await import("../domain/memory/touched-facts.mjs");
14921
+ return touchedFactRows(before, after);
14922
+ }
14923
+
14924
+ /** Run one turn and report which Fact rows it wrote, as `factsTouched` beside
14925
+ * the answer/record/logLines every caller already reads. The dispatch itself
14926
+ * is dispatchTurn, below; this wrapper exists so the field lands on EVERY
14927
+ * return path (dispatched, conversational, multi-sentence) from one place. */
14928
+ export async function runTurn(input, options = {}) {
14929
+ const memoryDir = options?.memoryDir ?? null;
14930
+ const before = await factRowSnapshot(memoryDir);
14931
+ const result = await dispatchTurn(input, options);
14932
+ if (!result || typeof result !== "object") return result;
14933
+ return { ...result, factsTouched: await factsTouchedSince(memoryDir, before) };
14934
+ }
14935
+
14936
+ async function dispatchTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, discourse = null, _noSplit = false } = {}) {
14789
14937
  // Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
14790
14938
  // bounds, the shared plan lane's search-depth cap) — a caller's own
14791
14939
  // gameConfig (chat-session.mjs resolves one per session from tmct.toml)
@@ -14863,7 +15011,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
14863
15011
  // what the shell prints. The narrate block is applied AFTER `last` is
14864
15012
  // captured from the PRE-narration finished result.
14865
15013
  const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
14866
- const finished = attachDialogueAct(finish(result, { graph }), trace);
15014
+ const finished = attachDialogueAct(finishTurn(result, { graph }), trace);
14867
15015
  // The logged transcript echo is ALWAYS the verbatim user line — no dispatch
14868
15016
  // path's internal rewrite (the indirect-request wrapper, the vocab-opener /
14869
15017
  // cleft / ESL rewrites, a discourse substitution) may leak into what the
@@ -15154,8 +15302,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
15154
15302
  let f = focus; let l = last; let ps = planHolder.state; let d = discourseHolder.record;
15155
15303
  const receipts = [];
15156
15304
  let finalRec = null;
15305
+ // dispatchTurn, not runTurn: the OUTER wrapper's before/after snapshot
15306
+ // already spans every sentence, so a per-sentence diff would only pay
15307
+ // for a narrower answer to the same question.
15157
15308
  for (const sentence of sentences) {
15158
- const r = await runTurn(sentence, {
15309
+ const r = await dispatchTurn(sentence, {
15159
15310
  config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
15160
15311
  narrate: false, vocabHint, tel, biasByBundle, planState: ps, discourse: d, _noSplit: true,
15161
15312
  });
@@ -354,6 +354,9 @@ export function renderCodeExplorerHtml(data, { bundleInline = "", bundleAvailabl
354
354
  <meta charset="utf-8">
355
355
  <meta name="viewport" content="width=device-width, initial-scale=1">
356
356
  <title>tmct code explorer</title>
357
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml">
358
+ <link rel="icon" href="./favicon.ico" sizes="any">
359
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">
357
360
  <style>
358
361
  ${THEME_TOKENS_CSS}
359
362
  * { box-sizing: border-box; }
@@ -20,7 +20,12 @@ const GRAMMAR_DIR = dirname(fileURLToPath(import.meta.url));
20
20
  /** The data-driven grammar-rule table. */
21
21
  const GRAMMAR_RULES_FILE = join(GRAMMAR_DIR, "..", "..", "data", "templates", "grammar-rules.toml");
22
22
 
23
- /** The segment type vocabulary. `prose` is the only unprotected type. */
23
+ /** The segment type vocabulary. `prose` is the only unprotected type.
24
+ *
25
+ * `code` is the one type maskSegments never infers: no regex tells source text
26
+ * from prose reliably enough to be trusted with a caller's edit. A producer
27
+ * that KNOWS it is serving source (chat's /snippet and /context) says so by
28
+ * handing finish() its own `code` span, and gets its bytes back exactly. */
24
29
  export const SEGMENT_TYPES = Object.freeze([
25
30
  "prose", "entity", "path", "number", "code", "provenance", "receipt",
26
31
  ]);
@@ -76,6 +76,9 @@ export function renderIngestHtml({ title = DEFAULT_TITLE } = {}) {
76
76
  <meta charset="utf-8">
77
77
  <meta name="viewport" content="width=device-width, initial-scale=1">
78
78
  <title>${escapeHtml(title)}</title>
79
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml">
80
+ <link rel="icon" href="./favicon.ico" sizes="any">
81
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">
79
82
  <!--
80
83
  The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
81
84
  first-party bundle (built by scripts/build-wink-vendor.mjs), one cached copy
@@ -570,6 +570,9 @@ export function renderLedgerHtml({ rows, terms, edges, focus, contradictions, wo
570
570
  <meta charset="utf-8">
571
571
  <meta name="viewport" content="width=device-width, initial-scale=1">
572
572
  <title>${escapeHtml(title)}</title>
573
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml">
574
+ <link rel="icon" href="./favicon.ico" sizes="any">
575
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">
573
576
  <!--
574
577
  The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
575
578
  first-party bundle of wink-nlp + wink-eng-lite-web-model (built by
@@ -293,6 +293,9 @@ export function renderPlanHtml({ plan, rendersAs = {}, sizeOrder = [], title } =
293
293
  <meta charset="utf-8">
294
294
  <meta name="viewport" content="width=device-width, initial-scale=1">
295
295
  <title>${escapeHtml(pageTitle)}</title>
296
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml">
297
+ <link rel="icon" href="./favicon.ico" sizes="any">
298
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">
296
299
  <!--
297
300
  The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
298
301
  first-party bundle of wink-nlp + wink-eng-lite-web-model (built by
@@ -87,6 +87,9 @@ export function renderResearchHtml({ title = DEFAULT_TITLE, digestStructures = [
87
87
  <meta charset="utf-8">
88
88
  <meta name="viewport" content="width=device-width, initial-scale=1">
89
89
  <title>${escapeHtml(title)}</title>
90
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml">
91
+ <link rel="icon" href="./favicon.ico" sizes="any">
92
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">
90
93
  <!--
91
94
  The wink lemma/POS tier loads from ./vendor/wink.js — the site's own shared
92
95
  first-party bundle (built by scripts/build-wink-vendor.mjs), one cached copy
@@ -225,7 +225,9 @@ export function nextCorpses(prevCorpses, prevAgents, agents, turn, lingerTurns =
225
225
  * the engine into the page instead of the sibling `<script src>`, for the
226
226
  * CLI's standalone export — one downloadable file that runs from file://
227
227
  * with no sibling assets. Default empty keeps the site build's sibling-file
228
- * arrangement byte-identical. */
228
+ * arrangement byte-identical, favicon links included; the standalone export
229
+ * drops them too, since a relative ./favicon.svg would be a dangling
230
+ * external reference the "no sibling assets" export can't carry. */
229
231
  export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [], engineBundleJs = "" } = {}) {
230
232
  const gridData = embedJson({
231
233
  gridSize: GRID_SIZE,
@@ -254,6 +256,9 @@ export function renderSpiderFlyHtml({ title = DEFAULT_TITLE, spriteTemplates = [
254
256
  <meta charset="utf-8">
255
257
  <meta name="viewport" content="width=device-width, initial-scale=1">
256
258
  <title>${escapeHtml(title)}</title>
259
+ ${engineBundleJs ? "" : `<link rel="icon" href="./favicon.svg" type="image/svg+xml">
260
+ <link rel="icon" href="./favicon.ico" sizes="any">
261
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">`}
257
262
  <style>
258
263
  ${THEME_TOKENS_CSS}
259
264
  :root { --fly: #A6791F; }
@@ -464,7 +464,9 @@ function sectionHtml(group, entries) {
464
464
  * same two template sets) and references the sibling
465
465
  * ./sprites-browser.bundle.js scripts/build-demo-site.mjs builds alongside
466
466
  * it. Left false, the page renders exactly as before — no dock, no bundle
467
- * reference, nothing extra to 404. */
467
+ * reference, nothing extra to 404 — including the favicon links, since the
468
+ * CLI's standalone export (also `spritesBundleAvailable: false`) can't carry
469
+ * a dangling relative ./favicon.svg either. */
468
470
  export function renderSpriteCatalogHtml({ title = DEFAULT_TITLE, iconTemplates = [], largeTemplates = [], factRows = [], spritesBundleAvailable = false } = {}) {
469
471
  const entries = buildSpriteCatalogEntries({ iconTemplates, largeTemplates, factRows });
470
472
  const totalSwatches = entries.reduce((n, e) => n + e.iconSwatches.length + e.largeSwatches.length, 0);
@@ -607,6 +609,9 @@ const SPRITE_CHAT = ${embedJson({ rows: dockRows })};
607
609
  <meta charset="utf-8">
608
610
  <meta name="viewport" content="width=device-width, initial-scale=1">
609
611
  <title>${escapeHtml(title)}</title>
612
+ ${spritesBundleAvailable ? `<link rel="icon" href="./favicon.svg" type="image/svg+xml">
613
+ <link rel="icon" href="./favicon.ico" sizes="any">
614
+ <link rel="apple-touch-icon" href="./apple-touch-icon.png">` : ""}
610
615
  <style>
611
616
  ${THEME_TOKENS_CSS}
612
617
  html { background: var(--bg); }