@polycode-projects/the-mechanical-code-talker 3.1.0 → 3.1.3
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/README.md +2 -0
- package/corpus/seon/relations.jsonl +2 -0
- package/package.json +3 -1
- package/src/adapters/repository-interface.mjs +3 -0
- package/src/domain/ask-vocab.mjs +23 -0
- package/src/domain/ask.mjs +63 -13
- package/src/domain/code-explorer-hints.mjs +4 -0
- package/src/domain/codegraph.mjs +25 -17
- package/src/domain/codeplan/graph-delta.mjs +1 -0
- package/src/domain/concept.mjs +16 -0
- package/src/domain/interpret/normalize.mjs +16 -1
- package/src/domain/module-paths.mjs +6 -4
- package/src/domain/paraphrase-ing8.mjs +195 -0
- package/src/domain/real-word-collisions.json +1 -1
- package/src/domain/router/resolver.mjs +2 -0
- package/src/services/chat-session.mjs +5 -0
- package/src/services/chat.mjs +207 -32
- package/src/services/finish.mjs +6 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +113 -113
- package/src/tools/handlers/kit.mjs +5 -3
- package/src/tools/handlers/tmct-context.mjs +3 -2
- package/src/tools/handlers/tmct-export.mjs +9 -3
- package/src/tools/handlers/tmct-members.mjs +1 -1
- package/src/tools/handlers/tmct-search.mjs +1 -0
- package/src/tools/schema-docs.mjs +10 -0
- package/src/tools/server.mjs +9 -4
package/src/services/chat.mjs
CHANGED
|
@@ -128,6 +128,8 @@ const GOAL_BY_KIND = {
|
|
|
128
128
|
touchesSymbol: "understand commit/change history",
|
|
129
129
|
cochange: "understand change-coupling between modules",
|
|
130
130
|
reexports: "understand a module's public exports/API surface",
|
|
131
|
+
serves: "understand which component provides or backs another",
|
|
132
|
+
denotes: "understand which vocabulary term names a code entity",
|
|
131
133
|
};
|
|
132
134
|
const goalNoun = (entityType) => (entityType ? `${String(entityType).toLowerCase()}(s)` : "entities");
|
|
133
135
|
|
|
@@ -257,7 +259,12 @@ function renderNarration(trace, { record, detail, fallbackGoal }) {
|
|
|
257
259
|
* renderVerbose) see the exact same text a narrate:false run would have
|
|
258
260
|
* produced; narrate is purely additive to what's PRINTED, never to what's
|
|
259
261
|
* REMEMBERED. No-op (returns `result` unchanged, by reference) when `trace`
|
|
260
|
-
* is null (narrate off) or empty (nothing was traced).
|
|
262
|
+
* is null (narrate off) or empty (nothing was traced).
|
|
263
|
+
*
|
|
264
|
+
* The same trace block also rides the result as its own `narration` field, so
|
|
265
|
+
* an embedding caller reads the answer and the trace apart without splitting
|
|
266
|
+
* the printed text on NARRATE_MARKER. `answer` keeps the glued form it always
|
|
267
|
+
* had — the field is additive, and absent on a turn that traced nothing. */
|
|
261
268
|
function withNarration(result, trace, fallbackGoal) {
|
|
262
269
|
if (!trace || !trace.length) return result;
|
|
263
270
|
const narrative = renderNarration(trace, { record: result.record, detail: result.detail, fallbackGoal });
|
|
@@ -265,7 +272,7 @@ function withNarration(result, trace, fallbackGoal) {
|
|
|
265
272
|
const logLines = Array.isArray(result.logLines)
|
|
266
273
|
? result.logLines.map((l) => (l === result.answer ? answer : l))
|
|
267
274
|
: result.logLines;
|
|
268
|
-
return { ...result, answer, logLines };
|
|
275
|
+
return { ...result, answer, narration: narrative, logLines };
|
|
269
276
|
}
|
|
270
277
|
|
|
271
278
|
/** A short "Goal (inferred): …" line appended (never prepended) after every
|
|
@@ -1111,14 +1118,26 @@ const CAPABILITY_PHRASES = [
|
|
|
1111
1118
|
// anything nameable. Answered the same as any other vague opener: sure,
|
|
1112
1119
|
// here's what I can help with.
|
|
1113
1120
|
/^can i ask (?:you\s+)?something(?:\s+random)?\??$/i,
|
|
1121
|
+
// The same orientation request with the SUBJECT flipped to "I" — "what can I
|
|
1122
|
+
// ask you" rather than "what can you do". Every entry above reads "you" as
|
|
1123
|
+
// the subject, so the flipped phrasings matched nothing and the bare "I"
|
|
1124
|
+
// token went to the code-graph lane as a module name. The optional
|
|
1125
|
+
// about-tail reuses META_ORIENT_RE's own closed self-referential noun list,
|
|
1126
|
+
// so "what questions can I ask about <a real module>" is untouched.
|
|
1127
|
+
/^what can (?:i|we) ask(?:\s+(?:you|u))?(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
|
|
1128
|
+
/^what questions can (?:i|we) ask(?:\s+(?:you|u))?(?:\s+about (?:this|the)\s+(?:app|codebase|repo|repository|project|code))?\??$/i,
|
|
1129
|
+
/^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
1130
|
];
|
|
1115
1131
|
/** IDENTITY questions — "who/what are you", by name, in plain or ESL-ish phrasing.
|
|
1116
1132
|
* Routed to a self-description (identity-self) that works regardless of graph
|
|
1117
1133
|
* state, never the code-graph deflection. */
|
|
1118
1134
|
const IDENTITY_PHRASES = [
|
|
1119
|
-
|
|
1135
|
+
// The optional adverb ("what EVEN is this thing", "what REALLY is this") is
|
|
1136
|
+
// emphasis on the same question — without it the casual spelling missed the
|
|
1137
|
+
// closed set entirely and the bare "this"/"I" token went to the graph lane.
|
|
1138
|
+
/^who are you\??$/i, /^what (?:even |really |actually )?(is|are|r) (this|you)\??$/i,
|
|
1120
1139
|
/^what('?s| is) your name\??$/i, /^what exactly are you\??$/i,
|
|
1121
|
-
/^(tell me about|introduce) yourself\??$/i, /^what is this thing\??$/i,
|
|
1140
|
+
/^(tell me about|introduce) yourself\??$/i, /^what (?:even |really |actually )?is this thing\??$/i,
|
|
1122
1141
|
/^what am i (talking|speaking|chatting) (to|with)\??$/i,
|
|
1123
1142
|
/^you are what\??$/i, /^what thing (are|is) you\??$/i,
|
|
1124
1143
|
// "explain [to me|please]* what (you are|this is)" in EITHER word order — a
|
|
@@ -1248,6 +1267,34 @@ function aiIdentityMatch(raw) {
|
|
|
1248
1267
|
return splitClauses(text).some((clause) => AI_IDENTITY_PHRASES.some((re) => re.test(clause)));
|
|
1249
1268
|
}
|
|
1250
1269
|
|
|
1270
|
+
/** The self-referential tail a casual identity question trails after a comma
|
|
1271
|
+
* ("what even is this thing, like what does it do?"). It names no term at all
|
|
1272
|
+
* — "it"/"this"/"you" is the thing already asked about in the first clause —
|
|
1273
|
+
* so it adds nothing to answer and only has to be RECOGNIZED, not routed. */
|
|
1274
|
+
const SELF_REFERENTIAL_TAIL_RE = /^(?:like\s+|so\s+|and\s+)?what (?:does|do|can) (?:it|this|you|u) do\??$/i;
|
|
1275
|
+
|
|
1276
|
+
/** IDENTITY_PHRASES matched against the raw turn, its preamble-peeled twin, or
|
|
1277
|
+
* a comma-joined identity clause trailed only by self-referential filler.
|
|
1278
|
+
*
|
|
1279
|
+
* The peeled check mirrors what CAPABILITY_PHRASES' own dispatch already does
|
|
1280
|
+
* ("hello there, what am I talking to?" is an exact identity question behind a
|
|
1281
|
+
* greeting frame, and every entry is anchored, so the frame alone sank it).
|
|
1282
|
+
*
|
|
1283
|
+
* The clause check is deliberately narrower than aiIdentityMatch's: it splits
|
|
1284
|
+
* on commas/"and" (conversationalClauses), which a real graph question can sit
|
|
1285
|
+
* on the far side of, so a match requires the FIRST clause to be an identity
|
|
1286
|
+
* question AND every later clause to be self-referential filler. "what is
|
|
1287
|
+
* this, and which modules import walk.mjs" therefore stays a graph query. */
|
|
1288
|
+
function identityPhraseMatch(raw) {
|
|
1289
|
+
const text = String(raw);
|
|
1290
|
+
const anchored = (s) => IDENTITY_PHRASES.some((re) => re.test(s));
|
|
1291
|
+
if (anchored(text) || anchored(applyPreambleFrames(text))) return true;
|
|
1292
|
+
const clauses = conversationalClauses(applyPreambleFrames(text).trim());
|
|
1293
|
+
if (clauses.length < 2) return false;
|
|
1294
|
+
return anchored(clauses[0].replace(/[?.!]+$/, ""))
|
|
1295
|
+
&& clauses.slice(1).every((c) => SELF_REFERENTIAL_TAIL_RE.test(c));
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1251
1298
|
/** "Do you have feelings/emotions" — with no closed-set match, this would
|
|
1252
1299
|
* otherwise misfire into a literal module-name lookup for the bare noun
|
|
1253
1300
|
* ("no module matching 'feelings' found in the index") — a wrong-flavor
|
|
@@ -2009,7 +2056,7 @@ function conversationalTurn(line, ctx) {
|
|
|
2009
2056
|
{ lane: "help" },
|
|
2010
2057
|
);
|
|
2011
2058
|
}
|
|
2012
|
-
if (
|
|
2059
|
+
if (identityPhraseMatch(raw)) {
|
|
2013
2060
|
note(ctx.trace, "goal: identity — who/what tmct is, not a capability listing");
|
|
2014
2061
|
note(ctx.trace, "lane: conversational — identity (IDENTITY_PHRASES closed set)");
|
|
2015
2062
|
return mk(t(T_IDENTITY_SELF), { lane: "help" });
|
|
@@ -2126,7 +2173,12 @@ const ORIENTATION_EMPTY_FALLBACK = "I'm tmct — a deterministic, offline chat a
|
|
|
2126
2173
|
* branch uses, so there is exactly one copy of that wording to keep in sync, not
|
|
2127
2174
|
* two hand-duplicated strings. */
|
|
2128
2175
|
function orientationText(graph, templates, vocabHint) {
|
|
2129
|
-
|
|
2176
|
+
// A null (never-loaded) graph reads as "unknown, not empty" to noCodeGraph,
|
|
2177
|
+
// which is the right call for the greeting/orientation CARD — but there is no
|
|
2178
|
+
// entity count to render from one either, so the empty wording is the only
|
|
2179
|
+
// truthful thing left to say. Without this the entity tally below threw on a
|
|
2180
|
+
// graph-less runTurn.
|
|
2181
|
+
if (!graph || noCodeGraph(graph)) {
|
|
2130
2182
|
return tRender(templates, T_ORIENTATION_EMPTY, { vocabHint }) ?? ORIENTATION_EMPTY_FALLBACK;
|
|
2131
2183
|
}
|
|
2132
2184
|
const by = (cls) => (graph.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
@@ -2190,6 +2242,8 @@ const MISS_EXAMPLES = {
|
|
|
2190
2242
|
define: ['"where is <name> defined"', '"where is <name> mentioned"'],
|
|
2191
2243
|
meaning: ['"what is a <ClassName>"', '"what does <term> mean"'],
|
|
2192
2244
|
count: ['"how many classes are there"', '"how many modules are there"'],
|
|
2245
|
+
serve: ['"what does <name> serve"', '"what serves <name>"'],
|
|
2246
|
+
denote: ['"what does <name> denote"', '"what denotes <name>"'],
|
|
2193
2247
|
};
|
|
2194
2248
|
const MISS_DEFAULT = ['"which modules import <name>"', '"what calls <name>"'];
|
|
2195
2249
|
|
|
@@ -2201,6 +2255,8 @@ function tailoredExamples(q) {
|
|
|
2201
2255
|
const has = (re) => re.test(q);
|
|
2202
2256
|
if (has(/\bimport/)) return MISS_EXAMPLES.import;
|
|
2203
2257
|
if (has(/\bexport/)) return MISS_EXAMPLES.export;
|
|
2258
|
+
if (has(/\bserv(?:e|es|ed|ing|ice)\b/)) return MISS_EXAMPLES.serve;
|
|
2259
|
+
if (has(/\bdenot(?:e|es|ed|ing|ation)\b/)) return MISS_EXAMPLES.denote;
|
|
2204
2260
|
if (has(/\b(?:calls?|caller|callee)\b/)) return MISS_EXAMPLES.call;
|
|
2205
2261
|
if (has(/\b(?:tests?|cover|covering|tested)\b/)) return MISS_EXAMPLES.test;
|
|
2206
2262
|
if (has(/\b(?:inherit|subclass|extends?|superclass|hierarchy|base class|parent class)\b/)) return MISS_EXAMPLES.inherit;
|
|
@@ -2245,7 +2301,14 @@ const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+
|
|
|
2245
2301
|
// stayed null) before ever trying the unknown-subject/object mint fallbacks
|
|
2246
2302
|
// below — the SAME sentence typed without the period worked. Mirrors
|
|
2247
2303
|
// UNKNOWN_SUBJECT_RE's own identical tolerance, added for the same reason.
|
|
2248
|
-
|
|
2304
|
+
// This is the gate that decides whether a bare declarative becomes a teach
|
|
2305
|
+
// payload at all, so a complement narrower than UNKNOWN_SUBJECT_RE's own object
|
|
2306
|
+
// capture kept a multi-word class name out of the mint fallbacks downstream no
|
|
2307
|
+
// matter how wide their captures were. A multi-word complement is admitted ONLY
|
|
2308
|
+
// behind an article — "is A unit of work" names a class, while the article-less
|
|
2309
|
+
// "is nice today" / "are fast animals" is ordinary prose that happens to carry a
|
|
2310
|
+
// copula, and admitting THAT grounds filler sentences as facts.
|
|
2311
|
+
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:(?:a |an )[\w-]+(?: [\w-]+){0,2}|[\w-]+)(?: too)?[.!?]*$/i;
|
|
2249
2312
|
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
2250
2313
|
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
2251
2314
|
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
@@ -2842,7 +2905,11 @@ export { singularizeSurface };
|
|
|
2842
2905
|
* and unknownObjectFallback's own docblocks) — a proper noun that happens to
|
|
2843
2906
|
* end in "s" ("redis") naively strips to "redi" if singularized on an "is"
|
|
2844
2907
|
* sentence, where no such fold is ever needed.
|
|
2845
|
-
* Y (the object) is
|
|
2908
|
+
* Y (the object) is ONE TO THREE tokens, so a natural multi-word class name
|
|
2909
|
+
* ("every Function is a unit of work") reaches the mint instead of failing
|
|
2910
|
+
* the match outright and falling to the grammar wall; the greedy quantifier
|
|
2911
|
+
* plus the end anchor keep a longer trailing phrase ("rex is a dog in the
|
|
2912
|
+
* garden") rejected exactly as before.
|
|
2846
2913
|
* X (the subject) is ONE OR TWO tokens, to cover a natural 2-word noun
|
|
2847
2914
|
* phrase ("vulcan gizmo is a tool"), the same class of gap OWNS_TEACH_RE's
|
|
2848
2915
|
* own object needed widening for, above. The greedy quantifier tries the
|
|
@@ -2867,7 +2934,7 @@ export { singularizeSurface };
|
|
|
2867
2934
|
* captures themselves are unaffected (`[\w-]+` never included the period in
|
|
2868
2935
|
* the first place), so this only widens WHICH sentences reach the match,
|
|
2869
2936
|
* 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;
|
|
2937
|
+
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
2938
|
|
|
2872
2939
|
/** ISA-family predicates (mirrors the private ISA_PREDICATES set defined near
|
|
2873
2940
|
* memoryFacts, below, at module scope — both are simple top-level consts
|
|
@@ -3124,6 +3191,12 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
3124
3191
|
* tagging surprise, degrades to a null tag treated as "no signal" (never a
|
|
3125
3192
|
* decline) — matching every other optional-adapter path in this file. */
|
|
3126
3193
|
async function objectReadsAsNonNoun(word) {
|
|
3194
|
+
// A MULTI-WORD complement only ever reaches here from behind an article, so
|
|
3195
|
+
// it is a noun phrase by construction and the adjective question doesn't
|
|
3196
|
+
// arise. Asking wink anyway tags the joined string as one opaque token and
|
|
3197
|
+
// reads back nonsense ("key value store" → ADJ), which then diverted a plain
|
|
3198
|
+
// class claim into the property lane.
|
|
3199
|
+
if (/\s/.test(String(word || "").trim())) return false;
|
|
3127
3200
|
try {
|
|
3128
3201
|
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
3129
3202
|
const adapter = nlpAdapter();
|
|
@@ -4766,12 +4839,18 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4766
4839
|
if (stored) return stored;
|
|
4767
4840
|
}
|
|
4768
4841
|
// PASSIVE ownership — "<X> is owned by <Name>". Same bare-form gate as the
|
|
4769
|
-
// active shape just above
|
|
4770
|
-
//
|
|
4771
|
-
//
|
|
4772
|
-
//
|
|
4842
|
+
// active shape just above, reading EITHER side and requiring no
|
|
4843
|
+
// interrogative lead, so "is TaskController owned by anyone" (a genuine
|
|
4844
|
+
// yes/no QUESTION, handled by factReadBack instead) never lands a bogus
|
|
4845
|
+
// fact here. Reading both sides matters as much as it does for the active
|
|
4846
|
+
// shape, and the SUBJECT side gets the path test too: "src/domain/
|
|
4847
|
+
// lexicon.mjs is owned by antony" states ownership just as plainly as a
|
|
4848
|
+
// capitalized owner does, while "the car is owned by john" still declines.
|
|
4773
4849
|
const ownPassive = ownSrc.match(OWNS_PASSIVE_TEACH_RE);
|
|
4774
|
-
|
|
4850
|
+
const ownPassiveSubject = ownPassive?.[1]?.trim() ?? "";
|
|
4851
|
+
if (ownPassive && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion
|
|
4852
|
+
&& (wrapped || /^[A-Z]/.test(ownPassive[2]) || /^[A-Z]/.test(ownPassiveSubject)
|
|
4853
|
+
|| MODULE_PATH_RE.test(ownPassiveSubject))) {
|
|
4775
4854
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
4776
4855
|
subject: ownPassive[1], predicate: OWNED_BY_PREDICATE, object: ownPassive[2],
|
|
4777
4856
|
});
|
|
@@ -5469,7 +5548,7 @@ const WHAT_KNOW_RE = /^(?:what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?|what
|
|
|
5469
5548
|
// do" needs the noun OPTIONAL after "this" (kept REQUIRED after "the") or it
|
|
5470
5549
|
// falls through to MODULE_ORIENT_RE, which fails to resolve "this" as an
|
|
5471
5550
|
// 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))?)$/;
|
|
5551
|
+
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
5552
|
/** A bare "what is in here"/"what's in here"/"whats in here" — the SAME
|
|
5474
5553
|
* orientation intent as META_ORIENT_RE's own
|
|
5475
5554
|
* "what's in this repo"-shaped members, just phrased with the CONTEXT_WORDS
|
|
@@ -5517,17 +5596,19 @@ async function memorySummary(memoryDir, graph) {
|
|
|
5517
5596
|
// #2(e) MODULE-GRAIN OVERVIEW. META_ORIENT_RE (above) can't match a module
|
|
5518
5597
|
// path/symbol name, so "what does app/lib/a.mjs do" needs its own lane.
|
|
5519
5598
|
// CASE-PRESERVING: reads the ORIGINAL query text, never metaLane's lowercased `q`.
|
|
5520
|
-
/**
|
|
5521
|
-
* store module do exactly?", "
|
|
5522
|
-
*
|
|
5523
|
-
*
|
|
5524
|
-
* raw grammar wall even though the
|
|
5525
|
-
* (used elsewhere in the file) never
|
|
5526
|
-
* at all. Mirrors
|
|
5527
|
-
* just below: closed, optional, single-lane blast
|
|
5528
|
-
* X do" still matches with
|
|
5529
|
-
|
|
5530
|
-
|
|
5599
|
+
/** One intensifier/filler adverb sitting either side of "do"/"does" ("what does
|
|
5600
|
+
* the store module do exactly?", "what does tasks.mjs actually do
|
|
5601
|
+
* internally", "what X does really") — the closed-form anchors below pin
|
|
5602
|
+
* "do"/"does" against the term on one side and the optional "?" on the
|
|
5603
|
+
* other, so either extra word would hit the raw grammar wall even though the
|
|
5604
|
+
* shared FILLER_WORDS/normalizeQuery pass (used elsewhere in the file) never
|
|
5605
|
+
* sees this lane's case-preserving text at all. Mirrors
|
|
5606
|
+
* MODULE_ORIENT_POLITENESS_RE just below: closed, optional, single-lane blast
|
|
5607
|
+
* radius — a bare "what does X do" still matches with both slots empty. The
|
|
5608
|
+
* PRE-verb slot also keeps an adverb out of the term capture, so
|
|
5609
|
+
* "tasks.mjs actually" never reaches entity resolution as one name. */
|
|
5610
|
+
const ORIENT_ADVERB_SLOT_RE = "(?:\\s+(?:exactly|really|actually|truly|anyway|internally|properly))?";
|
|
5611
|
+
const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)${ORIENT_ADVERB_SLOT_RE}\\s+do${ORIENT_ADVERB_SLOT_RE}\\??$`, "i");
|
|
5531
5612
|
/** The SUBJECT-FIRST word order of the SAME question ("what saveStore does" vs
|
|
5532
5613
|
* "what does saveStore do") — a perfectly natural alternate phrasing of an
|
|
5533
5614
|
* ALREADY-recognized intent that would otherwise hit the raw grammar wall
|
|
@@ -5537,7 +5618,7 @@ const MODULE_ORIENT_RE = new RegExp(`^what\\s+does\\s+(.+?)\\s+do${TRAILING_ADVE
|
|
|
5537
5618
|
* UNIQUE graph entity or this lane declines) is what keeps this loose an
|
|
5538
5619
|
* ending safe — a syntactic match against a term that isn't a real entity
|
|
5539
5620
|
* simply falls through unchanged, same as every other lane in this file. */
|
|
5540
|
-
const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${
|
|
5621
|
+
const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)${ORIENT_ADVERB_SLOT_RE}\\s+does${ORIENT_ADVERB_SLOT_RE}\\??$`, "i");
|
|
5541
5622
|
/** "whats X do" / "what's X do" / "what is X do" — the CONTRACTED phrasing of
|
|
5542
5623
|
* "what does X do", where the auxiliary collapses into the "what's"/"whats"
|
|
5543
5624
|
* opener and "do" trails the term. MODULE_ORIENT_RE's own "does BEFORE the
|
|
@@ -5548,7 +5629,7 @@ const MODULE_ORIENT_SVO_RE = new RegExp(`^what\\s+(.+?)\\s+does${TRAILING_ADVERB
|
|
|
5548
5629
|
* that is not a real unique entity (a pronoun subject "whats it do", a
|
|
5549
5630
|
* non-word) simply declines. The "what(?:'s|s|\s+is)" opener mirrors
|
|
5550
5631
|
* 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${
|
|
5632
|
+
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
5633
|
// Purpose/identity phrasing: "whats X for"/"what's X
|
|
5553
5634
|
// about"/"what is X for", the sibling of "what does X do" that asks for the
|
|
5554
5635
|
// SAME module-grain overview. Deliberately does NOT claim the literal noun
|
|
@@ -10927,9 +11008,33 @@ const STACCATO_PRONOUN_RE = /^(?:and|also|so|then|now)\s+(it|that|this|those|the
|
|
|
10927
11008
|
const DESCRIBE_GRAIN_WORD_RE = new RegExp(
|
|
10928
11009
|
`^(?:(?:the|a|an)\\s+)?(.+?)\\s+(${Object.keys(ENTITY_TO_TYPE).join("|")})$`, "i",
|
|
10929
11010
|
);
|
|
11011
|
+
/** A LEADING noise phrase glued onto the captured term by an outer bridge, the
|
|
11012
|
+
* mirror of the trailing grain word DESCRIBE_GRAIN_WORD_RE strips. "give me
|
|
11013
|
+
* the context for src/core/store.mjs" reaches this lane as "describe context
|
|
11014
|
+
* for src/core/store.mjs" (normalize.mjs's show/give-me frame rewrites the
|
|
11015
|
+
* whole tail into the term), and every resolver downstream then reads
|
|
11016
|
+
* "context" as part of the name. Closed list, and the term still has to
|
|
11017
|
+
* resolve afterwards, so a phrase that leaves nothing resolvable simply
|
|
11018
|
+
* declines to the ordinary miss.
|
|
11019
|
+
*
|
|
11020
|
+
* The "everything i need ..." branch is the same shape with a purpose clause
|
|
11021
|
+
* instead of a bare noun: "give me everything i need to change X"/"...for X"
|
|
11022
|
+
* both reach here as "describe everything i need to change X"/"describe
|
|
11023
|
+
* everything i need for X". The "to <verb...>" purpose clause is never split
|
|
11024
|
+
* word-by-word (a multi-word clause like "to work on X" would mis-split into
|
|
11025
|
+
* a captured verb plus a dangling remainder) — the greedy `.*\s` instead
|
|
11026
|
+
* always backtracks to the LAST whitespace run in the match, so the whole
|
|
11027
|
+
* clause strips as one unit and only the trailing symbol/path survives. */
|
|
11028
|
+
const DESCRIBE_LEADING_NOISE_RE =
|
|
11029
|
+
/^(?:(?:the\s+)?(?:context|details|detail|info|information|background)\s+(?:for|on|about|of|around)\s+|everything\s+i(?:'d|\s+would)?\s+need\s+(?:to\s+.*\s|for\s+))/i;
|
|
11030
|
+
function stripDescribeLeadingNoise(term) {
|
|
11031
|
+
const stripped = String(term || "").replace(DESCRIBE_LEADING_NOISE_RE, "").trim();
|
|
11032
|
+
return stripped || String(term || "").trim();
|
|
11033
|
+
}
|
|
11034
|
+
|
|
10930
11035
|
async function describeGrainRescue(graph, term) {
|
|
10931
11036
|
if (!graph) return null;
|
|
10932
|
-
const m =
|
|
11037
|
+
const m = stripDescribeLeadingNoise(term).match(DESCRIBE_GRAIN_WORD_RE);
|
|
10933
11038
|
if (!m) return null;
|
|
10934
11039
|
const [, head, grainWord] = m;
|
|
10935
11040
|
const expectedClass = ENTITY_TO_TYPE[grainWord.toLowerCase()];
|
|
@@ -10959,6 +11064,11 @@ async function describeWrapperAnswer(query, { config, source, focus, graph, tel
|
|
|
10959
11064
|
// "describe about X" (a doubled verb) leaves a redundant leading "about "
|
|
10960
11065
|
// glued to the captured term.
|
|
10961
11066
|
term = term.replace(/^about\s+/i, "");
|
|
11067
|
+
// "give me the context for X" arrives bridged into "describe context for X".
|
|
11068
|
+
// Cleaned once here so the grain rescue, the /describe dispatch and the
|
|
11069
|
+
// focus-setting resolveEntity below all read the same clean term — the
|
|
11070
|
+
// answer was already right without this, but the focus never updated.
|
|
11071
|
+
term = stripDescribeLeadingNoise(term);
|
|
10962
11072
|
// A trailing bare discourse tag ("describe Record then") glued onto the
|
|
10963
11073
|
// captured term, same class of gap stripTrailingDiscourseTag (ask-vocab.mjs)
|
|
10964
11074
|
// already fixes for the meta-whatis vocab lane.
|
|
@@ -13260,6 +13370,15 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
13260
13370
|
const described = await describeWrapperAnswer(query, { config, source, focus: newFocus, graph, tel });
|
|
13261
13371
|
if (described) {
|
|
13262
13372
|
answer = described.text; via = described.miss ? "miss" : "describe"; recordMiss = !!described.miss;
|
|
13373
|
+
// The composed engine's failed parse above (`via` was still "composed"
|
|
13374
|
+
// to reach this lane at all) can have deduced a goal/canonical for a
|
|
13375
|
+
// DIFFERENT reading it never resolved — e.g. "give me everything i need
|
|
13376
|
+
// to change X" bag-of-words-matches "change" as a touches verb and
|
|
13377
|
+
// deduces subject="describe everything i need". This rescue answers a
|
|
13378
|
+
// different question, so that stale interpretation must not survive
|
|
13379
|
+
// into its answer (same staleness the FUZZY-VERB DECLINE lane guards
|
|
13380
|
+
// against, below).
|
|
13381
|
+
canonical = null; deduced = null;
|
|
13263
13382
|
note(trace, "lane: (4d) DESCRIBE-WRAPPER RESCUE — a polite wrapper around \"describe/tell me about <symbol>\" resolved via /describe, tried last after every other lane declined");
|
|
13264
13383
|
note(trace, "goal: get a symbol's definition/kind/relations (phrased conversationally)");
|
|
13265
13384
|
// Carry the resolved entity forward as the new focus, same class-gated
|
|
@@ -14785,7 +14904,60 @@ function vocabAntecedentFrom(last) {
|
|
|
14785
14904
|
return m[1];
|
|
14786
14905
|
}
|
|
14787
14906
|
|
|
14788
|
-
|
|
14907
|
+
/** Commands whose answer is SERVED SOURCE TEXT — lines read byte-for-byte off a
|
|
14908
|
+
* file on disk, which a caller may paste straight into an edit. The prose
|
|
14909
|
+
* grammar rules rewrite real code into code that no longer parses: a spread
|
|
14910
|
+
* (`...base`) and an optional chain (`?.`) both read as runs of terminal
|
|
14911
|
+
* punctuation and collapse to a single character. So the whole answer goes to
|
|
14912
|
+
* finish() as ONE protected `code` span and comes back untouched. */
|
|
14913
|
+
const SOURCE_RENDERING_COMMANDS = new Set(["context", "snippet"]);
|
|
14914
|
+
|
|
14915
|
+
/** finish() a dispatched turn, byte-protecting a served-source answer. A
|
|
14916
|
+
* producer that segmented its own answer keeps its own segments; everything
|
|
14917
|
+
* else is masked and grammar-corrected exactly as before. */
|
|
14918
|
+
function finishTurn(result, ctx) {
|
|
14919
|
+
const preSegmented = Array.isArray(result?.segments) && result.segments.length > 0;
|
|
14920
|
+
if (!preSegmented && typeof result?.answer === "string"
|
|
14921
|
+
&& SOURCE_RENDERING_COMMANDS.has(result?.record?.command)) {
|
|
14922
|
+
return finish({ ...result, segments: [{ type: "code", text: result.answer }] }, ctx);
|
|
14923
|
+
}
|
|
14924
|
+
return finish(result, ctx);
|
|
14925
|
+
}
|
|
14926
|
+
|
|
14927
|
+
/** An uncached readFactRows() snapshot of the memory store — one half of the
|
|
14928
|
+
* before/after pair a turn's `factsTouched` diff is taken over. Deliberately
|
|
14929
|
+
* bypasses the turn's factRowsCache: the cache is invalidated by only some
|
|
14930
|
+
* write paths, and a diff read through it would miss the very writes it exists
|
|
14931
|
+
* to report. Null (rather than []) when there is no store or it won't load, so
|
|
14932
|
+
* the caller can tell "nothing to diff" from "diffed, nothing moved". */
|
|
14933
|
+
async function factRowSnapshot(memoryDir) {
|
|
14934
|
+
if (!memoryDir) return null;
|
|
14935
|
+
try { return readStoredFactRows(await loadMemoryStore(memoryDir)); } catch { return null; }
|
|
14936
|
+
}
|
|
14937
|
+
|
|
14938
|
+
/** The Fact rows this turn wrote, diffed against the snapshot taken before it.
|
|
14939
|
+
* Empty when the turn had no store to write to, or wrote nothing. */
|
|
14940
|
+
async function factsTouchedSince(memoryDir, before) {
|
|
14941
|
+
if (!before) return [];
|
|
14942
|
+
const after = await factRowSnapshot(memoryDir);
|
|
14943
|
+
if (!after) return [];
|
|
14944
|
+
const { touchedFactRows } = await import("../domain/memory/touched-facts.mjs");
|
|
14945
|
+
return touchedFactRows(before, after);
|
|
14946
|
+
}
|
|
14947
|
+
|
|
14948
|
+
/** Run one turn and report which Fact rows it wrote, as `factsTouched` beside
|
|
14949
|
+
* the answer/record/logLines every caller already reads. The dispatch itself
|
|
14950
|
+
* is dispatchTurn, below; this wrapper exists so the field lands on EVERY
|
|
14951
|
+
* return path (dispatched, conversational, multi-sentence) from one place. */
|
|
14952
|
+
export async function runTurn(input, options = {}) {
|
|
14953
|
+
const memoryDir = options?.memoryDir ?? null;
|
|
14954
|
+
const before = await factRowSnapshot(memoryDir);
|
|
14955
|
+
const result = await dispatchTurn(input, options);
|
|
14956
|
+
if (!result || typeof result !== "object") return result;
|
|
14957
|
+
return { ...result, factsTouched: await factsTouchedSince(memoryDir, before) };
|
|
14958
|
+
}
|
|
14959
|
+
|
|
14960
|
+
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
14961
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
14790
14962
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
14791
14963
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -14863,7 +15035,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
14863
15035
|
// what the shell prints. The narrate block is applied AFTER `last` is
|
|
14864
15036
|
// captured from the PRE-narration finished result.
|
|
14865
15037
|
const withLast = (result, fallbackGoal = "unclear — no goal signal for this turn type") => {
|
|
14866
|
-
const finished = attachDialogueAct(
|
|
15038
|
+
const finished = attachDialogueAct(finishTurn(result, { graph }), trace);
|
|
14867
15039
|
// The logged transcript echo is ALWAYS the verbatim user line — no dispatch
|
|
14868
15040
|
// path's internal rewrite (the indirect-request wrapper, the vocab-opener /
|
|
14869
15041
|
// cleft / ESL rewrites, a discourse substitution) may leak into what the
|
|
@@ -15154,8 +15326,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
15154
15326
|
let f = focus; let l = last; let ps = planHolder.state; let d = discourseHolder.record;
|
|
15155
15327
|
const receipts = [];
|
|
15156
15328
|
let finalRec = null;
|
|
15329
|
+
// dispatchTurn, not runTurn: the OUTER wrapper's before/after snapshot
|
|
15330
|
+
// already spans every sentence, so a per-sentence diff would only pay
|
|
15331
|
+
// for a narrower answer to the same question.
|
|
15157
15332
|
for (const sentence of sentences) {
|
|
15158
|
-
const r = await
|
|
15333
|
+
const r = await dispatchTurn(sentence, {
|
|
15159
15334
|
config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
|
|
15160
15335
|
narrate: false, vocabHint, tel, biasByBundle, planState: ps, discourse: d, _noSplit: true,
|
|
15161
15336
|
});
|
package/src/services/finish.mjs
CHANGED
|
@@ -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
|
]);
|