@polycode-projects/the-mechanical-code-talker 1.5.4 → 1.5.5

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.5.4",
3
+ "version": "1.5.5",
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
@@ -58,7 +58,7 @@ import {
58
58
  VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
59
59
  stripTrailingScopeFiller, stripTrailingDiscourseTag,
60
60
  } from "./ask-vocab.mjs";
61
- import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, escapeRegex } from "./interpret/normalize.mjs";
61
+ import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, normalizeQuery, escapeRegex } from "./interpret/normalize.mjs";
62
62
  import { fuzzyMatchInSet, fuzzyBound } from "./interpret/fuzzy.mjs";
63
63
 
64
64
  // uuidv7 lives in ./uuid.mjs (shared with telemetry + the bench stamp); re-exported
@@ -2196,6 +2196,31 @@ async function generalVerbTeach(payload) {
2196
2196
  return { subject, predicate, object };
2197
2197
  }
2198
2198
 
2199
+ /** HANDOVER.md 2026-07-10 item 2 — is `word` a genuine NOUN/PROPN, per wink-nlp's
2200
+ * optional POS tagger (ask-nlp.mjs's nlpAdapter, the SAME adapter the closed
2201
+ * structural grammar already leans on)? Used to let ONE narrow bare (unwrapped)
2202
+ * general-verb teach sentence through below: GENERAL_VERB_TEACH_RE's shape
2203
+ * ("<word> <word> <rest>") is too permissive to trust on a bare sentence with no
2204
+ * "remember"/"note" signal at all — "tell me a joke" and "explain the class
2205
+ * hierarchy to me" match the IDENTICAL shape (subject="tell"/"explain", the
2206
+ * imperative verb itself, mistaken for a subject) and must never be silently
2207
+ * reified as bogus mgx:me/mgx:the facts (confirmed live: both are tagged VERB).
2208
+ * A genuine declarative's first word is a NOUN/PROPN instead ("grace mentors
2209
+ * alan", "sam owns TaskController" — confirmed live: both tagged NOUN). No wink
2210
+ * installed degrades to false (never a guess), same as every other optional-
2211
+ * adapter path in this codebase. */
2212
+ async function subjectIsNounOrPropn(word) {
2213
+ try {
2214
+ const { nlpAdapter } = await import("./ask-nlp.mjs");
2215
+ const adapter = nlpAdapter();
2216
+ if (!adapter) return false;
2217
+ const [tag] = adapter.posTags([String(word || "")]);
2218
+ return tag === "NOUN" || tag === "PROPN";
2219
+ } catch {
2220
+ return false;
2221
+ }
2222
+ }
2223
+
2199
2224
  // ---- General verb-to-predicate DIRECT-QUESTION retrieval (item 5, this
2200
2225
  // session's follow-up to the teach mechanism above): "does margo eat ribs" /
2201
2226
  // "did margo eat ribs" / "what does margo eat" against a fact taught via
@@ -2343,11 +2368,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
2343
2368
  }
2344
2369
 
2345
2370
  // OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
2346
- // form is double-gated: a Capitalized name AND no interrogative lead, so the
2347
- // "who owns <X>" READ question and ordinary prose never land a fact here.
2371
+ // form is double-gated: no interrogative lead, PLUS either side spelling a
2372
+ // Capitalized token — so the "who owns <X>" READ question and ordinary
2373
+ // lowercase prose ("everybody owns a share") never land a fact here.
2374
+ // HANDOVER.md 2026-07-10 item 2 fix: the gate used to check ONLY the owner
2375
+ // name (own[1]) — "sam owns TaskController" WALLED entirely, because "sam"
2376
+ // isn't capitalized, even though "TaskController" (own[2], the owned thing)
2377
+ // is an obviously code-shaped proper name and just as strong a signal that
2378
+ // this isn't ordinary prose. Either side capitalized is now enough.
2348
2379
  const ownSrc = wrapped ?? raw.replace(/[.!?]+\s*$/, "");
2349
2380
  const own = ownSrc.match(OWNS_TEACH_RE);
