@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 +1 -1
- package/src/chat.mjs +121 -7
- package/src/completions/graph-adapter.mjs +118 -0
- package/src/interpret/normalize.mjs +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "1.5.
|
|
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:
|
|
2347
|
-
// "who owns <X>" READ question and ordinary
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|