2350
- if (own && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && (wrapped || /^[A-Z]/.test(own[1]))) {
2381
+ if (own && memoryDir && !QUESTION_LEAD_RE.test(ownSrc)
2382
+ && (wrapped || /^[A-Z]/.test(own[1]) || /^[A-Z]/.test(own[2]))) {
2351
2383
  const stored = await teachFact(memoryDir, sessionId, {
2352
2384
  subject: own[2], predicate: OWNED_BY_PREDICATE, object: own[1],
2353
2385
  });
@@ -2551,6 +2583,33 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
2551
2583
  const stored = await teachFact(memoryDir, sessionId, gv);
2552
2584
  if (stored) return stored;
2553
2585
  }
2586
+ } else if (!wrapped && memoryDir && !QUESTION_LEAD_RE.test(correctMisspellings(raw))) {
2587
+ // BARE path (HANDOVER.md 2026-07-10 item 2 fix): "grace mentors alan" — no
2588
+ // "remember"/"note" wrapper at all — used to silently reach neither this
2589
+ // frame NOR an honest miss, landing on the raw structural wall instead
2590
+ // (or, at exactly <=3 words with no code-ish token, the UNRELATED
2591
+ // isConversational() orientation card — see subjectIsNounOrPropn's own
2592
+ // docblock for why a plain wrapper-required gate can't safely widen to
2593
+ // bare sentences on shape alone: "tell me a joke" fits the identical SVO
2594
+ // shape and must never be reified). Only a POS-confirmed NOUN/PROPN
2595
+ // subject earns a try here — the same distinction that separates a
2596
+ // genuine declarative from an imperative request. The QUESTION_LEAD_RE
2597
+ // check runs the SAME correctMisspellings() pass ask.mjs's own typo
2598
+ // tolerance already uses (not `raw` itself) — found live: "wich modules
2599
+ // touch model.mjs" (a typo'd "which…" structural question, MISSPELLINGS-
2600
+ // table-corrected everywhere ELSE in this file) POS-tags its uncorrected
2601
+ // "wich" as a bare NOUN (wink's honest fallback for any unrecognized
2602
+ // token, not a real signal), which would otherwise mis-store it as a
2603
+ // fact instead of leaving it for the structural grammar's own typo-
2604
+ // tolerant retry to answer for real.
2605
+ const subjectWord = raw.match(/^([\w'-]+)/)?.[1];
2606
+ if (subjectWord && (await subjectIsNounOrPropn(subjectWord))) {
2607
+ const gv = await generalVerbTeach(raw);
2608
+ if (gv) {
2609
+ const stored = await teachFact(memoryDir, sessionId, gv);
2610
+ if (stored) return stored;
2611
+ }
2612
+ }
2554
2613
  }
2555
2614
 
2556
2615
  let payload = null;
@@ -2716,6 +2775,24 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
2716
2775
  // "what does the do" is not real input) — a natural stranger-opener that was one
2717
2776
  // token away from already working.
2718
2777
  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)|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))?)$/;
2778
+ /** HANDOVER.md 2026-07-10 item 3 (part 2): a bare "what is in here"/"what's in
2779
+ * here"/"whats in here" — the SAME orientation intent as META_ORIENT_RE's own
2780
+ * "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
2781
+ * pronoun "here" instead of a named noun (app/codebase/repo/…). ask.mjs's own
2782
+ * containment grammar parses "here" as a genuine pronoun object and, when a
2783
+ * focus IS standing, resolves it there exactly as intended — this regex is
2784
+ * ONLY ever tried when there is NO focus (see the call site's `!focus?.label`
2785
+ * gate), so that existing resolution path is completely untouched. With
2786
+ * nothing to resolve "here" against, ask.mjs's grammar instead renders the
2787
+ * honest but unhelpful "'here' needs a selected node…" miss — a poor answer
2788
+ * for a genuine first-time stranger who has never selected anything yet.
2789
+ * Tested against the NORMALIZED query (metaLane's call site runs
2790
+ * normalizeQuery first) rather than the raw text, so a preamble-wrapped
2791
+ * opener ("hey, first time trying this out - what is in here?") reaches this
2792
+ * exactly as the BARE "what is in here?" does — same preamble/filler-word
2793
+ * stripping ask.mjs's own grammar already applies before it ever sees the
2794
+ * pronoun. */
2795
+ const NO_FOCUS_WHATS_IN_HERE_RE = /^what(?:'s|s|\s+is)\s+in\s+here\??$/i;
2719
2796
 
2720
2797
  /** A SHORT memory summary (never a fact dump) for the bare "what do you know".
2721
2798
  * This branch only fires when rows.length === 0 — i.e. precisely the case where
@@ -2813,7 +2890,7 @@ async function moduleOrientLane(query, { graph }) {
2813
2890
  return { text: moduleOverviewText(graph, ind), via: "meta" };
2814
2891
  }
2815
2892
 
2816
- async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null }) {
2893
+ async function metaLane(query, { graph, memoryDir, last = null, templates = null, vocabHint = null, focus = null }) {
2817
2894
  const q = String(query).trim().toLowerCase().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
2818
2895
  if (WHAT_KNOW_RE.test(q) || q === "what have you learned" || q === "what have you learnt") {
2819
2896
  return { text: await memorySummary(memoryDir, graph), via: "meta" };
@@ -2829,6 +2906,22 @@ async function metaLane(query, { graph, memoryDir, last = null, templates = null
2829
2906
  const text = orientationText(graph, templates, vocabHint);
2830
2907
  return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
2831
2908
  }
2909
+ // HANDOVER.md 2026-07-10 item 3 (part 2): a bare "what is in here" with NO
2910
+ // standing focus — see NO_FOCUS_WHATS_IN_HERE_RE's own docblock. Tested
2911
+ // against normalizeQuery's output (the SAME normalization ask.mjs's own
2912
+ // grammar runs before it ever sees the "here" pronoun), not the raw `q`
2913
+ // above, so a preamble-wrapped opener reaches it identically to the bare
2914
+ // form. Gated on !focus?.label so a real standing focus (where ask.mjs
2915
+ // already resolves "here" against it) is completely unaffected — this only
2916
+ // ADDS a fallback for the true first-turn case, never changes resolution
2917
+ // when a focus exists.
2918
+ if (!focus?.label) {
2919
+ const stripped = normalizeQuery(String(query)).trim().replace(/[?.!]+$/, "").trim();
2920
+ if (NO_FOCUS_WHATS_IN_HERE_RE.test(stripped)) {
2921
+ const text = orientationText(graph, templates, vocabHint);
2922
+ return { text: last?.answer === text ? META_ORIENT_REPEAT_ONELINER : text, via: "meta" };
2923
+ }
2924
+ }
2832
2925
  // Bug E: an arbitrary "what does <term> do" that META_ORIENT_RE's closed noun
2833
2926
  // list didn't claim — try the module-grain overview before falling through to
2834
2927
  // the author-sha check below (disjoint triggers; order doesn't matter, but
@@ -5814,7 +5907,28 @@ async function completionsRescueAnswer(query, { memoryDir, graph }) {
5814
5907
  if (!term) return null;
5815
5908
  try {
5816
5909
  const { generateCompletion } = await import("./completions/complete.mjs");
5817
- const result = await generateCompletion(memoryDir, term, { query: term, graph });
5910
+ // HANDOVER.md item 1: broadSearch (src/completions/search.mjs) already accepts an
5911
+ // optional Repository-Interface `graphService` — its own docblock names
5912
+ // createGraphService(graph) (src/providers/graph-service.mjs) as the reference
5913
+ // shape — but until now nothing ever handed one through, so this lane could only
5914
+ // ever see memory BLOCKS saved via an explicit saveBlock() call. Ordinary chat
5915
+ // teaching/asking never calls saveBlock(), so a subject's first-ever mention in a
5916
+ // session always declined here, no matter how much the already-loaded graph (and
5917
+ // any taught Facts about it) actually knew. createCompletionsGraphAdapter
5918
+ // (src/completions/graph-adapter.mjs) wraps the SAME graph object this turn already
5919
+ // has in scope (runTurn's own `graph` param, loaded once per session by the chat
5920
+ // shell) plus this repo's already-loaded Fact store — no re-load, no new search
5921
+ // machinery, just handing broadSearch the adapter it was always built to accept.
5922
+ // Loading memory here (rather than letting generateCompletion load it itself at
5923
+ // Stage 3) lets the SAME loaded payload double as the adapter's Fact-search source;
5924
+ // passed straight through as opts.memory so Stage 3 doesn't re-read it a second
5925
+ // time. A null/empty graph (no code entities loaded yet) or empty memory (no Facts
5926
+ // taught yet) degrades to the pre-existing block-only search, exactly as before.
5927
+ const { createCompletionsGraphAdapter } = await import("./completions/graph-adapter.mjs");
5928
+ const { loadMemory } = await import("./memory/core.mjs");
5929
+ const memory = await loadMemory(memoryDir);
5930
+ const graphService = createCompletionsGraphAdapter(graph, memory);
5931
+ const result = await generateCompletion(memoryDir, term, { query: term, graph, memory, graphService });
5818
5932
  if (!result || result.declined || !result.text) return null; // honest decline — never fabricate
5819
5933
  return { text: result.text };
5820
5934
  } catch {
@@ -6157,7 +6271,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
6157
6271
  // codebase", "how do i start") → a summary / orientation, answered before the
6158
6272
  // fact-dump readers so "what do you know" gets a summary, not raw facts.
6159
6273
  if (miss) {
6160
- const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint });
6274
+ const meta = await metaLane(query, { graph, memoryDir, last, templates, vocabHint, focus });
6161
6275
  if (meta) {
6162
6276
  answer = meta.text; via = meta.via; recordMiss = false; handled = true;
6163
6277
  note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
@@ -0,0 +1,118 @@
1
+ // completions/graph-adapter.mjs — HANDOVER.md item 1: the graphService-shaped adapter
2
+ // src/completions/search.mjs's broadSearch() was always built to accept — its own
3
+ // docblock names createGraphService(graph) (src/providers/graph-service.mjs) as the
4
+ // reference shape — but until now nothing in live chat ever constructed and passed one
5
+ // through (src/chat.mjs's completionsRescueAnswer called generateCompletion() with no
6
+ // `graphService` at all). Without it, broadSearch could only ever see memory BLOCKS
7
+ // saved via an explicit saveBlock() call — never the already-loaded code graph, and
8
+ // never a taught Fact — so "give me a detailed summary of how X works" declined for any
9
+ // subject on its first real mention in a session, no matter how much the graph or
10
+ // taught Facts actually knew about it.
11
+ //
12
+ // createCompletionsGraphAdapter(graph, memory) wraps TWO already-loaded stores (never
13
+ // re-loads either from disk — both are handed in by the caller, exactly as loaded for
14
+ // this turn):
15
+ //
16
+ // - .search(q, {limit}) delegates straight to createGraphService(graph).search() —
17
+ // the same ranked lexical module/symbol search every other Repository-Interface
18
+ // consumer uses (src/codegraph.mjs's searchModulesRanked/scoreSymbolsRanked under
19
+ // the hood). No new search machinery.
20
+ //
21
+ // - .ask(q) does NOT delegate to createGraphService(graph).ask() (src/ask.mjs) —
22
+ // that engine is a mechanical NATURAL-LANGUAGE QUESTION grammar ("which functions
23
+ // call X", "what does X import"), and broadSearch always calls .ask() with the
24
+ // bare SUBJECT TERM itself ("TaskController"), not a question. Tried live: that
25
+ // produces an honest but useless "couldn't parse this as a graph question"
26
+ // rephrase-hint every time — real text, but not about the subject, and it would
27
+ // pollute the completion with noise. Instead .ask() here builds real sentences
28
+ // from two sources that a bare term CAN resolve against directly:
29
+ // 1. resolveSymbol + renderDescribe (src/codegraph.mjs) — the SAME graph-only
30
+ // renderer src/server.mjs's own tmct_describe tool uses: real facts (defining
31
+ // module, contains, inherits, calls, tests, attributes, …), never invented.
32
+ // 2. readFactRows(memory) (src/memory/core.mjs) — any TAUGHT Fact whose subject
33
+ // or object mentions the term. This is the one source the pipeline had NO
34
+ // path to before at all: Stage 3 (inferRelations) only ever augments groups
35
+ // that already exist from Stage 1's hits, so a subject with real taught Facts
36
+ // but zero blocks/code-graph hits still surfaced nothing.
37
+ // svc.ask() is still tried last, but its content is kept ONLY when it genuinely
38
+ // parsed (tmct_ask.miss === false) — e.g. the rare case where the bare term happens
39
+ // to also be a real registered question shape — never its own rephrase-hint noise.
40
+ //
41
+ // Every sentence this adapter returns traces to a real graph edge/attribute or a real
42
+ // taught Fact — never invented, matching src/completions/'s extractive-only discipline
43
+ // (see complete.mjs's own file header).
44
+
45
+ import { createGraphService } from "../providers/graph-service.mjs";
46
+ import { resolveSymbol, renderDescribe } from "../codegraph.mjs";
47
+ import { readFactRows } from "../memory/core.mjs";
48
+
49
+ /** renderDescribe() renders one LINE per fact (label header, each attribute, each edge
50
+ * group) with no terminal punctuation of its own — fine for its own "compact plain-text
51
+ * description for an agent consumer" purpose, but src/completions/rank.mjs's
52
+ * splitSentences() treats each line as its own candidate sentence, and complete.mjs
53
+ * joins kept sentences with a single space — so two adjacent kept lines without a
54
+ * period between them would otherwise read as one run-on clause. Ensuring every line
55
+ * ends in terminal punctuation here (never rewording/reordering the line itself) is
56
+ * the cheapest fix that stays entirely inside this adapter, touching neither
57
+ * renderDescribe() (server.mjs's tmct_describe tool relies on its current line shape)
58
+ * nor rank.mjs/complete.mjs's own join logic. */
59
+ function withTerminalPunctuation(text) {
60
+ return String(text || "")
61
+ .split("\n")
62
+ .map((line) => line.trim())
63
+ .filter(Boolean)
64
+ .map((line) => (/[.!?]$/.test(line) ? line : `${line}.`))
65
+ .join("\n");
66
+ }
67
+
68
+ /**
69
+ * @param {object|null} graph a parseEntities() result (src/codegraph.mjs), or null
70
+ * when no code graph is loaded — search()/the describe-half of ask() then honestly
71
+ * contribute nothing, rather than throwing.
72
+ * @param {object|null} [memory=null] a loadMemory() payload (src/memory/core.mjs), or
73
+ * null when there's no Fact store to search — the Fact-half of ask() then honestly
74
+ * contributes nothing.
75
+ * @returns {{search: Function, ask: Function}} a Repository-Interface-shaped
76
+ * graphService satisfying src/completions/search.mjs's broadSearch() contract.
77
+ */
78
+ export function createCompletionsGraphAdapter(graph, memory = null) {
79
+ const svc = graph ? createGraphService(graph) : null;
80
+
81
+ return {
82
+ search(q, { limit } = {}) {
83
+ if (!svc) return { ok: true, value: { results: [] } };
84
+ return svc.search(q, { limit });
85
+ },
86
+
87
+ ask(q) {
88
+ const term = String(q || "").trim();
89
+ if (!term) return { ok: true, value: { content: "" } };
90
+ const sentences = [];
91
+
92
+ if (svc) {
93
+ const { match, candidates } = resolveSymbol(graph, term);
94
+ if (match) sentences.push(withTerminalPunctuation(renderDescribe(graph, match, { candidates })));
95
+ }
96
+
97
+ if (memory) {
98
+ const needle = term.toLowerCase();
99
+ for (const row of readFactRows(memory)) {
100
+ if (!row.subject || !row.predicate || !row.object) continue;
101
+ const haystack = `${row.subject} ${row.object}`.toLowerCase();
102
+ if (!haystack.includes(needle)) continue;
103
+ sentences.push(`${row.subject} ${row.predicate} ${row.object}.`.trim());
104
+ }
105
+ }
106
+
107
+ if (svc) {
108
+ const res = svc.ask(term);
109
+ if (res?.ok && res.value?.tmct_ask?.miss === false && res.value.content) {
110
+ sentences.push(res.value.content);
111
+ }
112
+ }
113
+
114
+ if (!sentences.length) return { ok: true, value: { content: "" } };
115
+ return { ok: true, value: { content: sentences.join(" ") } };
116
+ },
117
+ };
118
+ }
@@ -191,8 +191,19 @@ const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|gre
191
191
  * §3: the vague-opener family a genuine first-time stranger types). Same
192
192
  * delimiter-required discipline as GREETING/THANKS/ACK_PREAMBLE_RE above —
193
193
  * a bare "just poking around" with no question stays small-talk (this file
194
- * never claims a turn that has no remainder to hand back). */
195
- const BROWSING_PREAMBLE_RE = /^just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)\s*[,.—–-]\s*(.+)$/i;
194
+ * never claims a turn that has no remainder to hand back).
195
+ * HANDOVER.md 2026-07-10 item 3: "first time trying this out"/"first time
196
+ * using this"/"first time here" is the SAME self-orientation species — a
197
+ * genuine stranger's opener, just phrased around their own inexperience
198
+ * rather than what they're doing right now — found live as "hey, first
199
+ * time trying this out - what is in here?" falling straight to the raw
200
+ * grammar wall (GREETING_PREAMBLE_RE peels "hey,", but nothing recognized
201
+ * the remainder as a preamble at all). Added as a sibling alternative in
202
+ * the SAME regex/capture group, so it strips into the identical downstream
203
+ * shape ("just poking around, X" and "first time trying this out, X" both
204
+ * hand back the bare "X" for the ordinary pipeline to answer) rather than a
205
+ * new frame with its own behavior. */
206
+ const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here))\s*[,.—–-]\s*(.+)$/i;
196
207
  /** A repeated leading HEDGE ADVERB ("maybe", "possibly", "perhaps") ahead of a
197
208
  * polite request verb — the sibling of ACK_PREAMBLE_RE for HEDGING rather than
198
209
  * acknowledging (Tier 6 playtest §3's own stacked-politeness example: "could