@polycode-projects/the-mechanical-code-talker 1.11.6 → 2.0.1
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 +244 -48
- package/ROADMAP.md +23 -34
- package/bin/tmct.mjs +117 -81
- package/corpus/LICENSES.json +118 -0
- package/corpus/README.md +17 -13
- package/corpus/conceptnet/README.md +5 -5
- package/corpus/conceptnet/fetch-slice.mjs +1 -1
- package/corpus/conceptnet/filter-dump.mjs +1 -1
- package/corpus/generated/README.md +9 -10
- package/corpus/namenet/README.md +39 -0
- package/corpus/seon/README.md +2 -2
- package/corpus/tier2/generate.mjs +58 -9
- package/corpus/tier2/manifest.json +44 -0
- package/corpus/wordnet/README.md +37 -0
- package/corpus/wordnet/generate.mjs +1 -1
- package/data/templates/constructions/agent-noun-relations.toml +2 -2
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +13 -22
- package/src/{ask-nlp.mjs → adapters/ask-nlp.mjs} +1 -1
- package/src/{config.mjs → adapters/config.mjs} +1 -1
- package/src/{corpus → adapters/corpus}/conceptnet-map.toml +4 -4
- package/src/{corpus → adapters/corpus}/conceptnet.mjs +3 -3
- package/src/adapters/corpus/construction-banks.mjs +43 -0
- package/src/{corpus → adapters/corpus}/templates.mjs +1 -1
- package/src/{embed.mjs → adapters/embed.mjs} +1 -11
- package/src/{graph-build.mjs → adapters/graph-build.mjs} +6 -6
- package/src/{memory → adapters/memory}/blocks.mjs +2 -2
- package/src/{memory → adapters/memory}/core.mjs +38 -94
- package/src/adapters/prose-tokens.mjs +98 -0
- package/src/{providers → adapters/providers}/bootstrap.mjs +2 -2
- package/src/{providers → adapters/providers}/fixture.mjs +3 -3
- package/src/{providers → adapters/providers}/graph-service.mjs +9 -4
- package/src/{source-slice.mjs → adapters/source-slice.mjs} +2 -2
- package/src/{source.mjs → adapters/source.mjs} +1 -1
- package/src/{toml-config.mjs → adapters/toml-config.mjs} +1 -1
- package/src/{answer-variants.json → domain/answer-variants.json} +1 -1
- package/src/domain/answer-variants.mjs +23 -0
- package/src/{ask-vocab.mjs → domain/ask-vocab.mjs} +37 -5
- package/src/{ask.mjs → domain/ask.mjs} +216 -52
- package/src/{codegraph.mjs → domain/codegraph.mjs} +31 -315
- package/src/{completions → domain/completions}/complete.mjs +16 -10
- package/src/{completions → domain/completions}/graph-adapter.mjs +10 -4
- package/src/{completions → domain/completions}/group.mjs +14 -6
- package/src/{completions → domain/completions}/infer.mjs +57 -39
- package/src/domain/completions/injected.mjs +21 -0
- package/src/{completions → domain/completions}/rank.mjs +15 -8
- package/src/{completions → domain/completions}/search.mjs +4 -2
- package/src/{grammar → domain/grammar}/ace.mjs +3 -3
- package/src/{grammar → domain/grammar}/assert.mjs +12 -8
- package/src/{grammar → domain/grammar}/lexicon-core.json +1 -1
- package/src/{grammar → domain/grammar}/lexicon.mjs +4 -6
- package/src/domain/hash.mjs +147 -0
- package/src/{interpret → domain/interpret}/fuzzy.mjs +42 -4
- package/src/domain/interpret/nlp-registry.mjs +20 -0
- package/src/{interpret → domain/interpret}/normalize.mjs +35 -5
- package/src/{interpret → domain/interpret}/pipeline.mjs +1 -5
- package/src/{interpret → domain/interpret}/strategies/ace.mjs +1 -1
- package/src/{interpret → domain/interpret}/strategies/constructions.mjs +30 -52
- package/src/{interpret → domain/interpret}/strategies/keywords.mjs +48 -26
- package/src/domain/memory/capability.mjs +235 -0
- package/src/domain/memory/fold.mjs +54 -0
- package/src/domain/memory/session-turns.mjs +7 -0
- package/src/{memory → domain/memory}/trust.mjs +48 -0
- package/src/{paraphrase.mjs → domain/paraphrase.mjs} +2 -2
- package/src/{prose.mjs → domain/prose.mjs} +1 -1
- package/src/domain/real-word-collisions.json +1 -0
- package/src/{router → domain/router}/call-validator.mjs +1 -1
- package/src/{router → domain/router}/drive.mjs +34 -25
- package/src/{router → domain/router}/goal-reasoner.mjs +1 -1
- package/src/{router → domain/router}/guardrail.mjs +1 -1
- package/src/{router → domain/router}/planner.mjs +1 -1
- package/src/{router → domain/router}/registry.mjs +5 -5
- package/src/{router → domain/router}/resolver.mjs +16 -13
- package/src/{router → domain/router}/results.mjs +1 -1
- package/src/{router → domain/router}/set-algebra.mjs +1 -1
- package/src/{router → domain/router}/taught.mjs +10 -9
- package/src/{syllogise.mjs → domain/syllogise.mjs} +21 -4
- package/src/domain/vector.mjs +12 -0
- package/src/services/chat-session.mjs +451 -0
- package/src/{chat.mjs → services/chat.mjs} +1238 -680
- package/src/{cli-args.mjs → services/cli-args.mjs} +2 -2
- package/src/services/completions.mjs +55 -0
- package/src/{extensions.mjs → services/extensions.mjs} +7 -7
- package/src/{finish.mjs → services/finish.mjs} +2 -2
- package/src/{memory → services}/fold.mjs +0 -0
- package/src/{import-file.mjs → services/import-file.mjs} +3 -3
- package/src/{index.mjs → services/index.mjs} +21 -12
- package/src/{init.mjs → services/init.mjs} +9 -9
- package/src/{ledger-viz.mjs → services/ledger-viz.mjs} +38 -7
- package/src/{plan-viz.mjs → services/plan-viz.mjs} +65 -26
- package/src/{sentences.mjs → services/sentences.mjs} +1 -1
- package/src/{sessions.mjs → services/sessions.mjs} +4 -5
- package/src/{telemetry.mjs → services/telemetry.mjs} +1 -1
- package/src/{server-http.mjs → surfaces/http/server-http.mjs} +11 -65
- package/src/{tui → surfaces/tui}/app.mjs +3 -3
- package/src/{memory-ask-browser-entry.mjs → surfaces/web/memory-ask-browser-entry.mjs} +5 -5
- package/src/{memory-ask-browser.bundle.js → surfaces/web/memory-ask-browser.bundle.js} +9465 -6366
- package/src/tools/catalog.mjs +29 -0
- package/src/{conformance.mjs → tools/conformance.mjs} +2 -2
- package/src/tools/definitions.mjs +288 -0
- package/src/tools/graph-load.mjs +20 -0
- package/src/tools/handlers/index.mjs +54 -0
- package/src/tools/handlers/kit.mjs +33 -0
- package/src/tools/handlers/tmct-architecture.mjs +7 -0
- package/src/tools/handlers/tmct-ask.mjs +14 -0
- package/src/tools/handlers/tmct-callees.mjs +6 -0
- package/src/tools/handlers/tmct-callers.mjs +6 -0
- package/src/tools/handlers/tmct-calls.mjs +6 -0
- package/src/tools/handlers/tmct-class-history.mjs +6 -0
- package/src/tools/handlers/tmct-cochanges.mjs +6 -0
- package/src/tools/handlers/tmct-context-more.mjs +9 -0
- package/src/tools/handlers/tmct-context.mjs +163 -0
- package/src/tools/handlers/tmct-describe.mjs +15 -0
- package/src/tools/handlers/tmct-exports.mjs +9 -0
- package/src/tools/handlers/tmct-file-history.mjs +6 -0
- package/src/tools/handlers/tmct-history.mjs +6 -0
- package/src/tools/handlers/tmct-impact.mjs +9 -0
- package/src/tools/handlers/tmct-members.mjs +16 -0
- package/src/tools/handlers/tmct-method-history.mjs +6 -0
- package/src/tools/handlers/tmct-search.mjs +22 -0
- package/src/tools/handlers/tmct-signature.mjs +6 -0
- package/src/tools/handlers/tmct-snippet.mjs +37 -0
- package/src/tools/handlers/tmct-subclasses.mjs +16 -0
- package/src/tools/handlers/tmct-tests-for.mjs +6 -0
- package/src/tools/handlers/tmct-untested.mjs +7 -0
- package/src/tools/memory-fallthrough.mjs +65 -0
- package/src/{schema-docs.mjs → tools/schema-docs.mjs} +1 -1
- package/src/tools/server.mjs +61 -0
- package/src/answer-variants.mjs +0 -39
- package/src/hash.mjs +0 -24
- package/src/server.mjs +0 -501
- /package/src/{corpus → adapters/corpus}/unknown-ingest.mjs +0 -0
- /package/src/{graph-merge.mjs → adapters/graph-merge.mjs} +0 -0
- /package/src/{memory → adapters/memory}/inspect.mjs +0 -0
- /package/src/{memory → adapters/memory}/shacl.mjs +0 -0
- /package/src/{prose-nlp.mjs → adapters/prose-nlp.mjs} +0 -0
- /package/src/{repository-interface.mjs → adapters/repository-interface.mjs} +0 -0
- /package/src/{uuid.mjs → adapters/uuid.mjs} +0 -0
- /package/src/{wink-model.mjs → adapters/wink-model.mjs} +0 -0
- /package/src/{completions → domain/completions}/prune.mjs +0 -0
- /package/src/{concept.mjs → domain/concept.mjs} +0 -0
- /package/src/{domain.mjs → domain/domain.mjs} +0 -0
- /package/src/{interpret → domain/interpret}/merge.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/grammar.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/noise-strip.mjs +0 -0
- /package/src/{memory → domain/memory}/bias.mjs +0 -0
- /package/src/{planning.mjs → domain/planning.mjs} +0 -0
- /package/src/{viz-theme.mjs → services/viz-theme.mjs} +0 -0
|
@@ -15,48 +15,55 @@
|
|
|
15
15
|
// ENGINE is imported lazily and failure-tolerated, so a turn never crashes
|
|
16
16
|
// (the one static ask.mjs import, classDisplayName, is a pure formatter).
|
|
17
17
|
// createSession(…) is the SESSION SINK every shell shares (runChat's readline
|
|
18
|
-
// loop, src/tui/app.mjs's Ink shell).
|
|
19
|
-
|
|
20
|
-
import { join, dirname
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
import * as defaultSource from "./source.mjs";
|
|
35
|
-
import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs";
|
|
36
|
-
import { resolveExtensions, mergedLexiconExtra } from "./extensions.mjs";
|
|
37
|
-
import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
38
|
-
import { HAS_A_PREDICATE } from "./memory/core.mjs";
|
|
18
|
+
// loop, src/surfaces/tui/app.mjs's Ink shell).
|
|
19
|
+
|
|
20
|
+
import { join, dirname } from "node:path";
|
|
21
|
+
import { dispatchTool, loadGraph, TOOLS } from "../tools/server.mjs";
|
|
22
|
+
import { ToolError } from "../adapters/config.mjs";
|
|
23
|
+
import { parseEntities, edgesOfKind, moduleCountOf, renderAuthorCard, renderAuthorTouches, renderCommitAuthor, resolveSymbol, renderCompare } from "../domain/codegraph.mjs";
|
|
24
|
+
import { classDisplayName } from "../domain/ask.mjs";
|
|
25
|
+
import { uuidv7 } from "../adapters/uuid.mjs";
|
|
26
|
+
import * as defaultSource from "../adapters/source.mjs";
|
|
27
|
+
import { loadTemplates, render as renderTemplate } from "../adapters/corpus/templates.mjs";
|
|
28
|
+
import { rankByBiasThenTrust } from "../domain/memory/bias.mjs";
|
|
29
|
+
import { HAS_A_PREDICATE, loadMemory as loadMemoryStore, normFactPredicate, normFactTerm as normFactTermStatic, readFactRows as readStoredFactRows, readRuleRows as readStoredRuleRows } from "../adapters/memory/core.mjs";
|
|
30
|
+
import {
|
|
31
|
+
CAPABILITY_REPORT_CAP, NEG_CAPABLE_OF_PREDICATE, capabilityBaseRate, capabilityExtension,
|
|
32
|
+
isNegatedPredicate, negatedPredicate, positivePredicate, resolveCapabilityPolarity,
|
|
33
|
+
} from "../domain/memory/capability.mjs";
|
|
39
34
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
40
35
|
import { splitSentences } from "./sentences.mjs";
|
|
41
36
|
import {
|
|
42
37
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
43
|
-
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS,
|
|
44
|
-
} from "
|
|
45
|
-
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint } from "
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
38
|
+
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS, LIST_TRIGGERS,
|
|
39
|
+
} from "../domain/ask-vocab.mjs";
|
|
40
|
+
import { COUNTERFACTUAL_RE, correctMisspellings, applyPreambleFrames, expandContractions, normalizeQuery, stripFillerWords, escapeRegex, kindNounAnaphoraHint } from "../domain/interpret/normalize.mjs";
|
|
41
|
+
import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
|
|
42
|
+
import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
|
|
43
|
+
import { nlpAdapter } from "../adapters/ask-nlp.mjs";
|
|
44
|
+
import { readConstructionFiles } from "../adapters/corpus/construction-banks.mjs";
|
|
45
|
+
import { fuzzyMatchInSet, fuzzyBound } from "../domain/interpret/fuzzy.mjs";
|
|
46
|
+
import { pickPhrase } from "../domain/answer-variants.mjs";
|
|
47
|
+
|
|
48
|
+
// Composition: the chat surface supplies the domain parser's default lemma/POS
|
|
49
|
+
// adapter (the browser bundle's ask-nlp stub carries no factory, so this is a
|
|
50
|
+
// no-op there and the parser stays adapter-less) and the construction-grammar
|
|
51
|
+
// banks (lazy — the TOML read happens on the first parse that needs them; the
|
|
52
|
+
// bundle's constructions stub ignores the registration entirely).
|
|
53
|
+
setDefaultNlpAdapter(nlpAdapter);
|
|
54
|
+
setConstructionBanks(readConstructionFiles);
|
|
48
55
|
|
|
49
56
|
// uuidv7 lives in ./uuid.mjs (shared with telemetry + the bench stamp); re-exported
|
|
50
57
|
// here because callers/tests still import it from chat.mjs.
|
|
51
58
|
export { uuidv7 };
|
|
52
59
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export
|
|
60
|
+
// The session-orchestration cluster (createSession/runChat, the readline shell,
|
|
61
|
+
// the log/sidecar writers, the graph upsert, the first-run seed bootstrap) lives
|
|
62
|
+
// in the session layer so runTurn and the fact engine below stay free of
|
|
63
|
+
// node:fs/child_process/os/readline. Re-exported here (services → services) so
|
|
64
|
+
// every existing import site — bin, tui, server-http, index, tests — keeps
|
|
65
|
+
// importing createSession/runChat/gitToplevel/SESSION_LOG_DIR/PROMPT from chat.mjs.
|
|
66
|
+
export { createSession, runChat, gitToplevel, SESSION_LOG_DIR, PROMPT } from "./chat-session.mjs";
|
|
60
67
|
|
|
61
68
|
/** dispatchTool("tmct_ask", …) returns the prose answer plus a delimited
|
|
62
69
|
* machine-readable envelope; the TUI shows the prose only. Reused verbatim
|
|
@@ -344,7 +351,16 @@ export function asBareCommand(line) {
|
|
|
344
351
|
// not part of the search term, and counting it could wrongly reject an
|
|
345
352
|
// otherwise-short query as "too long" ("search for the payment controller"
|
|
346
353
|
// is 4 tokens WITH "for", 3 without).
|
|
347
|
-
|
|
354
|
+
//
|
|
355
|
+
// "describe about X" takes the same strip for the same reason: "about" is
|
|
356
|
+
// filler between the command and its symbol, and left in it binds verbatim
|
|
357
|
+
// ("no entity matching symbol \"about a dog\"") because this path never
|
|
358
|
+
// reaches describeWrapperAnswer's own "^about " strip. A bare "describe
|
|
359
|
+
// about" keeps its argument — with nothing after it, "about" is all the user
|
|
360
|
+
// gave and dropping it would answer a command they didn't type.
|
|
361
|
+
const strippedFor = (fl === "search" || fl === "find") && restTokRaw[0]?.toLowerCase() === "for";
|
|
362
|
+
const strippedAbout = fl === "describe" && restTokRaw[0]?.toLowerCase() === "about" && restTokRaw.length > 1;
|
|
363
|
+
const stripped = strippedFor || strippedAbout;
|
|
348
364
|
const restTok = stripped ? restTokRaw.slice(1) : restTokRaw;
|
|
349
365
|
const rest = restTok.join(" ");
|
|
350
366
|
const effectiveLine = stripped ? `${fl}${rest ? ` ${rest}` : ""}` : trimmed;
|
|
@@ -597,7 +613,7 @@ async function answerEdgeCount(graph, query) {
|
|
|
597
613
|
const ind = graph.byId?.get?.(entity.id);
|
|
598
614
|
if (!ind) return null;
|
|
599
615
|
let degreeMetric;
|
|
600
|
-
try { ({ degreeMetric } = await import("
|
|
616
|
+
try { ({ degreeMetric } = await import("../domain/ask.mjs")); } catch { return null; }
|
|
601
617
|
const n = degreeMetric(graph, ind, metric);
|
|
602
618
|
return `${n} ${edgeCountNoun(noun, n)}.`;
|
|
603
619
|
}
|
|
@@ -615,7 +631,7 @@ async function countFromFacts(graph, memoryDir, query, biasByBundle = {}, cache
|
|
|
615
631
|
const asked = m[1].toLowerCase();
|
|
616
632
|
if (COUNT_NOUNS[asked]) return null; // a real graph kind — answerCount owns it
|
|
617
633
|
let normFactTerm;
|
|
618
|
-
try { ({ normFactTerm } = await import("
|
|
634
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
619
635
|
const objVariants = factTermVariants(normFactTerm, asked);
|
|
620
636
|
const isa = (await factRows(memoryDir, cache))
|
|
621
637
|
.filter((f) => ISA_PREDICATES.has(f.predicate) && objVariants.has(f.object));
|
|
@@ -643,7 +659,7 @@ async function answerQuantifierRecall(memoryDir, query, biasByBundle = {}, cache
|
|
|
643
659
|
const asked = m[1].toLowerCase();
|
|
644
660
|
if (COUNT_NOUNS[asked]) return null; // a real graph-countable class — answerCount owns it
|
|
645
661
|
let normFactTerm;
|
|
646
|
-
try { ({ normFactTerm } = await import("
|
|
662
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
647
663
|
const subjVariants = factTermVariants(normFactTerm, asked);
|
|
648
664
|
const rows = (await factRows(memoryDir, cache)).filter((f) => ISA_PREDICATES.has(f.predicate) && subjVariants.has(f.subject));
|
|
649
665
|
if (!rows.length) return null; // never heard of this subject at all — let answerCount own the shape
|
|
@@ -685,7 +701,7 @@ async function answerMemoryCount(memoryDir, query) {
|
|
|
685
701
|
}
|
|
686
702
|
if (!cls) return null;
|
|
687
703
|
let loadMemory;
|
|
688
|
-
try { ({ loadMemory } = await import("
|
|
704
|
+
try { ({ loadMemory } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
689
705
|
let mem;
|
|
690
706
|
try { mem = await loadMemory(memoryDir); } catch { return null; }
|
|
691
707
|
const n = (mem.individuals || []).filter((i) => (i.class || "") === cls).length;
|
|
@@ -885,7 +901,92 @@ export function isConversational(query) {
|
|
|
885
901
|
if (IDENTITY_PHRASES.some((re) => re.test(raw))) return true;
|
|
886
902
|
if (aiIdentityMatch(raw)) return true;
|
|
887
903
|
const codeish = looksCodeish(raw, q);
|
|
888
|
-
|
|
904
|
+
// The catch-all counts words, so a contraction decides the turn on
|
|
905
|
+
// punctuation alone: "what's on peg-a" counts 3 and gets the orientation
|
|
906
|
+
// card, "what is on peg-a" counts 4 and gets the answer. Write the
|
|
907
|
+
// contraction out for the count only.
|
|
908
|
+
//
|
|
909
|
+
// The count is the whole reason this is expandContractions and not
|
|
910
|
+
// normalizeQuery: the fuller pass strips filler, which takes the count DOWN,
|
|
911
|
+
// and sends "please describe a dog" and "tell me about a dog" to the card
|
|
912
|
+
// instead. `q` itself is left alone so the GREET/THANKS/OK_ACK membership
|
|
913
|
+
// above reads the text as typed, and looksCodeish reads `raw`.
|
|
914
|
+
return expandContractions(q).split(/\s+/).filter(Boolean).length <= 3 && !codeish;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/** The tmct tools dispatchTool can back (the set a tool-emitting caller may use).
|
|
918
|
+
* A declared tool outside this set is never emitted — the request falls through
|
|
919
|
+
* to a text answer. The COMMANDS map names the richer graph tools; TOOLS names
|
|
920
|
+
* the hot catalog. Their union is what dispatchTool serves. */
|
|
921
|
+
const BACKED_TOOLS = new Set([
|
|
922
|
+
...TOOLS.map((t) => t.name),
|
|
923
|
+
...Object.values(COMMANDS).map((s) => s.tool),
|
|
924
|
+
]);
|
|
925
|
+
|
|
926
|
+
/**
|
|
927
|
+
* Decide whether a user turn maps to a DECLARED, dispatch-backed graph-query
|
|
928
|
+
* tool, and bind its arguments. Deterministic, in-ethos (no NL guessing beyond
|
|
929
|
+
* the chat surface's own command routing):
|
|
930
|
+
*
|
|
931
|
+
* 1. A slash/bare command that names a tmct tool ("describe X", "/callers X",
|
|
932
|
+
* "untested") → that tool with its argument bound from the exact arg key the
|
|
933
|
+
* dispatchTool switch reads (COMMANDS above). Only when the tool is
|
|
934
|
+
* declared by the caller.
|
|
935
|
+
* 2. Otherwise, a non-conversational structural question → tmct_ask{query:…},
|
|
936
|
+
* when tmct_ask is declared. Small-talk (isConversational) never emits a
|
|
937
|
+
* call — it falls through to a text answer.
|
|
938
|
+
*
|
|
939
|
+
* Returns { name, input } or null (→ answer as text).
|
|
940
|
+
*/
|
|
941
|
+
export function selectTool(text, declaredNames) {
|
|
942
|
+
const t = String(text || "").trim();
|
|
943
|
+
if (!t) return null;
|
|
944
|
+
|
|
945
|
+
// 1. explicit command form → a specific tool, argument bound
|
|
946
|
+
const cmdLine = t.startsWith("/") ? t : asBareCommand(t);
|
|
947
|
+
if (cmdLine) {
|
|
948
|
+
const [first, ...restTok] = cmdLine.replace(/^\//, "").split(/\s+/);
|
|
949
|
+
const spec = COMMANDS[String(first).toLowerCase()];
|
|
950
|
+
if (spec && declaredNames.has(spec.tool) && BACKED_TOOLS.has(spec.tool)) {
|
|
951
|
+
const input = {};
|
|
952
|
+
if (spec.arg) {
|
|
953
|
+
const val = restTok.join(" ").trim();
|
|
954
|
+
if (val) input[spec.arg] = val;
|
|
955
|
+
// an entity command with no argument can't bind a call — fall through
|
|
956
|
+
else if (!spec.optional) return askFallback(t, declaredNames);
|
|
957
|
+
}
|
|
958
|
+
return { name: spec.tool, input };
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// 2. structural question → tmct_ask, unless it's small-talk
|
|
963
|
+
return askFallback(t, declaredNames);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/** The tmct_ask fallback: emit tmct_ask{query} for a non-conversational line when
|
|
967
|
+
* the caller declared tmct_ask; otherwise null (→ text answer). */
|
|
968
|
+
function askFallback(text, declaredNames) {
|
|
969
|
+
if (declaredNames.has("tmct_ask") && BACKED_TOOLS.has("tmct_ask") && !isConversational(text)) {
|
|
970
|
+
return { name: "tmct_ask", input: { query: text } };
|
|
971
|
+
}
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/** The live tool-layer dependencies buildCapabilityPlanCtx (router/drive.mjs)
|
|
976
|
+
* needs injected: the real dispatchTool, the ToolError classifier, the command
|
|
977
|
+
* register, and the memory-store readers the taught world-goal lane reloads
|
|
978
|
+
* per request. The router itself stays pure; every caller that wants the real
|
|
979
|
+
* tool layer spreads these into its ctx build. */
|
|
980
|
+
export function capabilityPlanDeps() {
|
|
981
|
+
return {
|
|
982
|
+
source: defaultSource,
|
|
983
|
+
dispatchTool,
|
|
984
|
+
isToolError: (e) => e instanceof ToolError,
|
|
985
|
+
selectTool,
|
|
986
|
+
loadMemory: loadMemoryStore,
|
|
987
|
+
readFactRows: readStoredFactRows,
|
|
988
|
+
readRuleRows: readStoredRuleRows,
|
|
989
|
+
};
|
|
889
990
|
}
|
|
890
991
|
|
|
891
992
|
/** Scoped exemption for the bare-meta-fact lane (2b/2c, further down this file)
|
|
@@ -907,6 +1008,25 @@ function isBareCamelCaseMetaQuestion(query) {
|
|
|
907
1008
|
return BARE_WHATIS_RE.test(raw) || IS_ADJECTIVE_YESNO_RE.test(raw);
|
|
908
1009
|
}
|
|
909
1010
|
|
|
1011
|
+
/** The same scoped exemption for the WRAPPERLESS form, lane (2c) only: a bare
|
|
1012
|
+
* "TaskController" typed on its own. isBareCamelCaseMetaQuestion above needs a
|
|
1013
|
+
* "what is X" / "is X <adjective>" wrapper, so a bare CamelCase name still
|
|
1014
|
+
* stops at looksCodeish()'s `/[a-z][A-Z]/` branch while its lowercase twin
|
|
1015
|
+
* ("task") reaches the lane and answers.
|
|
1016
|
+
*
|
|
1017
|
+
* A single unbroken word is the whole shape (2c looks the raw line up as a
|
|
1018
|
+
* label), and that shape is what keeps the exemption at the CamelCase reason
|
|
1019
|
+
* and nothing else: a path, a dotted ref, a `()` call or any multi-word
|
|
1020
|
+
* near-miss structural question ("what is import") can't be one bare word, and
|
|
1021
|
+
* every STRUCT_WORDS member is lowercase, so the CamelCase requirement leaves
|
|
1022
|
+
* them all where they are. Lane (2c) still only diverts on a real, unique
|
|
1023
|
+
* graph hit — an unknown CamelCase word answers exactly as it does now. */
|
|
1024
|
+
function isBareCamelCaseEntityName(query) {
|
|
1025
|
+
const raw = String(query).trim();
|
|
1026
|
+
if (!/^[A-Za-z][A-Za-z0-9]*$/.test(raw)) return false;
|
|
1027
|
+
return /[a-z][A-Z]/.test(raw);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
910
1030
|
// ---- the response-template library (W1: templates → render path) ----
|
|
911
1031
|
// The WORDING of the conversational/orientation surfaces lives in
|
|
912
1032
|
// data/templates/responses.jsonl (corpus/templates.mjs) — the template library is
|
|
@@ -1358,13 +1478,7 @@ function conversationalTurn(line, ctx) {
|
|
|
1358
1478
|
// gated and (for the lanes) only consulted on a would-miss, so ordinary graph
|
|
1359
1479
|
// queries are never hijacked. ----
|
|
1360
1480
|
|
|
1361
|
-
|
|
1362
|
-
* test. 0 means a graph-less bootstrap OR a graph.json with no code entities (the
|
|
1363
|
-
* degenerate trap); both orient rather than over-promise. */
|
|
1364
|
-
export function moduleCountOf(graph) {
|
|
1365
|
-
if (!graph || !Array.isArray(graph.individuals)) return 0;
|
|
1366
|
-
return graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
1367
|
-
}
|
|
1481
|
+
export { moduleCountOf };
|
|
1368
1482
|
|
|
1369
1483
|
/** A KNOWN-empty code graph: a loaded graph object with 0 modules. A null graph
|
|
1370
1484
|
* (a bare runTurn that wasn't handed one) is "unknown", NOT empty — the empty
|
|
@@ -1589,7 +1703,7 @@ async function hasMidSentenceInterrogative(text) {
|
|
|
1589
1703
|
}
|
|
1590
1704
|
if (!whIdx.length) return false;
|
|
1591
1705
|
try {
|
|
1592
|
-
const { nlpAdapter } = await import("
|
|
1706
|
+
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
1593
1707
|
const adapter = nlpAdapter();
|
|
1594
1708
|
if (!adapter) return false; // no wink — no signal, never a false positive
|
|
1595
1709
|
const tags = adapter.posTags(words);
|
|
@@ -1799,7 +1913,7 @@ const FILTER_RULE_TEACH_RE =
|
|
|
1799
1913
|
* independently captured and need not be identical to each other — only
|
|
1800
1914
|
* `m[1]`'s OWN name must recur at the end). Query side is a genuine
|
|
1801
1915
|
* KIND-CHANGE (reachability-SET enumeration via `findReachableSet`,
|
|
1802
|
-
* src/planning.mjs) from the single-target search above — see the
|
|
1916
|
+
* src/domain/planning.mjs) from the single-target search above — see the
|
|
1803
1917
|
* RECURSIVE_LIST_ASK_RE query recognizer, below. */
|
|
1804
1918
|
const RECURSIVE_RULE_TEACH_RE =
|
|
1805
1919
|
/^an?\s+([a-z][\w-]*)\s+(?:is|are)\s+an?\s+([a-z][\w-]*),?\s+or\s+an?\s+([a-z][\w-]*)\s+of\s+an?\s+\1[.!?]*$/i;
|
|
@@ -1807,7 +1921,7 @@ const RECURSIVE_RULE_TEACH_RE =
|
|
|
1807
1921
|
/** ACTION-RULE TEACH FRAMES — a world-mutating action taught one sentence at
|
|
1808
1922
|
* a time, each sentence its own Rule individual (kind action-signature /
|
|
1809
1923
|
* action-precond / action-effect / action-constraint) sharing one rule name ("<verb> <prep>",
|
|
1810
|
-
* e.g. "move onto"). src/domain.mjs collects the family by name
|
|
1924
|
+
* e.g. "move onto"). src/domain/domain.mjs collects the family by name
|
|
1811
1925
|
* (findRulesByName) and grounds it over class members at plan time; nothing
|
|
1812
1926
|
* in the teach lane executes an action. Predicate slot values are stored
|
|
1813
1927
|
* BARE ("rest-on") because normFactTerm strips a mgx: prefix from slot
|
|
@@ -1830,7 +1944,7 @@ const ACTION_EFFECT_TEACH_RE = new RegExp(
|
|
|
1830
1944
|
* without the farmer" — the co-location CONSTRAINT sentence (kind
|
|
1831
1945
|
* action-constraint): after a move, <left> and <right> may not share a
|
|
1832
1946
|
* position unless <guard> is there too. All three trailing words name a
|
|
1833
|
-
* class whose sole member src/domain.mjs binds at plan time. Disjoint from
|
|
1947
|
+
* class whose sole member src/domain/domain.mjs binds at plan time. Disjoint from
|
|
1834
1948
|
* the two precondition frames above by anchor phrase alone ("may not be
|
|
1835
1949
|
* with … without", never "nothing may" or "must be … than") — PREP_SRC has
|
|
1836
1950
|
* no "without", so the preposition captures can't collide either. */
|
|
@@ -1850,12 +1964,37 @@ const BARE_KINDOF_TEACH_RE = /^an?\s+([a-z][\w-]+)\s+is\s+a\s+kind\s+of\s+(?:an?
|
|
|
1850
1964
|
async function verbLemma(word) {
|
|
1851
1965
|
const w = String(word || "").toLowerCase();
|
|
1852
1966
|
try {
|
|
1853
|
-
const { proseLemma } = await import("
|
|
1967
|
+
const { proseLemma } = await import("../adapters/prose-nlp.mjs");
|
|
1854
1968
|
const lemma = proseLemma();
|
|
1855
1969
|
return lemma ? lemma(w) : w;
|
|
1856
1970
|
} catch { return w; }
|
|
1857
1971
|
}
|
|
1858
1972
|
|
|
1973
|
+
/** Did the keyword strategy's edit-distance tier repair an INFLECTION of the
|
|
1974
|
+
* verb the user typed, or swap the verb for a different one?
|
|
1975
|
+
*
|
|
1976
|
+
* Both come out of the same one-edit rewrite, but they are not the same
|
|
1977
|
+
* event. "used" -> "uses" and "imported" -> "imports" are the vocabulary's own
|
|
1978
|
+
* verb wearing a form the phrase list doesn't happen to spell out, so the
|
|
1979
|
+
* repaired sentence still asks what was typed. "rest" -> "test" and "during"
|
|
1980
|
+
* -> "using" are different verbs, so the repaired sentence asks something
|
|
1981
|
+
* else. A shared lemma separates the two: it holds for every inflection of one
|
|
1982
|
+
* verb and for no pair of distinct ones.
|
|
1983
|
+
*
|
|
1984
|
+
* Wink's lemmatiser is the same optional adapter generalVerbPredicate mints
|
|
1985
|
+
* through. Without it there is no signal, so this reports false and the caller
|
|
1986
|
+
* declines — the conservative direction, matching every other optional-adapter
|
|
1987
|
+
* path here. */
|
|
1988
|
+
async function repairSharesLemma(from, to) {
|
|
1989
|
+
const [a, b] = [String(from || "").toLowerCase(), String(to || "").toLowerCase()];
|
|
1990
|
+
if (a === b) return true;
|
|
1991
|
+
try {
|
|
1992
|
+
const { proseLemma } = await import("../adapters/prose-nlp.mjs");
|
|
1993
|
+
const lemma = proseLemma();
|
|
1994
|
+
return lemma ? lemma(a) === lemma(b) : false;
|
|
1995
|
+
} catch { return false; }
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1859
1998
|
/** Pre-ask declarative taxonomy teaches. Checked BEFORE the ask engine: "a
|
|
1860
1999
|
* disk is a kind of game piece." otherwise parses as an inherits QUESTION
|
|
1861
2000
|
* and dies on term resolution, even though an article-led declarative with
|
|
@@ -1901,6 +2040,17 @@ const GOAL_TEACH_RE = new RegExp(
|
|
|
1901
2040
|
// so the normalization is disclosed.
|
|
1902
2041
|
const GOAL_TEACH_INFINITIVE_RE = new RegExp(
|
|
1903
2042
|
`^(?:the\\s+goal\\s+is\\s+for|i\\s+want)\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+to\\s+([a-z]+)\\s+(${PREP_SRC})\\s+([\\w-]+)[.!?]*$`, "i");
|
|
2043
|
+
// The verbless voicing of the same goal ("i want every disk on peg-b"): the
|
|
2044
|
+
// verb the other two voicings spell out is simply absent. Captures 1, 2, 4 and
|
|
2045
|
+
// 5 of the frames above, minus the verb — planLaneAnswer reads that off the
|
|
2046
|
+
// taught locative facts, and declines when they don't name exactly one.
|
|
2047
|
+
const GOAL_TEACH_VERBLESS_RE = new RegExp(
|
|
2048
|
+
`^(?:the\\s+goal\\s+is\\s+for|i\\s+want)\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+(${PREP_SRC})\\s+([\\w-]+)[.!?]*$`, "i");
|
|
2049
|
+
// The question mirror of the two action-signature teach frames ("can you move a
|
|
2050
|
+
// disk onto a peg?"). Both taught voicings mint ONE rule name (verb lemma +
|
|
2051
|
+
// preposition), so this one reader answers either.
|
|
2052
|
+
const ACTION_SIGNATURE_ASK_RE = new RegExp(
|
|
2053
|
+
`^(?:can|could)\\s+you\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[?.!]*$`, "i");
|
|
1904
2054
|
const PLAN_SOLVE_RE = /^(?:solve\s+it|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
1905
2055
|
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
1906
2056
|
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
@@ -1930,7 +2080,7 @@ const teachProvenanceTag = (sessionId, ts) => `teach:chat${sessionId ? `:${sessi
|
|
|
1930
2080
|
* teach-miss text stands), never a crash. */
|
|
1931
2081
|
async function teachFact(memoryDir, sessionId, { subject, predicate, object, quantifier = "" }) {
|
|
1932
2082
|
try {
|
|
1933
|
-
const { appendFact, normFactTerm } = await import("
|
|
2083
|
+
const { appendFact, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
1934
2084
|
const s = normFactTerm(subject);
|
|
1935
2085
|
const o = normFactTerm(object);
|
|
1936
2086
|
if (!s || !o) return null;
|
|
@@ -2030,7 +2180,7 @@ async function isGroundedByFact(term, memoryDir, cache = null) {
|
|
|
2030
2180
|
if (!memoryDir) return false;
|
|
2031
2181
|
const raw = String(term ?? "").trim();
|
|
2032
2182
|
if (!raw) return false;
|
|
2033
|
-
const { normFactTerm } = await import("
|
|
2183
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
2034
2184
|
const t = normFactTerm(raw);
|
|
2035
2185
|
if (!t) return false;
|
|
2036
2186
|
// TAUGHT-only (same discipline factReadBack's own cax-sco/scm-sco proof
|
|
@@ -2059,7 +2209,7 @@ async function isGroundedTerm(term, lex, memoryDir, cache = null) {
|
|
|
2059
2209
|
const raw = String(term ?? "").trim();
|
|
2060
2210
|
if (!raw) return false;
|
|
2061
2211
|
if (GENERIC_ANCHOR_NOUNS.has(raw.toLowerCase())) return true;
|
|
2062
|
-
const { classify } = await import("
|
|
2212
|
+
const { classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2063
2213
|
if (classify(raw, lex)) return true;
|
|
2064
2214
|
return isGroundedByFact(raw, memoryDir, cache);
|
|
2065
2215
|
}
|
|
@@ -2089,7 +2239,7 @@ async function ungroundedPairHint(payload, lexicon, memoryDir, cache = null) {
|
|
|
2089
2239
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2090
2240
|
if (!m) return "";
|
|
2091
2241
|
const [, , subjectRaw, , objectRaw] = m;
|
|
2092
|
-
const { loadLexicon } = await import("
|
|
2242
|
+
const { loadLexicon } = await import("../domain/grammar/lexicon.mjs");
|
|
2093
2243
|
const lex = lexicon || loadLexicon();
|
|
2094
2244
|
if (await isGroundedTerm(subjectRaw, lex, memoryDir, cache)) return "";
|
|
2095
2245
|
if (await isGroundedTerm(objectRaw, lex, memoryDir, cache)) return "";
|
|
@@ -2139,7 +2289,7 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2139
2289
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2140
2290
|
if (!m) return null;
|
|
2141
2291
|
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
2142
|
-
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("
|
|
2292
|
+
const { loadLexicon, lookupNoun, lookupAdjective, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2143
2293
|
const lex = lexicon || loadLexicon();
|
|
2144
2294
|
// A known X's own ACE miss is a real miss — never silently reinterpreted here.
|
|
2145
2295
|
if (classify(subjectRaw, lex)) return null;
|
|
@@ -2235,7 +2385,7 @@ async function unknownSubjectFallback(payload, { memoryDir, sessionId, lexicon }
|
|
|
2235
2385
|
* decline) — matching every other optional-adapter path in this file. */
|
|
2236
2386
|
async function objectReadsAsNonNoun(word) {
|
|
2237
2387
|
try {
|
|
2238
|
-
const { nlpAdapter } = await import("
|
|
2388
|
+
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
2239
2389
|
const adapter = nlpAdapter();
|
|
2240
2390
|
if (!adapter) return false;
|
|
2241
2391
|
const [tag] = adapter.posTags([String(word || "")]);
|
|
@@ -2251,7 +2401,7 @@ async function unknownObjectFallback(payload, { memoryDir, sessionId, lexicon },
|
|
|
2251
2401
|
if (!m) return null;
|
|
2252
2402
|
const [, det, subjectRaw, verb, objectRaw] = m;
|
|
2253
2403
|
if (!/^(?:every|each|all)$/i.test((det || "").trim())) return null; // class-level mint needs a real universal quantifier
|
|
2254
|
-
const { loadLexicon, lookupNoun } = await import("
|
|
2404
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
2255
2405
|
const lex = lexicon || loadLexicon();
|
|
2256
2406
|
const subjectGrounded = await isGroundedTerm(subjectRaw, lex, memoryDir, cache);
|
|
2257
2407
|
if (!subjectGrounded) return null; // ungrounded subject isn't this fallback's asymmetry — never a guessed mint
|
|
@@ -2342,7 +2492,7 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
2342
2492
|
const m = String(payload).trim().match(UNKNOWN_SUBJECT_RE);
|
|
2343
2493
|
if (!m) return null;
|
|
2344
2494
|
const [, , subjectRaw, , objectRaw] = m;
|
|
2345
|
-
const { loadLexicon, lookupNoun, classify } = await import("
|
|
2495
|
+
const { loadLexicon, lookupNoun, classify } = await import("../domain/grammar/lexicon.mjs");
|
|
2346
2496
|
const lex = lexicon || loadLexicon();
|
|
2347
2497
|
// Y already a known NOUN or a fact-grounded CLASS term — a genuine class-
|
|
2348
2498
|
// membership sentence, unknownSubjectFallback/unknownObjectFallback's own
|
|
@@ -2385,8 +2535,37 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
2385
2535
|
// A frequency/degree ADVERB commonly sits between a bare-name subject and the
|
|
2386
2536
|
// real verb ("remember that TaskController usually needs review") — without
|
|
2387
2537
|
// this skip it would mis-split VERB="usually", minting a nonsense predicate.
|
|
2388
|
-
|
|
2538
|
+
// Every word here is skippable because dropping it leaves the sentence's claim
|
|
2539
|
+
// intact: "usually needs review" and "needs review" assert the same relation at
|
|
2540
|
+
// different strengths, and tmct stores no strength. "never" is NOT one of them.
|
|
2541
|
+
// It reverses the claim, so skipping it stored the exact opposite of what the
|
|
2542
|
+
// sentence said ("tony never eats ribs" -> tony eats ribs) — a truthful teach
|
|
2543
|
+
// read back as a confident lie. It belongs to NEG_MARKER_SRC below.
|
|
2544
|
+
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|always|typically|generally|"
|
|
2389
2545
|
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually)\\s+)?";
|
|
2546
|
+
/** The negation markers a teach/query frame recognizes, in ONE place so the
|
|
2547
|
+
* teach side and the query side can never disagree about what negates a
|
|
2548
|
+
* sentence — the same discipline TEACH_ADVERB_SKIP_SRC is shared under. */
|
|
2549
|
+
const NEG_MARKER_SRC = "(?:cannot|can't|can not|does not|doesn't|do not|don't|never)";
|
|
2550
|
+
/** Split a leading negation marker off a teach payload, returning the POSITIVE
|
|
2551
|
+
* twin of the sentence plus the negation flag. Rewriting to the positive and
|
|
2552
|
+
* re-reading it through the ordinary frames is what keeps polarity out of the
|
|
2553
|
+
* parser: one recognizer, one predicate mint, one preposition fold, and the
|
|
2554
|
+
* prefix swaps at the very end (memory/capability.mjs).
|
|
2555
|
+
*
|
|
2556
|
+
* The can-family rebuilds an explicit "can" so it lands on the SAME
|
|
2557
|
+
* mgx:capableOf the corpus's own /r/CapableOf data uses; the do-family and
|
|
2558
|
+
* "never" simply drop out, leaving the bare verb the mint already reads
|
|
2559
|
+
* ("fred does not eat kale" -> "fred eat kale", "tony never eats ribs" ->
|
|
2560
|
+
* "tony eats ribs"). */
|
|
2561
|
+
const GENERAL_VERB_NEGATION_RE = new RegExp(`^(.+?)\\s+(${NEG_MARKER_SRC})\\s+(.+)$`, "i");
|
|
2562
|
+
function splitTeachNegation(payload) {
|
|
2563
|
+
const m = String(payload || "").trim().match(GENERAL_VERB_NEGATION_RE);
|
|
2564
|
+
if (!m) return { payload: String(payload || "").trim(), negated: false };
|
|
2565
|
+
const marker = m[2].toLowerCase();
|
|
2566
|
+
const canFamily = /^can/.test(marker);
|
|
2567
|
+
return { payload: `${m[1]} ${canFamily ? "can " : ""}${m[3]}`.trim(), negated: true };
|
|
2568
|
+
}
|
|
2390
2569
|
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
2391
2570
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
2392
2571
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
@@ -2395,6 +2574,28 @@ const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC
|
|
|
2395
2574
|
* back to the is/are-specific frames above/below (their own territory) or an
|
|
2396
2575
|
* honest miss — never a guessed split. */
|
|
2397
2576
|
const GENERAL_VERB_DETERMINER_RE = /^(?:every|each|all|some|a|an|the|your|my|our|their|his|her|its)$/i;
|
|
2577
|
+
/** The determiner-led sentence shape that can be read without guessing a verb
|
|
2578
|
+
* position: "the small disk rests on the middle disk". The single-token
|
|
2579
|
+
* subject bound above stands. Rather than lift it, this frame supplies the
|
|
2580
|
+
* verb-position knowledge it lacks: a closed PREP_SRC preposition must sit
|
|
2581
|
+
* immediately after the verb slot, which pins the verb by construction and so
|
|
2582
|
+
* lets the subject take a second token safely.
|
|
2583
|
+
*
|
|
2584
|
+
* The pin does real work. Widen the subject without it (strip the determiner,
|
|
2585
|
+
* allow a greedy 2-token subject) and sentences that work today garble
|
|
2586
|
+
* silently. "margo eats ribs daily" binds subject="margo eats", verb="ribs".
|
|
2587
|
+
* "the small red disk rests on the middle disk" binds subject="small red",
|
|
2588
|
+
* verb="disk", storing a nonsense mgx:disk fact. No verb-slot gate catches
|
|
2589
|
+
* that one, because "disk" is not a closed-class word. With the preposition
|
|
2590
|
+
* pinned, both decline instead.
|
|
2591
|
+
*
|
|
2592
|
+
* Costs the 3-token subject ("the small red disk …"), which declines. Nothing
|
|
2593
|
+
* in that sentence says which of its three leading words is the subject's
|
|
2594
|
+
* head, so a decline is the honest read. */
|
|
2595
|
+
const GENERAL_VERB_DETERMINER_TEACH_RE = new RegExp(
|
|
2596
|
+
`^(?:the\\s+|an?\\s+)([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+([a-z]+)\\s+(${PREP_SRC})\\s+(.+?)[.!?]*$`,
|
|
2597
|
+
"i",
|
|
2598
|
+
);
|
|
2398
2599
|
/** Verbs owned by an earlier, more specific recognizer in this lane — is/are
|
|
2399
2600
|
* (class-membership/property, above) and owns/maintains (ownership, above).
|
|
2400
2601
|
* generalVerbTeach declines outright on these so it can never race a more
|
|
@@ -2445,6 +2646,28 @@ const GENERAL_VERB_NOT_A_VERB_RE = new RegExp(
|
|
|
2445
2646
|
"i",
|
|
2446
2647
|
);
|
|
2447
2648
|
|
|
2649
|
+
/** The same failure family as GENERAL_VERB_NOT_A_VERB_RE just above, one slot
|
|
2650
|
+
* over: a LISTING IMPERATIVE's own verb sitting in the subject position.
|
|
2651
|
+
* "list modules in nope" fits GENERAL_VERB_TEACH_RE perfectly — subject
|
|
2652
|
+
* "list", verb "modules", object "in nope" — and stores a Fact whose
|
|
2653
|
+
* confirmation ("noted — remembered: list modules in nope") looks like a
|
|
2654
|
+
* successful teach for a sentence that was a query.
|
|
2655
|
+
*
|
|
2656
|
+
* A POS gate can't hold this: subjectIsNounOrPropn already runs at the bare-
|
|
2657
|
+
* sentence call site and wink tags "list" NOUN, exactly as its own docblock
|
|
2658
|
+
* concedes. So this is a closed table for the same reason that one is —
|
|
2659
|
+
* seeded from the single-word LIST_TRIGGERS (ask-vocab.mjs), the set the
|
|
2660
|
+
* listing grammar itself reads, so the two can never disagree about which
|
|
2661
|
+
* words open a listing.
|
|
2662
|
+
*
|
|
2663
|
+
* Costs "list contains three items", which becomes a miss. It already misses
|
|
2664
|
+
* in its natural determiner form ("the list contains three items"), and a
|
|
2665
|
+
* miss beats a stored garbage fact. */
|
|
2666
|
+
const GENERAL_VERB_IMPERATIVE_SUBJECT_RE = new RegExp(
|
|
2667
|
+
`^(?:${LIST_TRIGGERS.filter((t) => !/\s/.test(t)).join("|")})$`,
|
|
2668
|
+
"i",
|
|
2669
|
+
);
|
|
2670
|
+
|
|
2448
2671
|
/** The predicate a general-verb teach payload's VERB maps to. "has"/"have"
|
|
2449
2672
|
* special-cases onto the EXISTING mgx:hasA predicate (point 2) — the same
|
|
2450
2673
|
* one ConceptNet's own /r/HasA facts already use (FACT_PREDICATE_PHRASES),
|
|
@@ -2474,23 +2697,33 @@ async function generalVerbPredicate(verb) {
|
|
|
2474
2697
|
// "can a X <verb>" reader finds it (same reasoning as HAS_A above).
|
|
2475
2698
|
if (v === "can") return "mgx:capableOf";
|
|
2476
2699
|
try {
|
|
2477
|
-
const { proseLemma } = await import("
|
|
2700
|
+
const { proseLemma } = await import("../adapters/prose-nlp.mjs");
|
|
2478
2701
|
const lemma = proseLemma();
|
|
2479
2702
|
const l = lemma ? lemma(v) : v;
|
|
2480
2703
|
if (l === "have") return HAS_A_PREDICATE;
|
|
2481
|
-
return `mgx:${l}
|
|
2704
|
+
return normFactPredicate(`mgx:${l}`);
|
|
2482
2705
|
} catch {
|
|
2483
|
-
return `mgx:${v}
|
|
2706
|
+
return normFactPredicate(`mgx:${v}`);
|
|
2484
2707
|
}
|
|
2485
2708
|
}
|
|
2486
2709
|
|
|
2710
|
+
/** The capability predicate at the polarity a recognized capability surface
|
|
2711
|
+
* carried: mgx:capableOf, or its mgxneg: twin. Routed through
|
|
2712
|
+
* generalVerbPredicate's own "can" case rather than naming mgx:capableOf
|
|
2713
|
+
* again, so every capability write in this file still mints from one place. */
|
|
2714
|
+
const capabilityPredicate = async (negated) => {
|
|
2715
|
+
const p = await generalVerbPredicate("can");
|
|
2716
|
+
return negated ? negatedPredicate(p) : p;
|
|
2717
|
+
};
|
|
2718
|
+
|
|
2487
2719
|
/** Recognize + resolve a general-verb teach payload into {subject, predicate,
|
|
2488
2720
|
* object}, or null when it doesn't fit the shape / names an excluded verb /
|
|
2489
2721
|
* is missing a real subject or object (point 6 — an honest decline, never a
|
|
2490
2722
|
* guess). Pure recognition + predicate mapping; the caller (teachLane) does
|
|
2491
2723
|
* the actual write via the shared teachFact. */
|
|
2492
2724
|
async function generalVerbTeach(payload) {
|
|
2493
|
-
const
|
|
2725
|
+
const raw = String(payload || "").trim();
|
|
2726
|
+
const { payload: p, negated } = splitTeachNegation(raw);
|
|
2494
2727
|
// A genuine declarative assertion never ends in a question mark — "g day
|
|
2495
2728
|
// mate, you alright?" (Priority 1, above) reaches this function with no
|
|
2496
2729
|
// leading question-word signal left to catch it (it never matched a
|
|
@@ -2500,20 +2733,37 @@ async function generalVerbTeach(payload) {
|
|
|
2500
2733
|
if (GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(p)) return null; // another frame's territory — stand down
|
|
2501
2734
|
const m = p.match(GENERAL_VERB_TEACH_RE);
|
|
2502
2735
|
if (!m) return null;
|
|
2503
|
-
|
|
2736
|
+
let [, subjectRaw, verbRaw, objectRaw] = m;
|
|
2737
|
+
// A determiner in the subject slot means the single-token subject bound has
|
|
2738
|
+
// bound the article and misread the real subject's second word as the verb
|
|
2739
|
+
// ("the small disk rests on…" gives subject="the", verb="small"). Re-read it
|
|
2740
|
+
// with the preposition pinning the verb; a sentence that frame can't pin
|
|
2741
|
+
// declines here exactly as it always has.
|
|
2742
|
+
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) {
|
|
2743
|
+
const det = p.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
2744
|
+
if (!det) return null; // not a bare-name subject, and no preposition to pin the verb
|
|
2745
|
+
subjectRaw = det[1];
|
|
2746
|
+
verbRaw = det[2];
|
|
2747
|
+
// hand the preposition back to the shared fold below, so the minted
|
|
2748
|
+
// predicate comes from the one place that mints it
|
|
2749
|
+
objectRaw = `${det[3]} ${det[4]}`;
|
|
2750
|
+
}
|
|
2504
2751
|
const verb = verbRaw.toLowerCase();
|
|
2505
2752
|
if (GENERAL_VERB_EXCLUDE_RE.test(verb)) return null; // owned by a more specific frame above
|
|
2506
2753
|
if (GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null; // a closed-class word can never be the real verb
|
|
2507
|
-
|
|
2508
|
-
// INVERTS the taught meaning — the vocabulary has no negative-capability
|
|
2509
|
-
// predicate, so an honest decline is the only correct move.
|
|
2510
|
-
if (verb === "cannot") return null;
|
|
2511
|
-
if (GENERAL_VERB_DETERMINER_RE.test(subjectRaw)) return null; // not a bare-name subject
|
|
2754
|
+
if (GENERAL_VERB_IMPERATIVE_SUBJECT_RE.test(subjectRaw)) return null; // an imperative's verb, not a subject
|
|
2512
2755
|
const subject = subjectRaw.trim();
|
|
2756
|
+
// The preposition folds on the POSITIVE predicate, and only then does the
|
|
2757
|
+
// polarity prefix swap. Negating first would hand the fold an mgxneg: CURIE
|
|
2758
|
+
// its /^mgx:[a-z]+$/ guard rejects, stranding "on water" inside the object of
|
|
2759
|
+
// "a penguin cannot rest on water" — the very bug the fold exists to prevent.
|
|
2513
2760
|
const folded = foldPrepositionIntoPredicate(await generalVerbPredicate(verb), objectRaw);
|
|
2514
|
-
|
|
2761
|
+
// "the" strips alongside "a"/"an": the read-back side already strips a
|
|
2762
|
+
// leading determiner off the queried term, so leaving it on here stores an
|
|
2763
|
+
// object no question can match.
|
|
2764
|
+
const object = folded.object.replace(/^(?:an?|the)\s+/i, "").trim();
|
|
2515
2765
|
if (!subject || !object) return null; // no well-formed triple — honest decline (point 6)
|
|
2516
|
-
return { subject, predicate: folded.predicate, object };
|
|
2766
|
+
return { subject, predicate: negated ? negatedPredicate(folded.predicate) : folded.predicate, object };
|
|
2517
2767
|
}
|
|
2518
2768
|
|
|
2519
2769
|
/** Is `word` a genuine NOUN/PROPN, per wink-nlp's optional POS tagger
|
|
@@ -2531,7 +2781,7 @@ async function generalVerbTeach(payload) {
|
|
|
2531
2781
|
* this codebase. */
|
|
2532
2782
|
async function subjectIsNounOrPropn(word) {
|
|
2533
2783
|
try {
|
|
2534
|
-
const { nlpAdapter } = await import("
|
|
2784
|
+
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
2535
2785
|
const adapter = nlpAdapter();
|
|
2536
2786
|
if (!adapter) return false;
|
|
2537
2787
|
const [tag] = adapter.posTags([String(word || "")]);
|
|
@@ -2572,7 +2822,7 @@ async function matchRelationalVerbTeach(text) {
|
|
|
2572
2822
|
if (GENERAL_VERB_DETERMINER_RE.test(head) || GENERAL_VERB_NOT_A_VERB_RE.test(head)) return null;
|
|
2573
2823
|
}
|
|
2574
2824
|
try {
|
|
2575
|
-
const { nlpAdapter } = await import("
|
|
2825
|
+
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
2576
2826
|
const adapter = nlpAdapter();
|
|
2577
2827
|
if (!adapter) return null;
|
|
2578
2828
|
const tags = adapter.posTags([...subjWords, verbRaw, ...objWords]);
|
|
@@ -2583,7 +2833,7 @@ async function matchRelationalVerbTeach(text) {
|
|
|
2583
2833
|
}
|
|
2584
2834
|
let base = strip;
|
|
2585
2835
|
try {
|
|
2586
|
-
const { proseLemma } = await import("
|
|
2836
|
+
const { proseLemma } = await import("../adapters/prose-nlp.mjs");
|
|
2587
2837
|
const lemma = proseLemma();
|
|
2588
2838
|
if (lemma) {
|
|
2589
2839
|
const l = lemma(verb);
|
|
@@ -2616,7 +2866,7 @@ async function bareTeachWrapperNudgeText(text) {
|
|
|
2616
2866
|
if (GENERAL_VERB_DETERMINER_RE.test(head) || GENERAL_VERB_NOT_A_VERB_RE.test(head)) return null;
|
|
2617
2867
|
}
|
|
2618
2868
|
try {
|
|
2619
|
-
const { nlpAdapter } = await import("
|
|
2869
|
+
const { nlpAdapter } = await import("../adapters/ask-nlp.mjs");
|
|
2620
2870
|
const adapter = nlpAdapter();
|
|
2621
2871
|
if (!adapter) return null;
|
|
2622
2872
|
const tags = adapter.posTags([subj, verbRaw, obj]);
|
|
@@ -2738,15 +2988,23 @@ function matchBareHabitualTeach(text) {
|
|
|
2738
2988
|
* here lets the teach lane's grounded-subject direct write catch a subject
|
|
2739
2989
|
* grounded only by a prior taught fact. Same closed verb-slot exclusions as
|
|
2740
2990
|
* the habitual shapes; a question lead ("can a wren sing") never reaches
|
|
2741
|
-
* this — every call site is already QUESTION_LEAD-gated.
|
|
2991
|
+
* this — every call site is already QUESTION_LEAD-gated.
|
|
2992
|
+
*
|
|
2993
|
+
* Its NEGATIVE twin rides the same shape and returns `negated`: "a penguin
|
|
2994
|
+
* cannot fly" is the identical claim about the identical relation with the
|
|
2995
|
+
* polarity reversed, so reading it anywhere else would give the two surfaces
|
|
2996
|
+
* two chances to disagree. Only the can-family negates here — this is the
|
|
2997
|
+
* capability frame, and "penguins never fly" is a habitual surface that lands
|
|
2998
|
+
* on generalVerbTeach's own split instead. */
|
|
2999
|
+
const BARE_CAN_TEACH_RE = /^(?:an?\s+|every\s+|all\s+)?([\w-]+)\s+(can|cannot|can't|can not)\s+([a-z][\w-]*)[.!?]*$/i;
|
|
2742
3000
|
function matchBareCanTeach(text) {
|
|
2743
|
-
const m = String(text || "").trim().match(
|
|
3001
|
+
const m = String(text || "").trim().match(BARE_CAN_TEACH_RE);
|
|
2744
3002
|
if (!m) return null;
|
|
2745
3003
|
const subject = m[1].toLowerCase();
|
|
2746
|
-
const verb = m[
|
|
3004
|
+
const verb = m[3].toLowerCase();
|
|
2747
3005
|
if (STRUCT_WORDS.has(verb) || HABITUAL_VERB_EXCLUDE.has(verb) || GENERAL_VERB_NOT_A_VERB_RE.test(verb)) return null;
|
|
2748
3006
|
if (GENERAL_VERB_DETERMINER_RE.test(subject) || GENERAL_VERB_NOT_A_VERB_RE.test(subject)) return null;
|
|
2749
|
-
return { subject, verb };
|
|
3007
|
+
return { subject, verb, negated: m[2].toLowerCase() !== "can" };
|
|
2750
3008
|
}
|
|
2751
3009
|
|
|
2752
3010
|
/** The "every X is a Y" rewrite of a declarative, for the "did you mean …"
|
|
@@ -2775,9 +3033,13 @@ function teachSuggestion(payload) {
|
|
|
2775
3033
|
function habitualGroundingHintText(line, habitual) {
|
|
2776
3034
|
const articleRule = grammarRules().find((r) => r.kind === "article");
|
|
2777
3035
|
const article = articleRule && beginsWithVowelSound(habitual.subject, articleRule) ? "an" : "a";
|
|
3036
|
+
// the promise must carry the sentence's OWN polarity — promising to remember
|
|
3037
|
+
// that a penguin CAN fly, to someone who just said it cannot, is the same
|
|
3038
|
+
// inversion the negative teach exists to stop, moved into the hint
|
|
3039
|
+
const promise = habitual.negated ? `cannot ${habitual.verb}` : `can ${habitual.verb}`;
|
|
2778
3040
|
return `I don't know "${habitual.subject}" yet, so I can't store "${line}" as a capability fact. `
|
|
2779
3041
|
+ `Ground it first — say "every ${habitual.subject} is a thing" — then say "${line}" again `
|
|
2780
|
-
+ `and I'll remember that ${article} ${habitual.subject}
|
|
3042
|
+
+ `and I'll remember that ${article} ${habitual.subject} ${promise}.`;
|
|
2781
3043
|
}
|
|
2782
3044
|
|
|
2783
3045
|
/** PRONOUN-SUBJECT GUARD: "remember you are a womble" and the literal "every
|
|
@@ -2810,7 +3072,12 @@ function habitualGroundingHintText(line, habitual) {
|
|
|
2810
3072
|
* grammatical category error regardless of the verb — keeping the guard
|
|
2811
3073
|
* ahead of every teach recognizer (copula AND general-verb alike) the same
|
|
2812
3074
|
* way it already stood ahead of teachSuggestion/unknownSubjectFallback. */
|
|
2813
|
-
const
|
|
3075
|
+
const TEACH_PRONOUNS = Object.freeze(["you", "i", "it", "they", "he", "she", "we"]);
|
|
3076
|
+
const TEACH_PRONOUN_RE = new RegExp(`^(?:every\\s+|each\\s+|all\\s+|some\\s+|a few\\s+|a\\s+|an\\s+)?(${TEACH_PRONOUNS.join("|")})\\s+\\S+`, "i");
|
|
3077
|
+
/** The same closed set, read as a whole-word membership test: a pronoun is no
|
|
3078
|
+
* more a legal fact subject when a reader LIFTS one out of a prior answer than
|
|
3079
|
+
* when a teach frame offers one. */
|
|
3080
|
+
const isTeachPronoun = (s) => TEACH_PRONOUNS.includes(String(s || "").trim().toLowerCase());
|
|
2814
3081
|
|
|
2815
3082
|
/** RETRACTION / NEGATION of an already-taught subClassOf fact: "X is not a Y"
|
|
2816
3083
|
* (tolerating the same "kind/type of" infix every other teach shape in this
|
|
@@ -2820,7 +3087,7 @@ const TEACH_PRONOUN_RE = /^(?:every\s+|each\s+|all\s+|some\s+|a few\s+|a\s+|an\s
|
|
|
2820
3087
|
* UNKNOWN_SUBJECT_RE's own subject width), never a general negation grammar.
|
|
2821
3088
|
* See the RETRACTION block's own comment in teachLane (below) for why a
|
|
2822
3089
|
* regex match here is only a TRIGGER, never itself proof a fact existed to
|
|
2823
|
-
* retract — retractSubClassOf (src/syllogise.mjs) is the actual authority. */
|
|
3090
|
+
* retract — retractSubClassOf (src/domain/syllogise.mjs) is the actual authority. */
|
|
2824
3091
|
const RETRACT_NOT_A_RE = /^(?:a\s+|an\s+)?([\w-]+(?:\s+[\w-]+)?)\s+(?:(?:is|are)\s+not|isn't|aren't)\s+(?:an?\s+)?(?:(?:kind|type)\s+of\s+)?([\w-]+)$/i;
|
|
2825
3092
|
/** "forget (that) X is a Y" — the second closed retraction phrasing. Never
|
|
2826
3093
|
* wrapped by TEACH_RE ("forget" isn't one of its
|
|
@@ -2881,8 +3148,26 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2881
3148
|
// keeping recognition exactly as closed as the shapes it's equivalent to.
|
|
2882
3149
|
const stripPossessiveNamedInstance = (s) =>
|
|
2883
3150
|
(s == null ? s : s.replace(/^my\s+[a-z][\w-]*\s+([\w'-]+\s+(?:is|are)\s+.+)$/i, "$1"));
|
|
2884
|
-
|
|
2885
|
-
|
|
3151
|
+
// "disk-2's bigger than disk-1" — the contracted copula. Written out it is
|
|
3152
|
+
// the comparative frame's own sentence ("disk-2 is bigger than disk-1"), but
|
|
3153
|
+
// contracted the "is" is invisible: GENERAL_VERB_ANYWHERE_EXCLUDE_RE can't
|
|
3154
|
+
// see the copula it would have stood down for, so the general-verb frame
|
|
3155
|
+
// takes the sentence first and mints a nonsense mgx:big fact reading back
|
|
3156
|
+
// "disk-2's bigs than disk-1". Expanding it here, alongside the other
|
|
3157
|
+
// surface rewrites, puts the sentence in front of the frame that owns it.
|
|
3158
|
+
//
|
|
3159
|
+
// The lookahead needs BOTH a comparative AND "than", and that pairing is the
|
|
3160
|
+
// whole guard. A comparative alone is not a discriminator: COMPARATIVE_SRC's
|
|
3161
|
+
// "[a-z]+er" matches father, mother, brother, sister and owner, so an
|
|
3162
|
+
// expansion anchored on it turns "ahab is john's father" into "ahab is john
|
|
3163
|
+
// is father" and destroys both genitive frames. Their role slot is a bare
|
|
3164
|
+
// noun that ends the sentence, so it can never be followed by "than".
|
|
3165
|
+
const expandComparativeContraction = (s) => (s == null ? s : s.replace(
|
|
3166
|
+
new RegExp(`\\b([\\w-]+)'s(?=\\s+${COMPARATIVE_SRC}\\s+than\\b)`, "i"), "$1 is",
|
|
3167
|
+
));
|
|
3168
|
+
const surfaces = (s) => expandComparativeContraction(stripKindOf(stripYour(stripPossessiveNamedInstance(s))));
|
|
3169
|
+
const raw = surfaces(rawInput);
|
|
3170
|
+
const wrapped = surfaces(wrappedInput);
|
|
2886
3171
|
|
|
2887
3172
|
// CONJUNCTION PRE-PASS — "ahab is male and is the father of john": two
|
|
2888
3173
|
// facts about ONE subject stated in one sentence. Split at the top-level
|
|
@@ -2981,7 +3266,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2981
3266
|
}
|
|
2982
3267
|
|
|
2983
3268
|
// RETRACTION — "X is not a Y" / "forget that X is a Y": wires the
|
|
2984
|
-
// data-layer retraction primitive (retractSubClassOf, src/syllogise.mjs) up
|
|
3269
|
+
// data-layer retraction primitive (retractSubClassOf, src/domain/syllogise.mjs) up
|
|
2985
3270
|
// to chat-level phrasing. Tried here, right after the pronoun guard, so a
|
|
2986
3271
|
// pronoun subject ("it is not an animal") still falls to that
|
|
2987
3272
|
// guard's own decline first (TEACH_PRONOUN_RE matches ANY verb after the
|
|
@@ -3017,8 +3302,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3017
3302
|
if (retractMatch) {
|
|
3018
3303
|
const retractSubject = retractMatch[1].trim();
|
|
3019
3304
|
const retractObject = retractMatch[2].trim();
|
|
3020
|
-
const { retractSubClassOf } = await import("
|
|
3021
|
-
const
|
|
3305
|
+
const { retractSubClassOf } = await import("../domain/syllogise.mjs");
|
|
3306
|
+
const { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts } = await import("../adapters/memory/core.mjs");
|
|
3307
|
+
const result = await retractSubClassOf(memoryDir, retractSubject, retractObject, {
|
|
3308
|
+
store: { loadMemory: loadMemForRetract, readFactRows: readRowsForRetract, removeFacts },
|
|
3309
|
+
});
|
|
3022
3310
|
if (result.found) {
|
|
3023
3311
|
const extra = result.count - 1; // beyond the target fact itself
|
|
3024
3312
|
return {
|
|
@@ -3147,7 +3435,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3147
3435
|
const compose2 = ownSrc.match(COMPOSE2_RULE_TEACH_RE);
|
|
3148
3436
|
if (compose2 && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3149
3437
|
try {
|
|
3150
|
-
const { appendRule, RULE_KIND_COMPOSE2 } = await import("
|
|
3438
|
+
const { appendRule, RULE_KIND_COMPOSE2 } = await import("../adapters/memory/core.mjs");
|
|
3151
3439
|
const { id } = await appendRule(memoryDir, {
|
|
3152
3440
|
name: compose2[1],
|
|
3153
3441
|
kind: RULE_KIND_COMPOSE2,
|
|
@@ -3172,7 +3460,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3172
3460
|
const filterRule = ownSrc.match(FILTER_RULE_TEACH_RE);
|
|
3173
3461
|
if (filterRule && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3174
3462
|
try {
|
|
3175
|
-
const { appendRule, RULE_KIND_FILTER } = await import("
|
|
3463
|
+
const { appendRule, RULE_KIND_FILTER } = await import("../adapters/memory/core.mjs");
|
|
3176
3464
|
const { id } = await appendRule(memoryDir, {
|
|
3177
3465
|
name: filterRule[1],
|
|
3178
3466
|
kind: RULE_KIND_FILTER,
|
|
@@ -3201,7 +3489,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3201
3489
|
const recursiveRule = ownSrc.match(RECURSIVE_RULE_TEACH_RE);
|
|
3202
3490
|
if (recursiveRule && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3203
3491
|
try {
|
|
3204
|
-
const { appendRule, RULE_KIND_RECURSIVE } = await import("
|
|
3492
|
+
const { appendRule, RULE_KIND_RECURSIVE } = await import("../adapters/memory/core.mjs");
|
|
3205
3493
|
const { id } = await appendRule(memoryDir, {
|
|
3206
3494
|
name: recursiveRule[1],
|
|
3207
3495
|
kind: RULE_KIND_RECURSIVE,
|
|
@@ -3237,7 +3525,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3237
3525
|
try {
|
|
3238
3526
|
const verb = await actionLemma(actionSig[1]);
|
|
3239
3527
|
const prep = actionSig[3].toLowerCase();
|
|
3240
|
-
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("
|
|
3528
|
+
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("../adapters/memory/core.mjs");
|
|
3241
3529
|
const { id } = await appendRule(memoryDir, {
|
|
3242
3530
|
name: `${verb} ${prep}`,
|
|
3243
3531
|
kind: RULE_KIND_ACTION_SIGNATURE,
|
|
@@ -3262,7 +3550,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3262
3550
|
if (verb !== participle && participle.startsWith(verb.slice(0, Math.min(3, verb.length)))) {
|
|
3263
3551
|
try {
|
|
3264
3552
|
const prep = actionSigPassive[3].toLowerCase();
|
|
3265
|
-
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("
|
|
3553
|
+
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("../adapters/memory/core.mjs");
|
|
3266
3554
|
const { id } = await appendRule(memoryDir, {
|
|
3267
3555
|
name: `${verb} ${prep}`,
|
|
3268
3556
|
kind: RULE_KIND_ACTION_SIGNATURE,
|
|
@@ -3293,7 +3581,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3293
3581
|
const prep = precondNothing[3].toLowerCase();
|
|
3294
3582
|
const innerVerb = await actionLemma(precondNothing[5]);
|
|
3295
3583
|
const scopeWord = precondNothing[4].toLowerCase();
|
|
3296
|
-
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("
|
|
3584
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("../adapters/memory/core.mjs");
|
|
3297
3585
|
const { id } = await appendRule(memoryDir, {
|
|
3298
3586
|
name: `${verb} ${prep}`,
|
|
3299
3587
|
kind: RULE_KIND_ACTION_PRECOND,
|
|
@@ -3331,7 +3619,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3331
3619
|
const verb = await actionLemma(precondComp[1]);
|
|
3332
3620
|
const prep = precondComp[3].toLowerCase();
|
|
3333
3621
|
const scopeWord = precondComp[4].toLowerCase();
|
|
3334
|
-
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("
|
|
3622
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("../adapters/memory/core.mjs");
|
|
3335
3623
|
const { id } = await appendRule(memoryDir, {
|
|
3336
3624
|
name: `${verb} ${prep}`,
|
|
3337
3625
|
kind: RULE_KIND_ACTION_PRECOND,
|
|
@@ -3357,7 +3645,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3357
3645
|
try {
|
|
3358
3646
|
const verb = await actionLemma(actionConstraint[1]);
|
|
3359
3647
|
const prep = actionConstraint[3].toLowerCase();
|
|
3360
|
-
const { appendRule, RULE_KIND_ACTION_CONSTRAINT } = await import("
|
|
3648
|
+
const { appendRule, RULE_KIND_ACTION_CONSTRAINT } = await import("../adapters/memory/core.mjs");
|
|
3361
3649
|
const { id } = await appendRule(memoryDir, {
|
|
3362
3650
|
name: `${verb} ${prep}`,
|
|
3363
3651
|
kind: RULE_KIND_ACTION_CONSTRAINT,
|
|
@@ -3398,7 +3686,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3398
3686
|
// A subject word naming neither the subject class nor "target" is
|
|
3399
3687
|
// CLASS-BOUND: a companion that travels with every move ("ferrying a
|
|
3400
3688
|
// passenger onto a bank makes the FARMER stand on the target"). Stored as
|
|
3401
|
-
// the bare class word; compileDomain (src/domain.mjs) requires the class
|
|
3689
|
+
// the bare class word; compileDomain (src/domain/domain.mjs) requires the class
|
|
3402
3690
|
// to have exactly one member at plan time, so a typo'd word fails loudly
|
|
3403
3691
|
// there rather than silently minting a role here.
|
|
3404
3692
|
const subjectRole = namedSubjectRole ?? subjectWord.toLowerCase();
|
|
@@ -3412,7 +3700,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3412
3700
|
try {
|
|
3413
3701
|
const prep = actionEffect[3].toLowerCase();
|
|
3414
3702
|
const effVerb = await actionLemma(actionEffect[6]);
|
|
3415
|
-
const { appendRule, RULE_KIND_ACTION_EFFECT } = await import("
|
|
3703
|
+
const { appendRule, RULE_KIND_ACTION_EFFECT } = await import("../adapters/memory/core.mjs");
|
|
3416
3704
|
const { id } = await appendRule(memoryDir, {
|
|
3417
3705
|
name: `${verb} ${prep}`,
|
|
3418
3706
|
kind: RULE_KIND_ACTION_EFFECT,
|
|
@@ -3461,7 +3749,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3461
3749
|
const quantifier = someMatch[1].toLowerCase();
|
|
3462
3750
|
const subject = singularizeSurface(someMatch[2]);
|
|
3463
3751
|
const object = singularizeSurface(someMatch[3]);
|
|
3464
|
-
const { loadLexicon, lookupNoun } = await import("
|
|
3752
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
3465
3753
|
const lex = lexicon || loadLexicon();
|
|
3466
3754
|
if (lookupNoun(lex, object)) {
|
|
3467
3755
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
@@ -3528,7 +3816,13 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3528
3816
|
// (wink's honest fallback for any unrecognized token, not a real signal),
|
|
3529
3817
|
// which would otherwise mis-store it as a fact instead of leaving it for
|
|
3530
3818
|
// the structural grammar's own typo-tolerant retry to answer for real.
|
|
3531
|
-
|
|
3819
|
+
// A determiner-led sentence opens with the article, which never POS-tags as
|
|
3820
|
+
// a noun, so the first word is the wrong word to gate on. When the
|
|
3821
|
+
// pinned-preposition frame can identify a real subject, POS-check the head
|
|
3822
|
+
// of THAT subject — the same word generalVerbTeach will store — and leave
|
|
3823
|
+
// every other sentence reading its first word exactly as before.
|
|
3824
|
+
const detLed = raw.match(GENERAL_VERB_DETERMINER_TEACH_RE);
|
|
3825
|
+
const subjectWord = detLed ? detLed[1].split(/\s+/).pop() : raw.match(/^([\w'-]+)/)?.[1];
|
|
3532
3826
|
if (subjectWord && (await subjectIsNounOrPropn(subjectWord))) {
|
|
3533
3827
|
// A PLURAL explicit-capability surface ("wrens can hum") whose
|
|
3534
3828
|
// SINGULAR is a grounded term stores under the singular first — the
|
|
@@ -3540,10 +3834,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3540
3834
|
const canSingular = canShape ? singularizeSurface(canShape.subject) : null;
|
|
3541
3835
|
if (canShape && canSingular !== canShape.subject) {
|
|
3542
3836
|
let canLex = lexicon;
|
|
3543
|
-
if (!canLex) { const { loadLexicon } = await import("
|
|
3837
|
+
if (!canLex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); canLex = loadLexicon(); }
|
|
3544
3838
|
if (await isGroundedTerm(canSingular, canLex, memoryDir, cache)) {
|
|
3545
3839
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
3546
|
-
subject: canSingular, predicate: await
|
|
3840
|
+
subject: canSingular, predicate: await capabilityPredicate(canShape.negated), object: canShape.verb,
|
|
3547
3841
|
});
|
|
3548
3842
|
if (stored) return stored;
|
|
3549
3843
|
}
|
|
@@ -3608,7 +3902,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3608
3902
|
const habitualTeach = matchBareHabitualTeach(payload) || matchBareCanTeach(payload);
|
|
3609
3903
|
if (habitualTeach) {
|
|
3610
3904
|
let habLex = lexicon;
|
|
3611
|
-
if (!habLex) { const { loadLexicon } = await import("
|
|
3905
|
+
if (!habLex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); habLex = loadLexicon(); }
|
|
3612
3906
|
// The singular is preferred so an explicit plural surface ("penguins
|
|
3613
3907
|
// can swim") stores under the same spelling the grounding fact (and
|
|
3614
3908
|
// every query-side variant fold) uses; a proper noun that only looks
|
|
@@ -3616,7 +3910,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3616
3910
|
for (const subj of new Set([singularizeSurface(habitualTeach.subject), habitualTeach.subject])) {
|
|
3617
3911
|
if (await isGroundedTerm(subj, habLex, memoryDir, cache)) {
|
|
3618
3912
|
const stored = await teachFact(memoryDir, sessionId, {
|
|
3619
|
-
subject: subj, predicate: await
|
|
3913
|
+
subject: subj, predicate: await capabilityPredicate(habitualTeach.negated), object: habitualTeach.verb,
|
|
3620
3914
|
});
|
|
3621
3915
|
if (stored) return stored;
|
|
3622
3916
|
}
|
|
@@ -3686,9 +3980,9 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
3686
3980
|
let unknown = [];
|
|
3687
3981
|
if (memoryDir) {
|
|
3688
3982
|
try {
|
|
3689
|
-
const { parseAce } = await import("
|
|
3983
|
+
const { parseAce } = await import("../domain/grammar/ace.mjs");
|
|
3690
3984
|
let lex = lexicon;
|
|
3691
|
-
if (!lex) { const { loadLexicon } = await import("
|
|
3985
|
+
if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
3692
3986
|
for (const cand of assertCandidates(payload)) {
|
|
3693
3987
|
const parse = parseAce(cand, lex);
|
|
3694
3988
|
if (parse?.residue?.length) { unknown = [...new Set(parse.residue.map((w) => String(w).toLowerCase()))]; break; }
|
|
@@ -4168,7 +4462,7 @@ async function presuppositionNudge(query, { graph, memoryDir }) {
|
|
|
4168
4462
|
let propHit = null;
|
|
4169
4463
|
if (memoryDir) {
|
|
4170
4464
|
let normFactTerm;
|
|
4171
|
-
try { ({ normFactTerm } = await import("
|
|
4465
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { normFactTerm = null; }
|
|
4172
4466
|
if (normFactTerm) {
|
|
4173
4467
|
const facts = await memoryFacts(memoryDir);
|
|
4174
4468
|
const subjMatches = (f) => normFactTerm(f.subject) === normFactTerm(entityTerm);
|
|
@@ -4211,18 +4505,6 @@ const ORIENTATION_REPEAT_ONELINER = "still the same overview — /help lists eve
|
|
|
4211
4505
|
* independent repeat-suppression sites can never be confused with one another. */
|
|
4212
4506
|
const META_ORIENT_REPEAT_ONELINER = "still the same overview — /stats for the full one, /help for commands.";
|
|
4213
4507
|
|
|
4214
|
-
// ---- repo-root resolution: default the target to the GIT ROOT, not raw cwd ----
|
|
4215
|
-
|
|
4216
|
-
/** The git top-level for `cwd`, or null if not in a repo (or git is unavailable).
|
|
4217
|
-
* Injected into runChat so tests exercise repo resolution without a real git tree. */
|
|
4218
|
-
export function gitToplevel(cwd = process.cwd()) {
|
|
4219
|
-
try {
|
|
4220
|
-
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
4221
|
-
if (r.status === 0) { const p = String(r.stdout || "").trim(); return p || null; }
|
|
4222
|
-
} catch { /* git missing / not a repo — fall back to cwd */ }
|
|
4223
|
-
return null;
|
|
4224
|
-
}
|
|
4225
|
-
|
|
4226
4508
|
/** Resolve a free-text term to a single graph entity via the ask engine's own
|
|
4227
4509
|
* tiered resolver — {id,label} on a UNIQUE hit, null on a miss/ambiguity/no graph.
|
|
4228
4510
|
* Lazy + failure-tolerated (see the file docblock): the worst case is a turn that
|
|
@@ -4234,7 +4516,7 @@ export function gitToplevel(cwd = process.cwd()) {
|
|
|
4234
4516
|
async function resolveEntity(graph, term) {
|
|
4235
4517
|
if (!graph || !term) return null;
|
|
4236
4518
|
try {
|
|
4237
|
-
const { resolveObject } = await import("
|
|
4519
|
+
const { resolveObject } = await import("../domain/ask.mjs");
|
|
4238
4520
|
const r = resolveObject(graph, term);
|
|
4239
4521
|
if (r?.match?.id && !r.ambiguous) return { id: r.match.id, label: r.match.label };
|
|
4240
4522
|
} catch { /* tolerated */ }
|
|
@@ -4252,6 +4534,8 @@ export async function helpText() {
|
|
|
4252
4534
|
["/memory [verbose]", "what tmct remembers: facts, utterances, sessions, folded blocks"],
|
|
4253
4535
|
["/focus <symbol>", "set the current focus (reused by 'it'/'this' and no-arg entity commands)"],
|
|
4254
4536
|
["/plan <request>", "the capability router: plan+execute a compound or maintenance-goal request (\"of the modules impacted by X, which are untested\", \"what most needs a test\")"],
|
|
4537
|
+
["/capabilities", "what /plan can plan over: the built-in graph tools plus your taught actions"],
|
|
4538
|
+
["/syllogise <term>", "work out and remember what follows from the facts about a term (needed for chains longer than 2 hops)"],
|
|
4255
4539
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
4256
4540
|
["/help", "this list"],
|
|
4257
4541
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
@@ -4259,7 +4543,7 @@ export async function helpText() {
|
|
|
4259
4543
|
const w = Math.max(...rows.map(([a]) => a.length));
|
|
4260
4544
|
const lines = rows.map(([a, b]) => ` ${a.padEnd(w)} ${b}`);
|
|
4261
4545
|
let shapes;
|
|
4262
|
-
try { const { rephraseHint } = await import("
|
|
4546
|
+
try { const { rephraseHint } = await import("../domain/ask.mjs"); shapes = rephraseHint(); }
|
|
4263
4547
|
catch {
|
|
4264
4548
|
// "touch" dropped from this cross-product for the same reason rephraseHint() drops it
|
|
4265
4549
|
// (ask.mjs) — Module/Function/Class is never the subject of a touch edge, only Commit.
|
|
@@ -4278,7 +4562,7 @@ export async function helpText() {
|
|
|
4278
4562
|
|
|
4279
4563
|
/** The conservative relevance floor a folded-session block must clear (the
|
|
4280
4564
|
* retrieveBlocks idf×(1+rank) score) before an honest ask-miss is answered from
|
|
4281
|
-
* memory. Calibrated in the small-corpus regime (test/wiring-recall.test.mjs):
|
|
4565
|
+
* memory. Calibrated in the small-corpus regime (test/tools/wiring-recall.test.mjs):
|
|
4282
4566
|
* a genuine re-ask scores ~4, a frame-word coincidence ~1. */
|
|
4283
4567
|
export const RECALL_MIN_SCORE = 2.0;
|
|
4284
4568
|
|
|
@@ -4388,7 +4672,7 @@ async function bestQaPair(blockText, query, graph) {
|
|
|
4388
4672
|
* Lazy + failure-tolerated (chat.mjs ethos): a broken store degrades to null. */
|
|
4389
4673
|
async function recallFromBlocks(memoryDir, query, graph) {
|
|
4390
4674
|
try {
|
|
4391
|
-
const { retrieveBlocks } = await import("
|
|
4675
|
+
const { retrieveBlocks } = await import("../adapters/memory/blocks.mjs");
|
|
4392
4676
|
const hits = await retrieveBlocks(memoryDir, query, RECALL_TOP_K);
|
|
4393
4677
|
const best = hits[0];
|
|
4394
4678
|
if (!best || best.score < RECALL_MIN_SCORE || !best.text) return null;
|
|
@@ -4463,9 +4747,40 @@ function thirdPersonSingularSurface(lemma) {
|
|
|
4463
4747
|
if (/(?:s|x|z|ch|sh|o)$/i.test(w)) return `${w}es`;
|
|
4464
4748
|
return `${w}s`;
|
|
4465
4749
|
}
|
|
4750
|
+
/** The INVERSE of thirdPersonSingularSurface — "eats" -> "eat", "flies" ->
|
|
4751
|
+
* "fly", "has" -> "have". Do-support wants the bare infinitive after it
|
|
4752
|
+
* ("does not EAT", never "does not eats"), and so does every derived
|
|
4753
|
+
* forward yes/no reader ("does X cause Y"), so both fold through this one
|
|
4754
|
+
* function and can never drift apart on a verb. */
|
|
4755
|
+
function baseVerbSurface(verb) {
|
|
4756
|
+
const w = String(verb || "");
|
|
4757
|
+
if (/^has$/i.test(w)) return "have";
|
|
4758
|
+
if (/[a-z]ies$/i.test(w) && !/[aeiou]ies$/i.test(w)) return `${w.slice(0, -3)}y`;
|
|
4759
|
+
if (/(?:s|x|z|ch|sh|o)es$/i.test(w)) return w.slice(0, -2);
|
|
4760
|
+
return w.replace(/s$/i, "");
|
|
4761
|
+
}
|
|
4466
4762
|
function predicatePhrase(predicate) {
|
|
4467
4763
|
if (FACT_PREDICATE_PHRASES[predicate]) return FACT_PREDICATE_PHRASES[predicate];
|
|
4468
4764
|
const p = String(predicate || "");
|
|
4765
|
+
// NEGATIVE polarity renders as its own positive phrase, negated — ONE branch
|
|
4766
|
+
// for every predicate that can carry a polarity, curated or minted. The
|
|
4767
|
+
// negative twins are deliberately absent from FACT_PREDICATE_PHRASES: the
|
|
4768
|
+
// TRAILING_PREDICATE_MARKERS / REVERSE_PREDICATE_MARKERS /
|
|
4769
|
+
// FORWARD_YESNO_MARKERS families all derive their vocabulary from that table,
|
|
4770
|
+
// so an entry there would auto-mint readers for "what cannot X" and
|
|
4771
|
+
// "does X cannot Y" that nobody wrote and nothing pins.
|
|
4772
|
+
// The three surface shapes split exactly as FORWARD_YESNO_MARKERS splits
|
|
4773
|
+
// them, for the same reason: a modal, a copula and a plain verb take
|
|
4774
|
+
// different negations, and nothing else does.
|
|
4775
|
+
const positive = positivePredicate(p);
|
|
4776
|
+
if (positive) {
|
|
4777
|
+
const phrase = predicatePhrase(positive);
|
|
4778
|
+
if (phrase === "can") return "cannot";
|
|
4779
|
+
if (phrase === "can be") return "cannot be";
|
|
4780
|
+
if (phrase === "is" || phrase.startsWith("is ")) return `is not${phrase.slice(2)}`;
|
|
4781
|
+
const [head, ...tail] = phrase.split(" ");
|
|
4782
|
+
return ["does not", baseVerbSurface(head), ...tail].join(" ");
|
|
4783
|
+
}
|
|
4469
4784
|
// a comparative renders as its copula surface: mgx:smaller-than ->
|
|
4470
4785
|
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
4471
4786
|
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
@@ -4568,7 +4883,7 @@ function renderIsaChain(premises) {
|
|
|
4568
4883
|
* object, provenance} rows. Lazy + failure-tolerated: no memory → []. */
|
|
4569
4884
|
async function memoryFacts(memoryDir) {
|
|
4570
4885
|
try {
|
|
4571
|
-
const { loadMemory } = await import("
|
|
4886
|
+
const { loadMemory } = await import("../adapters/memory/core.mjs");
|
|
4572
4887
|
const m = await loadMemory(memoryDir);
|
|
4573
4888
|
const out = [];
|
|
4574
4889
|
for (const ind of m.individuals || []) {
|
|
@@ -4601,7 +4916,7 @@ async function memoryFacts(memoryDir) {
|
|
|
4601
4916
|
async function factRows(memoryDir, cache = null) {
|
|
4602
4917
|
if (cache?.rows) return cache.rows;
|
|
4603
4918
|
try {
|
|
4604
|
-
const { loadMemory, readFactRows } = await import("
|
|
4919
|
+
const { loadMemory, readFactRows } = await import("../adapters/memory/core.mjs");
|
|
4605
4920
|
const rows = readFactRows(await loadMemory(memoryDir));
|
|
4606
4921
|
if (cache) { cache.rows = rows; cache.reloads = (cache.reloads || 0) + 1; }
|
|
4607
4922
|
return rows;
|
|
@@ -4713,7 +5028,7 @@ async function synonymIndex() {
|
|
|
4713
5028
|
if (!index.get(tb).some((e) => e.variant === ta)) index.get(tb).push({ variant: ta, source });
|
|
4714
5029
|
};
|
|
4715
5030
|
try {
|
|
4716
|
-
const { loadSlice, loadMap, termText } = await import("
|
|
5031
|
+
const { loadSlice, loadMap, termText } = await import("../adapters/corpus/conceptnet.mjs");
|
|
4717
5032
|
const [assertions, map] = await Promise.all([loadSlice(), loadMap()]);
|
|
4718
5033
|
const SINGLE_WORD_RE = /^[a-z]+$/;
|
|
4719
5034
|
for (const a of assertions) {
|
|
@@ -4726,7 +5041,7 @@ async function synonymIndex() {
|
|
|
4726
5041
|
}
|
|
4727
5042
|
} catch { /* corpus unavailable — degrade gracefully */ }
|
|
4728
5043
|
try {
|
|
4729
|
-
const { loadPhrasebook } = await import("
|
|
5044
|
+
const { loadPhrasebook } = await import("../adapters/corpus/templates.mjs");
|
|
4730
5045
|
const { synonyms } = await loadPhrasebook();
|
|
4731
5046
|
for (const family of synonyms) {
|
|
4732
5047
|
for (let i = 0; i < family.length; i += 1) {
|
|
@@ -4816,7 +5131,7 @@ function matchGenitiveWhoAsk(q) {
|
|
|
4816
5131
|
* the same naive plural fold SOME_A_FEW_RE's own teach-side surface already
|
|
4817
5132
|
* uses elsewhere in this file), `m[2]` = the start entity ("ahab"). Dispatch
|
|
4818
5133
|
* lives in factReadBack's own (a0.5) block, below — findRuleByName +
|
|
4819
|
-
* findReachableSet (src/planning.mjs), never a yes/no answer. */
|
|
5134
|
+
* findReachableSet (src/domain/planning.mjs), never a yes/no answer. */
|
|
4820
5135
|
const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([\w'-]+(?:\s+[A-Z][\w'-]*)?)[?.!\s]*$/i;
|
|
4821
5136
|
|
|
4822
5137
|
/** "is a module a component" — the yes/no vocabulary form the graph grammar
|
|
@@ -4824,6 +5139,14 @@ const RECURSIVE_LIST_ASK_RE = /^list\s+(?:the\s+|all\s+)?([a-z][\w-]*)\s+of\s+([
|
|
|
4824
5139
|
const ISA_ASK_RE = /^(?:is|are)\s+(?:an?\s+)?(.+?)\s+(?:a\s+kind\s+of|a\s+type\s+of|an?)\s+(.+?)[?.!\s]*$/i;
|
|
4825
5140
|
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
4826
5141
|
|
|
5142
|
+
/** How far the isa ladder's miss text probes for a chain it can name a
|
|
5143
|
+
* recovery for. Purely a REPORTING reach: the live chases answer within their
|
|
5144
|
+
* own hop bounds and this never widens them, it only tells the miss whether
|
|
5145
|
+
* "/syllogise <term>" would find anything. findIsaChain's own default, since
|
|
5146
|
+
* the probe wants the search's natural reach rather than a second opinion
|
|
5147
|
+
* about how deep is worth walking. */
|
|
5148
|
+
const DEEP_CHAIN_PROBE_HOPS = 6;
|
|
5149
|
+
|
|
4827
5150
|
/** "why is TaskController a handler" / "explain how you know TaskController is
|
|
4828
5151
|
* a handler" — the syllogise-verified proof render (the isaAsk block below,
|
|
4829
5152
|
* which cites the graph inherits-bridge / taught-fact chase / entailed
|
|
@@ -4914,6 +5237,16 @@ const WHERE_IS_FACT_RE = /^where(?:'s|\s+is|\s+are)\s+(.+?)(?:\s+now)?\s*[?.!]*$
|
|
|
4914
5237
|
* (mgx:rest-on, mgx:stand-on, mgx:sit-in, …) — what makes a taught fact a
|
|
4915
5238
|
* LOCATION answer rather than any arbitrary relation. */
|
|
4916
5239
|
const LOCATIVE_FACT_PREDICATE_RE = /^mgx:[a-z]+-(?:on|in|at|inside|under|below|above|near|beside|behind|by)$/;
|
|
5240
|
+
/** "what is on peg-a" / "what's on peg-a" — the reverse-by-OBJECT mirror of
|
|
5241
|
+
* WHERE_IS_FACT_RE, over the same taught locative facts. The bare copula
|
|
5242
|
+
* carries no verb to mint a predicate from, so the PREPOSITION is the anchor:
|
|
5243
|
+
* it's captured here and matched against the folded predicate's own tail, so
|
|
5244
|
+
* "what is on peg-a" can only ever answer with a fact that really says "on"
|
|
5245
|
+
* (a "-under" row is a different claim, never this question's answer).
|
|
5246
|
+
* Consumed by factAnswer's (a-pre5) reader, which diverts only on a real
|
|
5247
|
+
* stored hit — "what is on the roadmap" finds no such fact and falls through
|
|
5248
|
+
* to the ordinary BARE_WHATIS_RE handling untouched. */
|
|
5249
|
+
const WHAT_IS_PREP_FACT_RE = new RegExp(`^what(?:'s|\\s+is|\\s+are)\\s+(${PREP_SRC})\\s+(.+?)\\s*[?.!]*$`, "i");
|
|
4917
5250
|
|
|
4918
5251
|
// CAN_ASK_RE's remaining paraphrase-ladder siblings, all over the same
|
|
4919
5252
|
// mgx:capableOf facts:
|
|
@@ -4936,6 +5269,152 @@ const DO_VERB_ASK_RE = /^(?:do|does)\s+(all\s+|every\s+)?(?:an?\s+|the\s+)?([\w'
|
|
|
4936
5269
|
const WHAT_CAN_VERB_RE = /^what\s+can\s+(?!be\s)(.+?)[?.!\s]*$/i;
|
|
4937
5270
|
const WHICH_KIND_CAN_RE = /^(?:which|what)\s+([\w'-]+(?:\s+[\w'-]+)*?)\s+can\s+(.+?)[?.!\s]*$/i;
|
|
4938
5271
|
|
|
5272
|
+
/** The negative surface of a yes/no question asks the SAME question as its
|
|
5273
|
+
* positive twin — "can't a penguin fly" and "can a penguin fly" both want the
|
|
5274
|
+
* polarity of penguin's flight, and a reader that answered them differently
|
|
5275
|
+
* would be disagreeing with itself in one session. So the negation is stripped
|
|
5276
|
+
* here and the ordinary reader answers, carrying whatever polarity the facts
|
|
5277
|
+
* actually hold. A question with no negation in it comes back byte-identical,
|
|
5278
|
+
* so every existing surface reads exactly as it always has.
|
|
5279
|
+
*
|
|
5280
|
+
* Applied ONLY inside the capability + general-verb readers, never to `q` at
|
|
5281
|
+
* large: "is a task not an animal" is the retraction lane's copula surface,
|
|
5282
|
+
* and this must never reach it.
|
|
5283
|
+
*
|
|
5284
|
+
* Those readers match this surface INSTEAD of the raw question, never as a
|
|
5285
|
+
* fallback after it. A lazy subject slot happily swallows the negation word
|
|
5286
|
+
* itself — "do penguins not fly" binds subject "penguins not" and matches — so
|
|
5287
|
+
* trying the raw question first would take a garbage bind over the good one. */
|
|
5288
|
+
function positiveQuestionSurface(q) {
|
|
5289
|
+
const s = String(q || "")
|
|
5290
|
+
.replace(/^(?:can't|cannot|can not)\s+/i, "can ")
|
|
5291
|
+
.replace(/^(?:doesn't|does not)\s+/i, "does ")
|
|
5292
|
+
.replace(/^(?:don't|do not)\s+/i, "do ")
|
|
5293
|
+
.replace(/^(?:didn't|did not)\s+/i, "did ")
|
|
5294
|
+
.replace(/\s+(?:not|never)\s+/i, " ");
|
|
5295
|
+
return s.replace(/\s+/g, " ").trim();
|
|
5296
|
+
}
|
|
5297
|
+
|
|
5298
|
+
/** Cite an isa chain the way (b3b) already cites one — each step as its own
|
|
5299
|
+
* phrase plus verbatim source. Shared so the inherited-capability answers and
|
|
5300
|
+
* the reverse-by-kind listing can never describe the same chain two ways. */
|
|
5301
|
+
function renderIsaCite(chain, facts) {
|
|
5302
|
+
const steps = (chain || []).map((step) => facts.find(
|
|
5303
|
+
(f) => f.predicate === step.predicate && f.subject === step.subject && f.object === step.object,
|
|
5304
|
+
));
|
|
5305
|
+
if (!steps.length || !steps.every(Boolean)) return null;
|
|
5306
|
+
return steps.map((g) => `${factPhrase(g)}${g.provenance ? ` (source: ${g.provenance})` : ""}`).join("; ");
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
/** THE capability answer — every reader that asks "can X do Y" renders through
|
|
5310
|
+
* this one function, over the one resolver. Five readers with five local
|
|
5311
|
+
* polarity filters would drift, and the drift is invisible: each would answer
|
|
5312
|
+
* confidently from one side while a negative it never looked at sat in the
|
|
5313
|
+
* store. "do penguins fly" and "can a penguin fly" must not disagree inside a
|
|
5314
|
+
* single session.
|
|
5315
|
+
*
|
|
5316
|
+
* Returns null when the store holds no capability claim about the subject at
|
|
5317
|
+
* either specificity. The caller then keeps its own honest-miss text, and
|
|
5318
|
+
* falls to capabilityBaseRateReply only once that has nothing either: a
|
|
5319
|
+
* subject with capability facts of its own ("a dog can bark") is better
|
|
5320
|
+
* answered by citing them than by reciting what other animals do.
|
|
5321
|
+
*/
|
|
5322
|
+
function capabilityReply(subjectText, objectText, facts, { maxHops = 3 } = {}) {
|
|
5323
|
+
const subj = factTermVariants(normFactTermStatic, subjectText);
|
|
5324
|
+
const obj = factTermVariants(normFactTermStatic, objectText);
|
|
5325
|
+
const r = resolveCapabilityPolarity(subj, obj, facts, { maxHops });
|
|
5326
|
+
|
|
5327
|
+
const viaChain = (chain) => {
|
|
5328
|
+
const cite = chain && chain.length ? renderIsaCite(chain, facts) : null;
|
|
5329
|
+
return cite ? ` — via: ${cite}` : "";
|
|
5330
|
+
};
|
|
5331
|
+
|
|
5332
|
+
// both polarities at the same specificity: the disagreement is between the
|
|
5333
|
+
// SOURCES, not inside the knowledge, so both are true statements about who
|
|
5334
|
+
// said what. Report them and pick nothing.
|
|
5335
|
+
if (r.verdict === "both") {
|
|
5336
|
+
const lines = [...r.negative, ...r.positive].map(renderFactLine).join("\n");
|
|
5337
|
+
return {
|
|
5338
|
+
text: `I have both, at the same level of detail — my sources disagree, so I won't pick:\n${lines}`,
|
|
5339
|
+
replace: true,
|
|
5340
|
+
miss: true,
|
|
5341
|
+
};
|
|
5342
|
+
}
|
|
5343
|
+
|
|
5344
|
+
if (r.verdict === "yes" || r.verdict === "no") {
|
|
5345
|
+
const winner = r.verdict === "no" ? r.negative[0] : r.positive[0];
|
|
5346
|
+
let text = `${r.verdict} — ${renderFactLine(winner)}${viaChain(r.chain)}`;
|
|
5347
|
+
// a direct fact beat a general default: say WHAT it overrides, or the
|
|
5348
|
+
// answer silently contradicts what the same store says about the class
|
|
5349
|
+
if (r.overrides) {
|
|
5350
|
+
text += `. That overrides what I know about ${r.overrides.fact.subject} generally: ${renderFactLine(r.overrides.fact)}`;
|
|
5351
|
+
}
|
|
5352
|
+
return { text, replace: true };
|
|
5353
|
+
}
|
|
5354
|
+
|
|
5355
|
+
return null;
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5358
|
+
/** Nothing is known about the subject's capability. Report the CLASS it belongs
|
|
5359
|
+
* to and how that class's other kinds split, then STOP — neither yes nor no.
|
|
5360
|
+
* That is the only reading of "birds fly" that survives a penguin.
|
|
5361
|
+
*
|
|
5362
|
+
* Two axes, in order: the class base rate, and — when the class yields nothing
|
|
5363
|
+
* either way — the predicate's own extension ("but I do know 3 things that can
|
|
5364
|
+
* fly"). The pivot excludes the class itself: "I don't know if a penguin can
|
|
5365
|
+
* fly, but I know birds can" is circular, not informative.
|
|
5366
|
+
*
|
|
5367
|
+
* Returns null unless the subject has a known class. Without one there is no
|
|
5368
|
+
* base rate to report and no reason to believe the subject is a real term at
|
|
5369
|
+
* all — an unresolved "it" belongs to the pronoun lane, not here.
|
|
5370
|
+
*/
|
|
5371
|
+
function capabilityBaseRateReply(subjectText, objectText, facts, { maxHops = 3 } = {}) {
|
|
5372
|
+
const subj = factTermVariants(normFactTermStatic, subjectText);
|
|
5373
|
+
const obj = factTermVariants(normFactTermStatic, objectText);
|
|
5374
|
+
const baseRate = capabilityBaseRate(subj, obj, facts, { maxHops });
|
|
5375
|
+
if (!baseRate) return null;
|
|
5376
|
+
const lead = `${subjectText} is a kind of ${baseRate.klass}`;
|
|
5377
|
+
const opener = `I don't know if ${subjectText} can ${objectText}.`;
|
|
5378
|
+
|
|
5379
|
+
if (baseRate.positive.length || baseRate.negative.length) {
|
|
5380
|
+
// The split accounts for EVERY kind it counted — three ways, positive,
|
|
5381
|
+
// negative and unknown. Say 5 and split only 4 and the arithmetic lies
|
|
5382
|
+
// about what the store knows. The count is a fact about the kinds it has
|
|
5383
|
+
// seen; "most birds fly" would be a claim about the ones it has not.
|
|
5384
|
+
const split = [
|
|
5385
|
+
`${baseRate.positive.length} can ${objectText}`,
|
|
5386
|
+
`${baseRate.negative.length} cannot`,
|
|
5387
|
+
`${baseRate.unknown.length} I have nothing on`,
|
|
5388
|
+
].join(", ");
|
|
5389
|
+
const named = [...baseRate.positive, ...baseRate.negative]
|
|
5390
|
+
.slice(0, CAPABILITY_REPORT_CAP)
|
|
5391
|
+
.map((s) => renderFactLine(s.fact));
|
|
5392
|
+
return {
|
|
5393
|
+
text: `${opener} ${lead}, and of the ${baseRate.kinds} kind${baseRate.kinds === 1 ? "" : "s"} of ${baseRate.klass} I know, ${split}.\n${named.join("\n")}`,
|
|
5394
|
+
replace: true,
|
|
5395
|
+
miss: true,
|
|
5396
|
+
};
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
const extension = capabilityExtension(obj, facts, { exclude: new Set([...subj, baseRate.klass]) });
|
|
5400
|
+
if (extension.length) {
|
|
5401
|
+
const shown = extension.slice(0, CAPABILITY_REPORT_CAP);
|
|
5402
|
+
const rest = extension.slice(shown.length);
|
|
5403
|
+
return {
|
|
5404
|
+
text: `${opener} ${lead}, and nothing I know about ${baseRate.klass} says whether one can ${objectText}. I do know ${extension.length} thing${extension.length === 1 ? "" : "s"} that can ${objectText}${rest.length ? ` (first ${shown.length} shown)` : ""}:\n${shown.map(renderFactLine).join("\n")}`,
|
|
5405
|
+
replace: true,
|
|
5406
|
+
miss: true,
|
|
5407
|
+
...(rest.length ? { pending: { items: rest.map(renderFactLine), noun: "facts" } } : {}),
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
return {
|
|
5412
|
+
text: `${opener} ${lead}, but nothing I remember says whether any kind of ${baseRate.klass} can ${objectText}.`,
|
|
5413
|
+
replace: true,
|
|
5414
|
+
miss: true,
|
|
5415
|
+
};
|
|
5416
|
+
}
|
|
5417
|
+
|
|
4939
5418
|
/** SUPERLATIVE over TAUGHT COMPARATIVES — "which disk is smallest" / "what is
|
|
4940
5419
|
* the smallest disk" answered from the mgx:<comparative>-than facts the
|
|
4941
5420
|
* comparative teach frame mints ("disk-1 is smaller than disk-2"). The
|
|
@@ -5012,7 +5491,7 @@ const FORWARD_YESNO_MARKERS = Object.entries(FACT_PREDICATE_PHRASES)
|
|
|
5012
5491
|
re = new RegExp(`^(?:is|are)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${rest}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
|
|
5013
5492
|
} else {
|
|
5014
5493
|
const [head, ...tail] = phrase.split(" ");
|
|
5015
|
-
const base = [head
|
|
5494
|
+
const base = [baseVerbSurface(head), ...tail].map(escapeRegex).join("\\s+");
|
|
5016
5495
|
re = new RegExp(`^(?:does|do)\\s+(?:an?\\s+|the\\s+)?(.+?)\\s+${base}\\s+(?:an?\\s+|the\\s+)?(.+?)[?.!\\s]*$`, "i");
|
|
5017
5496
|
}
|
|
5018
5497
|
return { predicate, phrase, re };
|
|
@@ -5053,7 +5532,7 @@ function uniqueFacts(rows) {
|
|
|
5053
5532
|
* graph's Facts. Returns { text, replace } — `replace:false` means the engine's
|
|
5054
5533
|
* own (schema-docs) answer stands and the fact lines are appended under it —
|
|
5055
5534
|
* or null when memory holds nothing relevant (misses stay unchanged).
|
|
5056
|
-
* Exported so src/memory-ask-browser-entry.mjs
|
|
5535
|
+
* Exported so src/surfaces/web/memory-ask-browser-entry.mjs
|
|
5057
5536
|
* can re-export it for `tmct viz`'s embedded "Ask the graph" panel — the ONLY
|
|
5058
5537
|
* reason this is `export` rather than module-private; the function's own
|
|
5059
5538
|
* behavior is unchanged (same signature, same logic, answers identically in
|
|
@@ -5068,8 +5547,8 @@ function uniqueFacts(rows) {
|
|
|
5068
5547
|
* deducible (withDeducedGoal, below) — the ledger page's chat dock renders
|
|
5069
5548
|
* it as its own "Goal (inferred)" line; every other consumer reads named
|
|
5070
5549
|
* fields and is unaffected. */
|
|
5071
|
-
export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null) {
|
|
5072
|
-
return withDeducedGoal(await factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle, cache), envelope, query);
|
|
5550
|
+
export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null, focusLabel = null) {
|
|
5551
|
+
return withDeducedGoal(await factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle, cache, focusLabel), envelope, query);
|
|
5073
5552
|
}
|
|
5074
5553
|
|
|
5075
5554
|
/** Attach the additive `goal` field to a fact reader's return: the same
|
|
@@ -5097,9 +5576,9 @@ function withDeducedGoal(res, envelope, query) {
|
|
|
5097
5576
|
return goal ? { ...res, goal } : res;
|
|
5098
5577
|
}
|
|
5099
5578
|
|
|
5100
|
-
async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null) {
|
|
5579
|
+
async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle = {}, cache = null, focusLabel = null) {
|
|
5101
5580
|
let normFactTerm;
|
|
5102
|
-
try { ({ normFactTerm } = await import("
|
|
5581
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
5103
5582
|
const q = String(query).trim();
|
|
5104
5583
|
|
|
5105
5584
|
// (a-pre) "what is used for riding" / "what can be used for riding" / "what
|
|
@@ -5240,6 +5719,31 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5240
5719
|
}
|
|
5241
5720
|
}
|
|
5242
5721
|
|
|
5722
|
+
// (a-pre5) "what is on peg-a" over the SAME taught locative facts as
|
|
5723
|
+
// (a-pre4), asked by OBJECT instead of by subject. The general-verb reverse
|
|
5724
|
+
// reader ("what rests on peg-a") can't take this shape: it needs a surface
|
|
5725
|
+
// verb to mint a predicate from, and the bare copula has none. So the
|
|
5726
|
+
// captured preposition anchors the lookup instead — see
|
|
5727
|
+
// WHAT_IS_PREP_FACT_RE. Hit-gated the same way every reader in this cascade
|
|
5728
|
+
// is: it returns only when a locative row with that exact preposition and
|
|
5729
|
+
// object exists, so a plain vocabulary question keeps its own answer.
|
|
5730
|
+
const whatIsPrepQ = q.match(WHAT_IS_PREP_FACT_RE);
|
|
5731
|
+
if (whatIsPrepQ) {
|
|
5732
|
+
const prep = whatIsPrepQ[1].toLowerCase();
|
|
5733
|
+
const variants = factTermVariants(normFactTerm, whatIsPrepQ[2].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
5734
|
+
const hits = (await factRows(memoryDir, cache)).filter(
|
|
5735
|
+
(f) => LOCATIVE_FACT_PREDICATE_RE.test(f.predicate) && f.predicate.endsWith(`-${prep}`) && variants.has(f.object),
|
|
5736
|
+
);
|
|
5737
|
+
if (hits.length) {
|
|
5738
|
+
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
5739
|
+
const lines = ranked.map(renderFactLine);
|
|
5740
|
+
const shown = lines.slice(0, FACT_ANSWER_CAP);
|
|
5741
|
+
const rest = lines.slice(FACT_ANSWER_CAP);
|
|
5742
|
+
const extra = rest.length ? `\n…and ${rest.length} more — say 'more' to see them.` : "";
|
|
5743
|
+
return { text: shown.join("\n") + extra, replace: true, ...(rest.length ? { pending: { items: rest, noun: "facts" } } : {}) };
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
|
|
5243
5747
|
// (a) meta-shaped questions ("what is a module", "what does cache mean") — the
|
|
5244
5748
|
// parsed object term, matched against fact SUBJECTS; consulted for hits (append
|
|
5245
5749
|
// alongside the schema-docs answer) and misses (facts answer alone) alike.
|
|
@@ -5329,14 +5833,25 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5329
5833
|
const compWord = compAsk[2].toLowerCase().replace(/\s+/g, "-");
|
|
5330
5834
|
const compPredicate = `mgx:${compWord}-than`;
|
|
5331
5835
|
const facts = await memoryFacts(memoryDir);
|
|
5332
|
-
|
|
5333
|
-
|
|
5836
|
+
// Either side may be a context pronoun ("is it bigger than peg-a", "is
|
|
5837
|
+
// peg-a bigger than that"), resolved against the standing focus the same
|
|
5838
|
+
// way the property and relation lanes below resolve theirs. With no focus
|
|
5839
|
+
// to bind to, the pronoun stays literal and the honest can't-confirm below
|
|
5840
|
+
// stands — the lane never picks a subject the session hasn't named.
|
|
5841
|
+
const compTerm = (raw) => {
|
|
5842
|
+
const t = raw.replace(/^(?:an?|the)\s+/i, "").trim();
|
|
5843
|
+
return focusLabel && IS_ADJECTIVE_PRONOUN_RE.test(t) ? focusLabel : t;
|
|
5844
|
+
};
|
|
5845
|
+
const subjTerm = compTerm(compAsk[1]);
|
|
5846
|
+
const objTerm = compTerm(compAsk[3]);
|
|
5847
|
+
const subj = factTermVariants(normFactTerm, subjTerm);
|
|
5848
|
+
const obj = factTermVariants(normFactTerm, objTerm);
|
|
5334
5849
|
const hit = facts.find((f) => f.predicate === compPredicate && subj.has(f.subject) && obj.has(f.object));
|
|
5335
5850
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
5336
5851
|
const known = facts.filter((f) => f.predicate === compPredicate && (subj.has(f.subject) || subj.has(f.object)));
|
|
5337
5852
|
const shown = known.length ? ` I do know: ${known.slice(0, 3).map(renderFactLine).join("; ")}.` : "";
|
|
5338
5853
|
return {
|
|
5339
|
-
text: `I can't confirm that — nothing I remember compares them that way.${shown} If it's true, teach me: "${
|
|
5854
|
+
text: `I can't confirm that — nothing I remember compares them that way.${shown} If it's true, teach me: "${subjTerm} is ${compAsk[2].toLowerCase()} than ${objTerm}".`,
|
|
5340
5855
|
replace: true,
|
|
5341
5856
|
miss: true,
|
|
5342
5857
|
};
|
|
@@ -5391,23 +5906,22 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5391
5906
|
return null; // no remembered fact — the honest miss stands (never a guessed "no")
|
|
5392
5907
|
}
|
|
5393
5908
|
|
|
5394
|
-
// (b2) "can a dog bark" —
|
|
5395
|
-
//
|
|
5396
|
-
//
|
|
5397
|
-
|
|
5909
|
+
// (b2) "can a dog bark" — the polarity of a capability, resolved through the
|
|
5910
|
+
// ONE resolver every capability reader in this file shares (see
|
|
5911
|
+
// capabilityReply). Mirrors the ISA_ASK_RE block just above on the "never a
|
|
5912
|
+
// guessed no" discipline: a "no" here is a REMEMBERED negative, never the
|
|
5913
|
+
// absence of a positive.
|
|
5914
|
+
const can = positiveQuestionSurface(q).match(CAN_ASK_RE);
|
|
5398
5915
|
if (can) {
|
|
5399
|
-
const facts = await
|
|
5400
|
-
const
|
|
5401
|
-
|
|
5402
|
-
const hit = facts.find(
|
|
5403
|
-
(f) => f.predicate === "mgx:capableOf" && subj.has(f.subject) && obj.has(f.object),
|
|
5404
|
-
);
|
|
5405
|
-
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
5916
|
+
const facts = await factRows(memoryDir, cache);
|
|
5917
|
+
const reply = capabilityReply(can[1], can[2], facts);
|
|
5918
|
+
if (reply) return reply;
|
|
5406
5919
|
// A KNOWN subject with capability facts, none matching: an honest,
|
|
5407
5920
|
// specific miss citing what it CAN do — the same closer the is-a ladder
|
|
5408
5921
|
// answers with, instead of the misleading structural parse wall. An
|
|
5409
5922
|
// unknown subject still declines. Never a guessed "no": absence of a
|
|
5410
5923
|
// capableOf fact proves nothing.
|
|
5924
|
+
const subj = factTermVariants(normFactTerm, can[1]);
|
|
5411
5925
|
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
5412
5926
|
if (knownCan.length) {
|
|
5413
5927
|
const shown = knownCan.slice(0, 3).map(renderFactLine).join("; ");
|
|
@@ -5420,7 +5934,9 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5420
5934
|
miss: true,
|
|
5421
5935
|
};
|
|
5422
5936
|
}
|
|
5423
|
-
|
|
5937
|
+
// nothing about the subject at all — report the class base rate, and answer
|
|
5938
|
+
// neither yes nor no
|
|
5939
|
+
return capabilityBaseRateReply(can[1], can[2], facts);
|
|
5424
5940
|
}
|
|
5425
5941
|
|
|
5426
5942
|
// (b2b) "does a dog have a tail" — yes iff a remembered mgx:hasA fact says
|
|
@@ -5446,22 +5962,22 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5446
5962
|
// (falls through instead): the shape is looser than (b2)'s, so a do-lead
|
|
5447
5963
|
// question some later reader owns must keep its turn. The can't-confirm
|
|
5448
5964
|
// branch is additionally miss-gated for the same reason.
|
|
5449
|
-
const doAsk = q.match(DO_VERB_ASK_RE);
|
|
5965
|
+
const doAsk = positiveQuestionSurface(q).match(DO_VERB_ASK_RE);
|
|
5450
5966
|
if (doAsk) {
|
|
5451
|
-
const facts = await
|
|
5967
|
+
const facts = await factRows(memoryDir, cache);
|
|
5452
5968
|
const universal = !!doAsk[1];
|
|
5453
5969
|
const subj = factTermVariants(normFactTerm, doAsk[2]);
|
|
5454
5970
|
const obj = factTermVariants(normFactTerm, doAsk[3]);
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
);
|
|
5458
|
-
if (
|
|
5971
|
+
// the SAME resolver (b2) answers through, so "do penguins fly" and "can a
|
|
5972
|
+
// penguin fly" can never disagree in one session
|
|
5973
|
+
const reply = capabilityReply(doAsk[2], doAsk[3], facts);
|
|
5974
|
+
if (reply && universal) {
|
|
5459
5975
|
return {
|
|
5460
|
-
text: `I can't speak for all ${doAsk[2]} — what I remember is generic, not universal.
|
|
5976
|
+
text: `I can't speak for all ${doAsk[2]} — what I remember is generic, not universal. ${reply.text}.`,
|
|
5461
5977
|
replace: true,
|
|
5462
5978
|
};
|
|
5463
5979
|
}
|
|
5464
|
-
if (
|
|
5980
|
+
if (reply) return reply;
|
|
5465
5981
|
if (miss) {
|
|
5466
5982
|
const knownCan = facts.filter((f) => f.predicate === "mgx:capableOf" && subj.has(f.subject));
|
|
5467
5983
|
if (knownCan.length) {
|
|
@@ -5472,6 +5988,8 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5472
5988
|
miss: true,
|
|
5473
5989
|
};
|
|
5474
5990
|
}
|
|
5991
|
+
const base = capabilityBaseRateReply(doAsk[2], doAsk[3], facts);
|
|
5992
|
+
if (base) return base;
|
|
5475
5993
|
}
|
|
5476
5994
|
}
|
|
5477
5995
|
|
|
@@ -5481,7 +5999,12 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5481
5999
|
const canDo = q.match(WHAT_CAN_DO_RE);
|
|
5482
6000
|
if (canDo) {
|
|
5483
6001
|
const variants = factTermVariants(normFactTerm, canDo[1]);
|
|
5484
|
-
|
|
6002
|
+
// BOTH polarities. Filtering to the positive would silently omit what the
|
|
6003
|
+
// store explicitly says the subject CANNOT do, which reads as "I don't
|
|
6004
|
+
// know" for something it knows outright. renderFactLine spells the polarity
|
|
6005
|
+
// ("a penguin cannot fly"), so the two never blur together in the list.
|
|
6006
|
+
const hits = (await factRows(memoryDir, cache))
|
|
6007
|
+
.filter((f) => (f.predicate === "mgx:capableOf" || f.predicate === NEG_CAPABLE_OF_PREDICATE) && variants.has(f.subject));
|
|
5485
6008
|
if (!hits.length) return null;
|
|
5486
6009
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
5487
6010
|
const lines = ranked.map(renderFactLine);
|
|
@@ -5507,9 +6030,16 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5507
6030
|
const kindVariants = factTermVariants(normFactTerm, whichCan[1]);
|
|
5508
6031
|
const verbVariants = factTermVariants(normFactTerm, whichCan[2]);
|
|
5509
6032
|
const facts = await factRows(memoryDir, cache);
|
|
5510
|
-
|
|
6033
|
+
// A subject the store explicitly says CANNOT do this is not an answer to
|
|
6034
|
+
// "which birds can fly", even when a corpus row also says it can: the
|
|
6035
|
+
// direct negative is the more specific claim, and listing penguin here
|
|
6036
|
+
// while "can a penguin fly" answers "no" would be the same session
|
|
6037
|
+
// contradicting itself. The resolver decides, so the two agree by
|
|
6038
|
+
// construction.
|
|
6039
|
+
const capable = uniqueFacts(facts.filter((f) => f.predicate === "mgx:capableOf" && verbVariants.has(f.object)))
|
|
6040
|
+
.filter((f) => resolveCapabilityPolarity(new Set([f.subject]), verbVariants, facts).verdict === "yes");
|
|
5511
6041
|
if (capable.length) {
|
|
5512
|
-
const { findIsaChain, SUBCLASS_PREDICATE: SC_PRED, TYPE_PREDICATE: TYPE_PRED } = await import("
|
|
6042
|
+
const { findIsaChain, SUBCLASS_PREDICATE: SC_PRED, TYPE_PREDICATE: TYPE_PRED } = await import("../domain/syllogise.mjs");
|
|
5513
6043
|
const subClassRows = facts.filter((f) => f.predicate === SC_PRED);
|
|
5514
6044
|
const typeRows = facts.filter((f) => f.predicate === TYPE_PRED);
|
|
5515
6045
|
const subClassEdges = subClassRows.map((f) => [f.subject, f.object]);
|
|
@@ -5549,7 +6079,9 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5549
6079
|
const canVerb = q.match(WHAT_CAN_VERB_RE);
|
|
5550
6080
|
if (canVerb && canVerb[1].trim().split(/\s+/).at(-1)?.toLowerCase() !== "do") {
|
|
5551
6081
|
const verbVariants = factTermVariants(normFactTerm, canVerb[1]);
|
|
5552
|
-
|
|
6082
|
+
// same polarity discipline as (b3b): a subject with a direct negative is
|
|
6083
|
+
// not an answer to "what can fly"
|
|
6084
|
+
const hits = capabilityExtension(verbVariants, await factRows(memoryDir, cache));
|
|
5553
6085
|
if (hits.length) {
|
|
5554
6086
|
const ranked = rankByBiasThenTrust(uniqueFacts(hits), biasByBundle);
|
|
5555
6087
|
const lines = ranked.map(renderFactLine);
|
|
@@ -5705,7 +6237,7 @@ async function factAnswerReaders(memoryDir, query, envelope, miss, biasByBundle
|
|
|
5705
6237
|
// above. A hit REFUSES the whole answer (every belief about a
|
|
5706
6238
|
// contradictory subject is suspect, not just the clashing pair) rather
|
|
5707
6239
|
// than silently answering from a memory that's already inconsistent.
|
|
5708
|
-
const { findConsistencyViolations, TYPE_PREDICATE: CONS_TYPE_PREDICATE, SUBCLASS_PREDICATE: CONS_SC_PREDICATE, DISJOINT_PREDICATE: CONS_DISJOINT_PREDICATE } = await import("
|
|
6240
|
+
const { findConsistencyViolations, TYPE_PREDICATE: CONS_TYPE_PREDICATE, SUBCLASS_PREDICATE: CONS_SC_PREDICATE, DISJOINT_PREDICATE: CONS_DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
5709
6241
|
const consIsTaught = (f) => !f.provenance?.includes("corpus:") && !f.provenance?.includes("web:");
|
|
5710
6242
|
const consTypeEdges = rows.filter((f) => f.predicate === CONS_TYPE_PREDICATE && consIsTaught(f)).map((f) => [f.subject, f.object]);
|
|
5711
6243
|
const consSubClassEdges = rows.filter((f) => f.predicate === CONS_SC_PREDICATE && consIsTaught(f)).map((f) => [f.subject, f.object]);
|
|
@@ -5793,7 +6325,7 @@ async function whatElseAnswer(memoryDir, query, last) {
|
|
|
5793
6325
|
const term = m[1].trim();
|
|
5794
6326
|
if (!term) return null;
|
|
5795
6327
|
let normFactTerm;
|
|
5796
|
-
try { ({ normFactTerm } = await import("
|
|
6328
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
5797
6329
|
const variants = factTermVariants(normFactTerm, term);
|
|
5798
6330
|
const hits = (await memoryFacts(memoryDir)).filter((f) => variants.has(f.subject));
|
|
5799
6331
|
const picture = pickPhrase("full-picture", term.toLowerCase(), "the full picture");
|
|
@@ -5834,7 +6366,7 @@ async function synonymFactAnswer(memoryDir, query, envelope) {
|
|
|
5834
6366
|
const term = metaTermOf(query, envelope);
|
|
5835
6367
|
if (!term) return null;
|
|
5836
6368
|
let normFactTerm;
|
|
5837
|
-
try { ({ normFactTerm } = await import("
|
|
6369
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
5838
6370
|
const facts = await memoryFacts(memoryDir);
|
|
5839
6371
|
for (const { variant, source } of await synonymsOf(term)) {
|
|
5840
6372
|
const variants = factTermVariants(normFactTerm, variant);
|
|
@@ -5859,11 +6391,11 @@ const TOLD_ABOUT_RE = /^what\s+(?:did|have)\s+(?:i|we|you)\s+(?:told|tell|said|s
|
|
|
5859
6391
|
const KIND_OF_RE = /^what\s+kind\s+of\s+(?:thing|class|type|category|entity)?\s*(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
5860
6392
|
/** "does every <N1> have at least <m> <N2>" — cardinality monotonicity: a
|
|
5861
6393
|
* class's OWN declared exactly/min cardinality restriction proves "at least
|
|
5862
|
-
* m" for any queried m <= n (src/syllogise.mjs's proveCardinalityAtLeast). */
|
|
6394
|
+
* m" for any queried m <= n (src/domain/syllogise.mjs's proveCardinalityAtLeast). */
|
|
5863
6395
|
const CARD_AT_LEAST_ASK_RE = /^does\s+every\s+(.+?)\s+have\s+at\s+least\s+(\d+)\s+(.+?)[?.!\s]*$/i;
|
|
5864
6396
|
/** "does a/an <N1> have a/an <N2>" — a declared max-cardinality-0 restriction
|
|
5865
6397
|
* proves the class-level "no" directly
|
|
5866
|
-
* (src/syllogise.mjs's proveMaxCardinalityZeroDenial). Both readers FALL
|
|
6398
|
+
* (src/domain/syllogise.mjs's proveMaxCardinalityZeroDenial). Both readers FALL
|
|
5867
6399
|
* THROUGH ON A MISS (no unconditional decline, unlike isaAsk's own closing
|
|
5868
6400
|
* `return null`): "does SUBJ have OBJ" is broad enough to otherwise collide
|
|
5869
6401
|
* with GENERAL_VERB_YESNO_RE below and a few unclear max0 cases — a miss
|
|
@@ -6055,7 +6587,7 @@ export async function factReadBack(memoryDir, query, envelope, miss, graph = nul
|
|
|
6055
6587
|
async function factReadBackReaders(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
6056
6588
|
if (!miss) return null;
|
|
6057
6589
|
let normFactTerm;
|
|
6058
|
-
try { ({ normFactTerm } = await import("
|
|
6590
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
6059
6591
|
const q = String(query).trim();
|
|
6060
6592
|
// DIRECT STRUCTURAL CHECK: "is X a Y"
|
|
6061
6593
|
// naming a real code-graph inheritance edge needs NO taught fact at all — the
|
|
@@ -6217,7 +6749,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6217
6749
|
const object = relAsk[3].trim();
|
|
6218
6750
|
if (subject && !ISA_IDIOM_ROLE_WORDS.has(relationName)) {
|
|
6219
6751
|
const aliasTrees = buildAliasSubClassTrees(rows);
|
|
6220
|
-
const { findIsaChain: chaseAlias } = await import("
|
|
6752
|
+
const { findIsaChain: chaseAlias } = await import("../domain/syllogise.mjs");
|
|
6221
6753
|
// Shared alias-chase substrate (item 2): every stored Fact whose
|
|
6222
6754
|
// predicate resolves — directly, or via a taught (or, failing that,
|
|
6223
6755
|
// general-knowledge) rdfs:subClassOf chain over relation-NAME strings
|
|
@@ -6260,9 +6792,10 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6260
6792
|
// `relationFactsFor`/`renderFactLine`/`factPhrase`/`factTermVariants`/
|
|
6261
6793
|
// `byTrust`/`rows`/`HAS_PROPERTY_PREDICATE` are this block's own local
|
|
6262
6794
|
// closures/constants, threaded through explicitly.
|
|
6263
|
-
const { loadMemory, findRuleByName, resolveRelationChase } = await import("
|
|
6795
|
+
const { loadMemory, findRuleByName, resolveRelationChase } = await import("../adapters/memory/core.mjs");
|
|
6796
|
+
const { findActionPath } = await import("../domain/planning.mjs");
|
|
6264
6797
|
const memory = await loadMemory(memoryDir);
|
|
6265
|
-
const relationChaseHelpers = { relationFactsFor, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE };
|
|
6798
|
+
const relationChaseHelpers = { relationFactsFor, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE, findActionPath };
|
|
6266
6799
|
const hit = await resolveRelationChase(memory, relationName, subject, object, relationChaseHelpers);
|
|
6267
6800
|
if (hit) return { text: `yes — ${hit.citation.join("; ")}`, replace: true };
|
|
6268
6801
|
// A bare `return null` on any miss here would be wrong — the
|
|
@@ -6309,7 +6842,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6309
6842
|
const object = IS_ADJECTIVE_PRONOUN_RE.test(rawObject) ? (focusLabel || null) : rawObject;
|
|
6310
6843
|
if (object && !ISA_IDIOM_ROLE_WORDS.has(relationName)) {
|
|
6311
6844
|
const aliasTreesWho = buildAliasSubClassTrees(rows);
|
|
6312
|
-
const { findIsaChain: chaseAliasWho } = await import("
|
|
6845
|
+
const { findIsaChain: chaseAliasWho } = await import("../domain/syllogise.mjs");
|
|
6313
6846
|
// Same candidate-list shape as (a0)'s own relationFactsFor — every
|
|
6314
6847
|
// stored Fact whose predicate resolves, directly or via a taught (or
|
|
6315
6848
|
// general-knowledge) rdfs:subClassOf chain over relation-NAME
|
|
@@ -6330,7 +6863,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6330
6863
|
}
|
|
6331
6864
|
return out;
|
|
6332
6865
|
};
|
|
6333
|
-
const { loadMemory: loadMemWho, findRuleByName: findRuleByNameWho, resolveRelationChaseReverse } = await import("
|
|
6866
|
+
const { loadMemory: loadMemWho, findRuleByName: findRuleByNameWho, resolveRelationChaseReverse } = await import("../adapters/memory/core.mjs");
|
|
6334
6867
|
const memoryWho = await loadMemWho(memoryDir);
|
|
6335
6868
|
// Generic REVERSE relation-NAME resolver — the mirror image of (a0)'s
|
|
6336
6869
|
// resolveRelationChase: given a relation/rule name and a FIXED OBJECT,
|
|
@@ -6343,7 +6876,8 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6343
6876
|
// `relationFactsForWho`/`renderFactLine`/`factPhrase`/
|
|
6344
6877
|
// `factTermVariants`/`byTrust`/`rows`/`HAS_PROPERTY_PREDICATE` are this
|
|
6345
6878
|
// block's own local closures/constants, threaded through explicitly.
|
|
6346
|
-
const
|
|
6879
|
+
const { findReachableSet } = await import("../domain/planning.mjs");
|
|
6880
|
+
const relationChaseHelpersWho = { relationFactsFor: relationFactsForWho, renderFactLine, factPhrase, factTermVariants, byTrust, rows, HAS_PROPERTY_PREDICATE, findReachableSet };
|
|
6347
6881
|
const hits = await resolveRelationChaseReverse(memoryWho, relationName, object, relationChaseHelpersWho);
|
|
6348
6882
|
if (hits.length) {
|
|
6349
6883
|
const lines = hits.map((h) => `${h.subject} — ${h.citation.join("; ")}`);
|
|
@@ -6376,7 +6910,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6376
6910
|
// enumeration (every node ever reached), not single-target search.
|
|
6377
6911
|
// Dispatches to a `recursive`-kind taught Rule via the SAME "what kind of
|
|
6378
6912
|
// thing is this name" lookup (findRuleByName) the yes/no dispatcher uses,
|
|
6379
|
-
// then calls findReachableSet (src/planning.mjs) seeded from baseCase's taught edges for
|
|
6913
|
+
// then calls findReachableSet (src/domain/planning.mjs) seeded from baseCase's taught edges for
|
|
6380
6914
|
// the start entity, stepping via recStep's edges at every further hop.
|
|
6381
6915
|
// Renders each result with its own derivation path, mirroring the yes/no
|
|
6382
6916
|
// chain-citation style above (renderFactLine + interleaved alias-fact
|
|
@@ -6389,7 +6923,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6389
6923
|
if (subject) {
|
|
6390
6924
|
const {
|
|
6391
6925
|
loadMemory, findRuleByName, RULE_KIND_PROP: ruleKindProp, RULE_KIND_RECURSIVE: recKind,
|
|
6392
|
-
} = await import("
|
|
6926
|
+
} = await import("../adapters/memory/core.mjs");
|
|
6393
6927
|
const memory = await loadMemory(memoryDir);
|
|
6394
6928
|
const rule = findRuleByName(memory, ruleName);
|
|
6395
6929
|
const ruleKind = rule?.attributes?.find((a) => a.prop === ruleKindProp)?.value;
|
|
@@ -6399,7 +6933,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6399
6933
|
const startEntity = normFactTerm(subject);
|
|
6400
6934
|
if (baseCase && recStep && startEntity) {
|
|
6401
6935
|
const aliasTreesList = buildAliasSubClassTrees(rows);
|
|
6402
|
-
const { findIsaChain: chaseAlias } = await import("
|
|
6936
|
+
const { findIsaChain: chaseAlias } = await import("../domain/syllogise.mjs");
|
|
6403
6937
|
// Same alias-chase substrate the yes/no dispatcher's own
|
|
6404
6938
|
// relationFactsFor uses (re-derived here rather than shared across
|
|
6405
6939
|
// the two `if` blocks, which never run in the same call — one
|
|
@@ -6429,7 +6963,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6429
6963
|
// (this is also what makes a genuine cycle in the taught edges —
|
|
6430
6964
|
// e.g. two individuals mutually taught as each other's parent —
|
|
6431
6965
|
// terminate safely: the cyclic-back node is already `seen`).
|
|
6432
|
-
const { findReachableSet } = await import("
|
|
6966
|
+
const { findReachableSet } = await import("../domain/planning.mjs");
|
|
6433
6967
|
const applyActions = (state) => {
|
|
6434
6968
|
const relName = state.hop === 0 ? baseCase : recStep;
|
|
6435
6969
|
return relationFactsForList(relName)
|
|
@@ -6481,8 +7015,17 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6481
7015
|
// INSTANCE BRIDGE below would otherwise answer yes. Same stripTrailingDiscourseTag
|
|
6482
7016
|
// fix, applied here too.
|
|
6483
7017
|
const objVariants = factTermVariants(normFactTerm, stripTrailingDiscourseTag(isaAsk[2]));
|
|
6484
|
-
|
|
6485
|
-
|
|
7018
|
+
// "is that an animal" — the subject slot takes a context pronoun like every
|
|
7019
|
+
// other reader in this file, resolved against the session's standing focus
|
|
7020
|
+
// through IS_ADJECTIVE_PRONOUN_RE (the same set/swap the property, relation
|
|
7021
|
+
// and ownership lanes above already use). With no focus standing, the
|
|
7022
|
+
// pronoun stays literal and the lane keeps its existing decline — the miss
|
|
7023
|
+
// below reads it as a pronoun and suppresses the "I don't know it at all"
|
|
7024
|
+
// wording, which is still the right answer with nothing to bind to.
|
|
7025
|
+
const isaSubject = focusLabel && IS_ADJECTIVE_PRONOUN_RE.test(isaAsk[1].trim())
|
|
7026
|
+
? focusLabel : isaAsk[1];
|
|
7027
|
+
const subjCandidates = new Set(factTermVariants(normFactTerm, isaSubject));
|
|
7028
|
+
const noun = await entityClassNoun(graph, isaSubject);
|
|
6486
7029
|
if (noun) for (const v of factTermVariants(normFactTerm, noun)) subjCandidates.add(v);
|
|
6487
7030
|
const hit = isa
|
|
6488
7031
|
.filter((f) => subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
@@ -6493,7 +7036,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6493
7036
|
// "controller ⊑ handler" composes with a graph "TaskController inherits
|
|
6494
7037
|
// Controller" so "is TaskController a handler" answers yes, naming BOTH
|
|
6495
7038
|
// sources (the graph edge + the taught fact with its provenance).
|
|
6496
|
-
const ent = await resolveEntity(graph,
|
|
7039
|
+
const ent = await resolveEntity(graph, isaSubject);
|
|
6497
7040
|
if (ent) {
|
|
6498
7041
|
const bridgeSubjects = new Map(); // fact-term variant → the superclass label as spelled in the graph
|
|
6499
7042
|
for (const sup of inheritsChain(graph, ent.id)) {
|
|
@@ -6528,7 +7071,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6528
7071
|
// technically-true-per-ConceptNet "yes" that has nothing to do with
|
|
6529
7072
|
// what the OPERATOR taught; only operator/teach/entailed-sourced isa
|
|
6530
7073
|
// facts are chased, matching "TAUGHT" in the gap's own name.
|
|
6531
|
-
const { findIsaChain, SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE } = await import("
|
|
7074
|
+
const { findIsaChain, SUBCLASS_PREDICATE: SC_PREDICATE, TYPE_PREDICATE: RDF_TYPE_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
6532
7075
|
const isTaught = isOperatorTaught;
|
|
6533
7076
|
const chainSubClassRows = isa.filter((f) => f.predicate === SC_PREDICATE && isTaught(f));
|
|
6534
7077
|
const chainTypeRows = isa.filter((f) => f.predicate === RDF_TYPE_PREDICATE && isTaught(f));
|
|
@@ -6577,7 +7120,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6577
7120
|
// answer "no" from absence-of-membership rather than decline; anything
|
|
6578
7121
|
// this chase can't connect through a stated disjointness falls through
|
|
6579
7122
|
// to the honest miss below, never a guessed "no".
|
|
6580
|
-
const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("
|
|
7123
|
+
const { deriveDisjointViolations, DISJOINT_PREDICATE } = await import("../domain/syllogise.mjs");
|
|
6581
7124
|
const disjointRows = rows.filter((f) => f.predicate === DISJOINT_PREDICATE && isTaught(f));
|
|
6582
7125
|
// NEGATED membership — "is a dog not a cat". ISA_ASK_RE captures the
|
|
6583
7126
|
// subject as "dog not" (the "not" glues onto the subject because the
|
|
@@ -6590,7 +7133,14 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6590
7133
|
// shape. Deliberately shallow — no chain chases on the negated side; a
|
|
6591
7134
|
// negative proved through a multi-hop positive chain stays an honest
|
|
6592
7135
|
// miss rather than a guess.
|
|
6593
|
-
|
|
7136
|
+
// The pronoun swap above can't see this shape — the trailing "not" rides
|
|
7137
|
+
// inside the subject capture ("is that not a cat"), so the bare subject
|
|
7138
|
+
// resolves against the focus here instead, on the same terms.
|
|
7139
|
+
const negSubjectMatch = isaSubject.match(/^(.*\S)\s+not$/i);
|
|
7140
|
+
const negSubject = negSubjectMatch && [
|
|
7141
|
+
negSubjectMatch[0],
|
|
7142
|
+
focusLabel && IS_ADJECTIVE_PRONOUN_RE.test(negSubjectMatch[1].trim()) ? focusLabel : negSubjectMatch[1],
|
|
7143
|
+
];
|
|
6594
7144
|
if (negSubject) {
|
|
6595
7145
|
const negSubjVariants = factTermVariants(normFactTerm, negSubject[1]);
|
|
6596
7146
|
const negObjVariants = objVariants;
|
|
@@ -6645,7 +7195,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6645
7195
|
const {
|
|
6646
7196
|
deriveSomeValuesFromApplication, ON_PROPERTY_PREDICATE, SOME_VALUES_FROM_PREDICATE,
|
|
6647
7197
|
deriveSomeValuesFromSubsumption, ENTAILED_SCM_SVF_PROVENANCE, SCM_SVF_RULE_CONFIDENCE, entailedTrustFrom,
|
|
6648
|
-
} = await import("
|
|
7198
|
+
} = await import("../domain/syllogise.mjs");
|
|
6649
7199
|
const onPropertyRows = rows.filter((f) => f.predicate === ON_PROPERTY_PREDICATE && isTaught(f));
|
|
6650
7200
|
const someValuesFromRows = rows.filter((f) => f.predicate === SOME_VALUES_FROM_PREDICATE && isTaught(f));
|
|
6651
7201
|
if (onPropertyRows.length && someValuesFromRows.length) {
|
|
@@ -6674,7 +7224,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6674
7224
|
}
|
|
6675
7225
|
// LIVE scm-svf1 PROOF CHASE (W3C OWL 2 RL Table 9's scm-svf1 — distinct
|
|
6676
7226
|
// from scm-svf2, which needs rdfs:subPropertyOf, which the ACE grammar
|
|
6677
|
-
// can't teach at all — see src/syllogise.mjs's own header comment): every
|
|
7227
|
+
// can't teach at all — see src/domain/syllogise.mjs's own header comment): every
|
|
6678
7228
|
// strategy above missed — two INDEPENDENTLY taught someValuesFrom
|
|
6679
7229
|
// restrictions sharing the SAME property, whose filler classes are
|
|
6680
7230
|
// themselves ⊑-related, license a restriction-to-restriction ⊑ fact
|
|
@@ -6690,7 +7240,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6690
7240
|
const enlargedSubClassEdges = chainSubClassEdges.concat(svfSubsumption.map((d) => [d.subject, d.object]));
|
|
6691
7241
|
// The SAME `min(premiseTrusts) x
|
|
6692
7242
|
// ruleConfidence` discipline syllogise()'s own batch pass now applies
|
|
6693
|
-
// to scm-svf1 (src/syllogise.mjs), computed here for this LIVE,
|
|
7243
|
+
// to scm-svf1 (src/domain/syllogise.mjs), computed here for this LIVE,
|
|
6694
7244
|
// read-only chase — each restriction's own onProperty/someValuesFrom
|
|
6695
7245
|
// scaffolding trust plus the y1⊑y2 subClassOf premise that licensed
|
|
6696
7246
|
// the comparison (always present, mirroring syllogise()'s own
|
|
@@ -6699,8 +7249,8 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6699
7249
|
// syllogise()'s batch pass uses.
|
|
6700
7250
|
const restrictionByRid = new Map(restrictionEdges.map((r) => [r.restriction, r]));
|
|
6701
7251
|
const svfTrustByTriple = new Map();
|
|
6702
|
-
for (const f of rows) svfTrustByTriple.set(`${f.subject}
|
|
6703
|
-
const svfPremiseTrust = (s, p, o) => svfTrustByTriple.get(`${s}
|
|
7252
|
+
for (const f of rows) svfTrustByTriple.set(`${f.subject}\0${f.predicate}\0${f.object}`, f.trust);
|
|
7253
|
+
const svfPremiseTrust = (s, p, o) => svfTrustByTriple.get(`${s}\0${p}\0${o}`);
|
|
6704
7254
|
const svfTrustOf = new Map(); // "c1\0c2" -> computed trust, for the synthetic row below
|
|
6705
7255
|
for (const d of svfSubsumption) {
|
|
6706
7256
|
const r1 = restrictionByRid.get(d.subject);
|
|
@@ -6713,7 +7263,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6713
7263
|
svfPremiseTrust(d.viaY1, SC_PREDICATE, d.viaY2),
|
|
6714
7264
|
].filter((t) => typeof t === "number");
|
|
6715
7265
|
const t = entailedTrustFrom(premiseTrusts, SCM_SVF_RULE_CONFIDENCE);
|
|
6716
|
-
if (t !== null) svfTrustOf.set(`${d.subject}
|
|
7266
|
+
if (t !== null) svfTrustOf.set(`${d.subject}\0${d.object}`, t);
|
|
6717
7267
|
}
|
|
6718
7268
|
// A derived restriction⊑restriction edge has no underlying stored
|
|
6719
7269
|
// Fact row to cite (it's a schema-level conclusion, not a taught
|
|
@@ -6730,7 +7280,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6730
7280
|
return derived
|
|
6731
7281
|
? {
|
|
6732
7282
|
subject: derived.subject, predicate: SC_PREDICATE, object: derived.object, provenance: ENTAILED_SCM_SVF_PROVENANCE,
|
|
6733
|
-
trust: svfTrustOf.get(`${derived.subject}
|
|
7283
|
+
trust: svfTrustOf.get(`${derived.subject}\0${derived.object}`),
|
|
6734
7284
|
}
|
|
6735
7285
|
: undefined;
|
|
6736
7286
|
};
|
|
@@ -6762,14 +7312,30 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6762
7312
|
// with the graph entity's class noun (the CLASS↔INSTANCE bridge), and
|
|
6763
7313
|
// filtering on it here would cite facts about that noun ("class ⊑
|
|
6764
7314
|
// component") as if they were facts about the asked subject ("Widget").
|
|
6765
|
-
const directSubjVariants = factTermVariants(normFactTerm,
|
|
7315
|
+
const directSubjVariants = factTermVariants(normFactTerm, isaSubject);
|
|
6766
7316
|
const knownSubjectIsa = isa.filter((f) => directSubjVariants.has(f.subject)).sort(byTrust);
|
|
6767
|
-
const subjectWord =
|
|
7317
|
+
const subjectWord = isaSubject.trim();
|
|
6768
7318
|
const kindWord = stripTrailingDiscourseTag(isaAsk[2]).trim();
|
|
7319
|
+
// A REPORTING probe, never an answer: re-run the same rooted search at
|
|
7320
|
+
// findIsaChain's own default reach to learn whether a chain exists that
|
|
7321
|
+
// the live chases above simply don't walk. The answer stays a miss either
|
|
7322
|
+
// way — this only decides which recovery the miss can honestly name.
|
|
7323
|
+
//
|
|
7324
|
+
// Naming /syllogise unconditionally would be a lie whenever no such chain
|
|
7325
|
+
// exists, and telling someone to teach a fact that already follows from
|
|
7326
|
+
// what they taught is the mirror lie. The probe reads the SAME taught
|
|
7327
|
+
// edge lists the chases use, and /syllogise closes over a superset of
|
|
7328
|
+
// them, so a chain found here is one it can really materialize.
|
|
7329
|
+
const deeperChainExists = [...subjCandidates].some(
|
|
7330
|
+
(subj) => findIsaChain(subj, objVariants, chainTypeEdges, chainSubClassEdges, { maxHops: DEEP_CHAIN_PROBE_HOPS }),
|
|
7331
|
+
);
|
|
6769
7332
|
if (knownSubjectIsa.length) {
|
|
6770
7333
|
const shown = knownSubjectIsa.slice(0, 3).map(renderFactLine).join("; ");
|
|
7334
|
+
const recovery = deeperChainExists
|
|
7335
|
+
? `The facts to settle it are here, but the chain is longer than I follow while answering. Run "/syllogise ${subjectWord}", then ask me again.`
|
|
7336
|
+
: `If it's true, teach me: "${subjectWord} is a kind of ${kindWord}".`;
|
|
6771
7337
|
return {
|
|
6772
|
-
text: `I can't confirm that — nothing I remember says ${subjectWord} is a ${kindWord}. I do know: ${shown}.
|
|
7338
|
+
text: `I can't confirm that — nothing I remember says ${subjectWord} is a ${kindWord}. I do know: ${shown}. ${recovery}`,
|
|
6773
7339
|
replace: true,
|
|
6774
7340
|
miss: true, // still a MISS in the turn record — honest wording, not an answer
|
|
6775
7341
|
};
|
|
@@ -6791,7 +7357,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6791
7357
|
|
|
6792
7358
|
// (a1c-i) CARDINALITY MONOTONICITY — "does every X have at least N Y" over
|
|
6793
7359
|
// a TAUGHT exactly/min cardinality restriction (pattern-5,
|
|
6794
|
-
// src/grammar/ace.mjs's parseCardinality).
|
|
7360
|
+
// src/domain/grammar/ace.mjs's parseCardinality).
|
|
6795
7361
|
// FALLS THROUGH ON A MISS (see CARD_AT_LEAST_ASK_RE's own doc comment) —
|
|
6796
7362
|
// never an unconditional decline, unlike isaAsk's own closing `return null`.
|
|
6797
7363
|
const cardAtLeast = q.match(CARD_AT_LEAST_ASK_RE);
|
|
@@ -6800,7 +7366,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6800
7366
|
const {
|
|
6801
7367
|
SUBCLASS_PREDICATE: CARD_SC_PREDICATE, ON_PROPERTY_PREDICATE: CARD_ON_PROPERTY_PREDICATE,
|
|
6802
7368
|
buildCardinalityRestrictions, proveCardinalityAtLeast, CARDINALITY_RULE_CONFIDENCE, entailedTrustFrom,
|
|
6803
|
-
} = await import("
|
|
7369
|
+
} = await import("../domain/syllogise.mjs");
|
|
6804
7370
|
const isTaughtCard = isOperatorTaught;
|
|
6805
7371
|
const cardSubClassEdges = isa.filter((f) => f.predicate === CARD_SC_PREDICATE && isTaughtCard(f)).map((f) => [f.subject, f.object]);
|
|
6806
7372
|
const cardRows = rows.filter((f) => (f.predicate === CARD_ON_PROPERTY_PREDICATE || CARDINALITY_ROW_PREDICATES.has(f.predicate)) && isTaughtCard(f));
|
|
@@ -6816,7 +7382,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6816
7382
|
const kindWord = witness.kind === "exactly" ? "exactly" : "at least";
|
|
6817
7383
|
const plural = (w, n) => `${w}${n === 1 ? "" : "s"}`;
|
|
6818
7384
|
// Premise-derived trust for THIS
|
|
6819
|
-
// rule's answer (src/syllogise.mjs's CARDINALITY_RULE_CONFIDENCE doc
|
|
7385
|
+
// rule's answer (src/domain/syllogise.mjs's CARDINALITY_RULE_CONFIDENCE doc
|
|
6820
7386
|
// comment explains why there is no persisted Fact for it to attach
|
|
6821
7387
|
// to) — the restriction's OWN scaffolding rows (onProperty/kind/
|
|
6822
7388
|
// onClass, all keyed to witness.viaRestriction), the declaring
|
|
@@ -6850,7 +7416,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
6850
7416
|
const {
|
|
6851
7417
|
SUBCLASS_PREDICATE: CARD_SC_PREDICATE, ON_PROPERTY_PREDICATE: CARD_ON_PROPERTY_PREDICATE,
|
|
6852
7418
|
buildCardinalityRestrictions, proveMaxCardinalityZeroDenial, CAX_MAXC0_RULE_CONFIDENCE, entailedTrustFrom,
|
|
6853
|
-
} = await import("
|
|
7419
|
+
} = await import("../domain/syllogise.mjs");
|
|
6854
7420
|
const isTaughtCard = isOperatorTaught;
|
|
6855
7421
|
const cardSubClassEdges = isa.filter((f) => f.predicate === CARD_SC_PREDICATE && isTaughtCard(f)).map((f) => [f.subject, f.object]);
|
|
6856
7422
|
const cardRows = rows.filter((f) => (f.predicate === CARD_ON_PROPERTY_PREDICATE || CARDINALITY_ROW_PREDICATES.has(f.predicate)) && isTaughtCard(f));
|
|
@@ -7061,7 +7627,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7061
7627
|
// attempt, never a replacement: on no chain either, this falls through
|
|
7062
7628
|
// to the ordinary property-miss handling just below, unchanged.
|
|
7063
7629
|
{
|
|
7064
|
-
const { findIsaChain: chaseAdj, SUBCLASS_PREDICATE: SC_PREDICATE_ADJ, TYPE_PREDICATE: TYPE_PREDICATE_ADJ } = await import("
|
|
7630
|
+
const { findIsaChain: chaseAdj, SUBCLASS_PREDICATE: SC_PREDICATE_ADJ, TYPE_PREDICATE: TYPE_PREDICATE_ADJ } = await import("../domain/syllogise.mjs");
|
|
7065
7631
|
const isTaughtAdj = isOperatorTaught;
|
|
7066
7632
|
const chainSubClassRowsAdj = rows.filter((f) => f.predicate === SC_PREDICATE_ADJ && isTaughtAdj(f));
|
|
7067
7633
|
const chainTypeRowsAdj = rows.filter((f) => f.predicate === TYPE_PREDICATE_ADJ && isTaughtAdj(f));
|
|
@@ -7138,7 +7704,12 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7138
7704
|
// to the ordinary honest-miss cascade instead of fabricating a "no". Open
|
|
7139
7705
|
// form lists every stored fact row for {subject, predicate} regardless of
|
|
7140
7706
|
// object.
|
|
7141
|
-
|
|
7707
|
+
// The negation strips first, so "does fred not eat kale" and "doesn't fred
|
|
7708
|
+
// eat kale" reach the SAME predicate lookup as "does fred eat kale" and the
|
|
7709
|
+
// stored polarity — positive or negative — is what answers. The teach side
|
|
7710
|
+
// strips through splitTeachNegation over the same NEG_MARKER_SRC, so the two
|
|
7711
|
+
// sides can never disagree about what negates a sentence.
|
|
7712
|
+
const genYN = positiveQuestionSurface(q).match(GENERAL_VERB_YESNO_RE);
|
|
7142
7713
|
if (genYN && !GENERAL_VERB_ANYWHERE_EXCLUDE_RE.test(q)) {
|
|
7143
7714
|
const [, subjectRaw, verbRaw, objectRaw] = genYN;
|
|
7144
7715
|
const verb = verbRaw.toLowerCase();
|
|
@@ -7153,10 +7724,31 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7153
7724
|
const predicate = folded.predicate;
|
|
7154
7725
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
7155
7726
|
const objVariants = factTermVariants(normFactTerm, object);
|
|
7727
|
+
// BOTH polarities are looked up under one predicate pair: a stored
|
|
7728
|
+
// negative answers "no" as confidently as a positive answers "yes",
|
|
7729
|
+
// and neither is ever inferred from the other's absence.
|
|
7730
|
+
const polar = [predicate, negatedPredicate(predicate)];
|
|
7156
7731
|
const hit = rows
|
|
7157
|
-
.filter((f) => f.predicate
|
|
7732
|
+
.filter((f) => polar.includes(f.predicate) && subjVariants.has(f.subject) && objVariants.has(f.object))
|
|
7158
7733
|
.sort(byTrust)[0];
|
|
7159
|
-
if (hit)
|
|
7734
|
+
if (hit) {
|
|
7735
|
+
const verdict = isNegatedPredicate(hit.predicate) ? "no" : "yes";
|
|
7736
|
+
return { text: `${verdict} — ${renderFactLine(hit)}`, replace: true, generalVerbQuery: true };
|
|
7737
|
+
}
|
|
7738
|
+
// A KNOWN subject under the SAME relation, no row matching this
|
|
7739
|
+
// object: an honest, specific miss citing what the subject IS
|
|
7740
|
+
// remembered to relate to, instead of the generic structural wall.
|
|
7741
|
+
// Still never a guessed "no" — the text declines to confirm and says
|
|
7742
|
+
// what it does know, and `miss: true` keeps it out of recall.
|
|
7743
|
+
const sameRelation = rows.filter((f) => polar.includes(f.predicate) && subjVariants.has(f.subject));
|
|
7744
|
+
if (sameRelation.length) {
|
|
7745
|
+
const shown = sameRelation.slice(0, 3).map(renderFactLine).join("; ");
|
|
7746
|
+
return {
|
|
7747
|
+
text: `I can't confirm that — nothing I remember says ${factPhrase({ subject, predicate, object })}. I do know: ${shown}.`,
|
|
7748
|
+
replace: true,
|
|
7749
|
+
miss: true,
|
|
7750
|
+
};
|
|
7751
|
+
}
|
|
7160
7752
|
return null; // no remembered fact — the honest miss stands (never a guessed "no")
|
|
7161
7753
|
}
|
|
7162
7754
|
}
|
|
@@ -7174,7 +7766,11 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7174
7766
|
let predicate = await generalVerbPredicate(verb);
|
|
7175
7767
|
if (verbPrep && /^mgx:[a-z]+$/.test(predicate)) predicate = `${predicate}-${verbPrep}`;
|
|
7176
7768
|
const subjVariants = factTermVariants(normFactTerm, subject);
|
|
7177
|
-
|
|
7769
|
+
// both polarities: "what does fred eat" should surface a remembered
|
|
7770
|
+
// "fred does not eat kale" rather than miss on it — renderFactLine
|
|
7771
|
+
// spells the polarity out, so the list can't be misread
|
|
7772
|
+
const polar = [predicate, negatedPredicate(predicate)];
|
|
7773
|
+
const hits = rankByBiasThenTrust(rows.filter((f) => polar.includes(f.predicate) && subjVariants.has(f.subject)), biasByBundle);
|
|
7178
7774
|
if (hits.length) return { ...renderMany(hits), generalVerbQuery: true };
|
|
7179
7775
|
}
|
|
7180
7776
|
}
|
|
@@ -7264,7 +7860,7 @@ async function factReadBackReaders(memoryDir, query, envelope, miss, graph = nul
|
|
|
7264
7860
|
* nothing about this subject. */
|
|
7265
7861
|
async function describedFacts(memoryDir, label, biasByBundle = {}, cache = null) {
|
|
7266
7862
|
let normFactTerm;
|
|
7267
|
-
try { ({ normFactTerm } = await import("
|
|
7863
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
7268
7864
|
const rows = await factRows(memoryDir, cache);
|
|
7269
7865
|
if (!rows.length) return null;
|
|
7270
7866
|
const variants = factTermVariants(normFactTerm, label);
|
|
@@ -7295,8 +7891,8 @@ let corpusPromise = null; // the local slice as renderable rows, one load per pr
|
|
|
7295
7891
|
function localCorpus() {
|
|
7296
7892
|
if (!corpusPromise) {
|
|
7297
7893
|
corpusPromise = (async () => {
|
|
7298
|
-
const { loadSlice, loadMap, termText } = await import("
|
|
7299
|
-
const { normFactTerm } = await import("
|
|
7894
|
+
const { loadSlice, loadMap, termText } = await import("../adapters/corpus/conceptnet.mjs");
|
|
7895
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
7300
7896
|
const [assertions, map] = await Promise.all([loadSlice(), loadMap()]);
|
|
7301
7897
|
const rows = [];
|
|
7302
7898
|
for (const a of assertions) {
|
|
@@ -7320,7 +7916,7 @@ function localCorpus() {
|
|
|
7320
7916
|
* where the tier-3 network lookup would attach — see CORPUS_LOOKUP_FLAG). */
|
|
7321
7917
|
async function corpusAside(term) {
|
|
7322
7918
|
try {
|
|
7323
|
-
const { normFactTerm } = await import("
|
|
7919
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
7324
7920
|
const variants = factTermVariants(normFactTerm, term);
|
|
7325
7921
|
const rows = (await localCorpus()).filter((r) => variants.has(r.key));
|
|
7326
7922
|
if (!rows.length) return null;
|
|
@@ -7339,7 +7935,8 @@ const RECALL_ASK_RE = /^what (?:did|have) (?:i|we) (?:ask(?:ed)?(?: you)?|talk(?
|
|
|
7339
7935
|
* uuidv7s, so a plain sort is chronological). Null when nothing is folded yet. */
|
|
7340
7936
|
async function recallSummary(memoryDir) {
|
|
7341
7937
|
try {
|
|
7342
|
-
const { loadBlockIndex, BLOCKS_DIR_REL } = await import("
|
|
7938
|
+
const { loadBlockIndex, BLOCKS_DIR_REL } = await import("../adapters/memory/blocks.mjs");
|
|
7939
|
+
const { readFile } = await import("node:fs/promises");
|
|
7343
7940
|
const index = await loadBlockIndex(memoryDir);
|
|
7344
7941
|
const id = Object.keys(index.blocks).sort().at(-1);
|
|
7345
7942
|
if (!id) return null;
|
|
@@ -7427,6 +8024,28 @@ function discourseRewrite(query, last) {
|
|
|
7427
8024
|
// that happens to contain "it"/"this"/"that" as a substring (whole-word
|
|
7428
8025
|
// boundaries only).
|
|
7429
8026
|
if (PRONOUN_IN_QUERY_RE.test(prevQ)) return prevQ.replace(PRONOUN_IN_QUERY_RE, () => newSubj);
|
|
8027
|
+
// TOPIC SHIFT after a plain vocabulary question: "what is a dog" -> "what
|
|
8028
|
+
// about cats" means "what is a cat". Such a prior query has neither a
|
|
8029
|
+
// NAME_TOKEN nor a pronoun for the two rules above to swap, so both decline
|
|
8030
|
+
// and the turn used to reach the wall.
|
|
8031
|
+
//
|
|
8032
|
+
// The gate is the PRIOR turn's own shape (BARE_WHATIS_RE — a plain "what
|
|
8033
|
+
// is/are X"), never a looser reading of the new term. Widening NAME_TOKEN_RE
|
|
8034
|
+
// to cover ordinary words would look like the same fix and is not: it would
|
|
8035
|
+
// let "what about cats" rewrite "which modules import Widget" by swapping
|
|
8036
|
+
// "modules", answering a question nobody asked.
|
|
8037
|
+
//
|
|
8038
|
+
// vagueTouchTermOf owns the "what about X" surface already, so the term
|
|
8039
|
+
// comes from there rather than a second parse — it strips the article that
|
|
8040
|
+
// WHAT_ABOUT_RE's own capture keeps ("what about a cat" -> "cat", not "a
|
|
8041
|
+
// cat"). It reads the "what about"/"tell me about"/"explain" surfaces only,
|
|
8042
|
+
// so the staccato swap ("and Widget") declines here and keeps the behaviour
|
|
8043
|
+
// it has today. singularizeSurface matches the stored singular; facts are
|
|
8044
|
+
// stored one way and "cats" would find nothing.
|
|
8045
|
+
if (BARE_WHATIS_RE.test(prevQ)) {
|
|
8046
|
+
const term = vagueTouchTermOf(query);
|
|
8047
|
+
if (term) return `what is a ${singularizeSurface(term)}`;
|
|
8048
|
+
}
|
|
7430
8049
|
return null;
|
|
7431
8050
|
}
|
|
7432
8051
|
|
|
@@ -7482,6 +8101,35 @@ function existentialAnythingRewrite(query) {
|
|
|
7482
8101
|
return m ? `what ${m[1].trim()}` : null;
|
|
7483
8102
|
}
|
|
7484
8103
|
|
|
8104
|
+
/** REVERSE CLEFT "what/who is it that <verb-phrase>" -> "what/who <verb-phrase>",
|
|
8105
|
+
* the closed sibling of EXISTENTIAL_ANYTHING_RE just above and the same trade:
|
|
8106
|
+
* a textual rewrite onto the ALREADY-CORRECT "what <verb> X" shape, no new
|
|
8107
|
+
* capability.
|
|
8108
|
+
*
|
|
8109
|
+
* The "it that" here is pure scaffolding. A reverse cleft names no contrasted
|
|
8110
|
+
* element — "what is it that calls loadStore" asks exactly what "what calls
|
|
8111
|
+
* loadStore" asks, so dropping the frame loses nothing. Without the rewrite
|
|
8112
|
+
* parseKeywordSpot finds the verb, splits the text around it, and the leftover
|
|
8113
|
+
* "it that" survives the STOPWORDS filter (which carries "what"/"is" but not
|
|
8114
|
+
* "it"/"that") to become the subject — so the turn asks about an entity named
|
|
8115
|
+
* "it that" and misses.
|
|
8116
|
+
*
|
|
8117
|
+
* The FORWARD cleft "is it X that calls Y" is deliberately left alone. It DOES
|
|
8118
|
+
* name a contrasted element ("it is X, not something else"), it already answers
|
|
8119
|
+
* correctly, and it discriminates: "is it createTask that calls saveStore" ->
|
|
8120
|
+
* yes, "is it loadStore that calls saveStore" -> no. Flattening that shape
|
|
8121
|
+
* would throw the contrast away for nothing.
|
|
8122
|
+
*
|
|
8123
|
+
* The "that <verb-phrase>" tail is mandatory, exactly as it is for
|
|
8124
|
+
* EXISTENTIAL_ANYTHING_RE. A bare "what is it" has no tail and keeps its own
|
|
8125
|
+
* path, and "what time is it" never opens with "what is it" at all, so the
|
|
8126
|
+
* personal-assistant decline is untouched. */
|
|
8127
|
+
const REVERSE_CLEFT_RE = /^(what|who)\s+(?:is|was)\s+it\s+that\s+(.+?)\s*\??$/i;
|
|
8128
|
+
function reverseCleftRewrite(query) {
|
|
8129
|
+
const m = REVERSE_CLEFT_RE.exec(String(query || "").trim());
|
|
8130
|
+
return m ? `${m[1].toLowerCase()} ${m[2].trim()}` : null;
|
|
8131
|
+
}
|
|
8132
|
+
|
|
7485
8133
|
// ---- curated SEON definitions (corpus/seon/definitions.jsonl) ----
|
|
7486
8134
|
// A "what is a <term>" for a LEXICON term prefers the curated one-sentence
|
|
7487
8135
|
// definition, cited via:"corpus/seon" — but only when this repo carries the
|
|
@@ -7493,8 +8141,9 @@ let seonDefsPromise = null;
|
|
|
7493
8141
|
function seonDefinitions() {
|
|
7494
8142
|
if (!seonDefsPromise) {
|
|
7495
8143
|
seonDefsPromise = (async () => {
|
|
7496
|
-
const { SEON_DEFINITIONS_FILE } = await import("
|
|
7497
|
-
const { normFactTerm } = await import("
|
|
8144
|
+
const { SEON_DEFINITIONS_FILE } = await import("../adapters/corpus/conceptnet.mjs");
|
|
8145
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
8146
|
+
const { readFile } = await import("node:fs/promises");
|
|
7498
8147
|
const raw = await readFile(SEON_DEFINITIONS_FILE, "utf8");
|
|
7499
8148
|
const map = new Map();
|
|
7500
8149
|
for (const line of raw.split("\n")) {
|
|
@@ -7519,7 +8168,8 @@ let seonRelsPromise = null;
|
|
|
7519
8168
|
function relationDefinitions() {
|
|
7520
8169
|
if (!seonRelsPromise) {
|
|
7521
8170
|
seonRelsPromise = (async () => {
|
|
7522
|
-
const { SEON_DEFINITIONS_FILE } = await import("
|
|
8171
|
+
const { SEON_DEFINITIONS_FILE } = await import("../adapters/corpus/conceptnet.mjs");
|
|
8172
|
+
const { readFile } = await import("node:fs/promises");
|
|
7523
8173
|
const relFile = join(dirname(SEON_DEFINITIONS_FILE), "relations.jsonl");
|
|
7524
8174
|
const raw = await readFile(relFile, "utf8");
|
|
7525
8175
|
const map = new Map();
|
|
@@ -7542,7 +8192,7 @@ function relationDefinitions() {
|
|
|
7542
8192
|
* — NOT grammar.mjs's structural T5 template, which keeps its article MANDATORY
|
|
7543
8193
|
* on purpose (a bare "what is <anything>" would also swallow "what is the
|
|
7544
8194
|
* meaning of this codebase", an existing, deliberately honest grammar-miss
|
|
7545
|
-
* regression — test/ask.test.mjs pins it null; see T5's own docblock). That
|
|
8195
|
+
* regression — test/tools/ask.test.mjs pins it null; see T5's own docblock). That
|
|
7546
8196
|
* collision risk is a STRUCTURAL-PARSE concern (T5's tail becomes the literal
|
|
7547
8197
|
* graph-query object); it doesn't apply here: this regex only extracts a
|
|
7548
8198
|
* SUBJECT STRING to look up against the memory Facts store / curated lexicon —
|
|
@@ -7610,12 +8260,12 @@ async function curatedDefinitionAnswer(query, envelope, { memoryDir, lexicon })
|
|
|
7610
8260
|
const term = metaTermOf(query, envelope);
|
|
7611
8261
|
if (!term) return null;
|
|
7612
8262
|
let normFactTerm;
|
|
7613
|
-
try { ({ normFactTerm } = await import("
|
|
8263
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { return null; }
|
|
7614
8264
|
// lexicon-noun gate: the curated defs are keyed on SE lexicon terms only.
|
|
7615
8265
|
let lex = lexicon;
|
|
7616
8266
|
try {
|
|
7617
|
-
if (!lex) { const { loadLexicon } = await import("
|
|
7618
|
-
const { lookupNoun } = await import("
|
|
8267
|
+
if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
8268
|
+
const { lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
7619
8269
|
if (!lookupNoun(lex, term)) return null;
|
|
7620
8270
|
} catch { return null; }
|
|
7621
8271
|
const def = (await seonDefinitions()).get(normFactTerm(term));
|
|
@@ -7792,7 +8442,7 @@ async function describeGrainRescue(graph, term) {
|
|
|
7792
8442
|
const expectedClass = ENTITY_TO_TYPE[grainWord.toLowerCase()];
|
|
7793
8443
|
if (!head?.trim() || !expectedClass) return null;
|
|
7794
8444
|
try {
|
|
7795
|
-
const { resolveObject } = await import("
|
|
8445
|
+
const { resolveObject } = await import("../domain/ask.mjs");
|
|
7796
8446
|
const r = resolveObject(graph, head.trim(), { expectedClass });
|
|
7797
8447
|
if (r?.match?.id && !r.ambiguous) return { id: r.match.id, label: r.match.label };
|
|
7798
8448
|
} catch { /* tolerated */ }
|
|
@@ -7925,7 +8575,7 @@ async function compareAnswer(query, { graph, config, source }) {
|
|
|
7925
8575
|
|
|
7926
8576
|
/** DETAILED-SUMMARY / EXPLAIN-IN-DETAIL closed phrasings — "give me a detailed
|
|
7927
8577
|
* summary of how the task system works" / "explain in detail how X works" /
|
|
7928
|
-
* "give me a detailed overview of X", wired to src/completions/'s extractive
|
|
8578
|
+
* "give me a detailed overview of X", wired to src/domain/completions/'s extractive
|
|
7929
8579
|
* multi-sentence pipeline below. Two closed shapes (DETAILED_HOW_WORKS_RE
|
|
7930
8580
|
* tried first, more specific); distinct from DESCRIBE_WRAPPER_RE, which
|
|
7931
8581
|
* neither anchors on "give me"/"explain ... in detail". */
|
|
@@ -7935,9 +8585,9 @@ const DETAILED_HOW_WORKS_RE =
|
|
|
7935
8585
|
const DETAILED_OVERVIEW_RE =
|
|
7936
8586
|
/^(?:(?:can|could|would)\s+you\s+(?:please\s+)?|please\s+)?give\s+me\s+a\s+detailed\s+(?:overview|summary|explanation)\s+of\s+(.+?)\s*\??$/i;
|
|
7937
8587
|
|
|
7938
|
-
/** THE COMPLETIONS RESCUE — wires src/completions/'s extractive, cited,
|
|
8588
|
+
/** THE COMPLETIONS RESCUE — wires src/domain/completions/'s extractive, cited,
|
|
7939
8589
|
* groundedness-checked multi-sentence pipeline (generateCompletion(),
|
|
7940
|
-
* src/completions/complete.mjs) into live chat dispatch. Tried in runAsk
|
|
8590
|
+
* src/domain/completions/complete.mjs) into live chat dispatch. Tried in runAsk
|
|
7941
8591
|
* ONLY after (4d) DESCRIBE-WRAPPER RESCUE has already declined, and only for
|
|
7942
8592
|
* an EXPLICIT detailed/multi-sentence request (DETAILED_HOW_WORKS_RE /
|
|
7943
8593
|
* DETAILED_OVERVIEW_RE, above). Honest by construction: generateCompletion()
|
|
@@ -7958,13 +8608,13 @@ async function completionsRescueAnswer(query, { memoryDir, graph }) {
|
|
|
7958
8608
|
term = term.replace(/^(?:the|a|an)\s+/i, "").trim();
|
|
7959
8609
|
if (!term) return null;
|
|
7960
8610
|
try {
|
|
7961
|
-
const { generateCompletion } = await import("./completions
|
|
8611
|
+
const { generateCompletion } = await import("./completions.mjs");
|
|
7962
8612
|
// createCompletionsGraphAdapter wraps the SAME graph object this turn
|
|
7963
8613
|
// already has in scope plus this repo's already-loaded Fact store, so
|
|
7964
8614
|
// broadSearch can search live graph/memory content, not just saved
|
|
7965
8615
|
// memory blocks.
|
|
7966
|
-
const { createCompletionsGraphAdapter } = await import("./completions
|
|
7967
|
-
const { loadMemory } = await import("
|
|
8616
|
+
const { createCompletionsGraphAdapter } = await import("./completions.mjs");
|
|
8617
|
+
const { loadMemory } = await import("../adapters/memory/core.mjs");
|
|
7968
8618
|
const memory = await loadMemory(memoryDir);
|
|
7969
8619
|
const graphService = createCompletionsGraphAdapter(graph, memory);
|
|
7970
8620
|
const result = await generateCompletion(memoryDir, term, { query: term, graph, memory, graphService });
|
|
@@ -7986,7 +8636,7 @@ async function relationForceAnswer(query, envelope, { graph, config, source, tem
|
|
|
7986
8636
|
const rawTerm = relationTermOf(query, envelope);
|
|
7987
8637
|
if (!rawTerm) return null;
|
|
7988
8638
|
let composeRelation; let RELATION_TERM;
|
|
7989
|
-
try { ({ composeRelation, RELATION_TERM } = await import("
|
|
8639
|
+
try { ({ composeRelation, RELATION_TERM } = await import("../domain/concept.mjs")); }
|
|
7990
8640
|
catch { return null; }
|
|
7991
8641
|
const term = String(rawTerm).toLowerCase();
|
|
7992
8642
|
const kind = RELATION_TERM[term];
|
|
@@ -8025,8 +8675,8 @@ async function conceptForceAnswer(query, envelope, { graph, config, source, memo
|
|
|
8025
8675
|
if (!rawTerm) return null;
|
|
8026
8676
|
let normFactTerm; let composeConcept; let CONCEPT_CLASS;
|
|
8027
8677
|
try {
|
|
8028
|
-
({ normFactTerm } = await import("
|
|
8029
|
-
({ composeConcept, CONCEPT_CLASS } = await import("
|
|
8678
|
+
({ normFactTerm } = await import("../adapters/memory/core.mjs"));
|
|
8679
|
+
({ composeConcept, CONCEPT_CLASS } = await import("../domain/concept.mjs"));
|
|
8030
8680
|
} catch { return null; }
|
|
8031
8681
|
const term = normFactTerm(rawTerm);
|
|
8032
8682
|
if (!CONCEPT_CLASS[term]) return null; // not an enumerable code concept — ordinary path owns it
|
|
@@ -8080,7 +8730,7 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
8080
8730
|
if (seen.has(key)) continue;
|
|
8081
8731
|
seen.add(key);
|
|
8082
8732
|
try {
|
|
8083
|
-
const { resolveObject } = await import("
|
|
8733
|
+
const { resolveObject } = await import("../domain/ask.mjs");
|
|
8084
8734
|
const r = resolveObject(graph, tok, { expectedClass });
|
|
8085
8735
|
if (r?.match?.id && !r.ambiguous) return { id: r.match.id, label: r.match.label };
|
|
8086
8736
|
} catch { /* tolerated — falls through to the next token */ }
|
|
@@ -8098,8 +8748,8 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
8098
8748
|
* through src/domain.mjs. Fresh-loads memory (never the turn cache) because
|
|
8099
8749
|
* the caller may have just written snapshot rows this same turn. */
|
|
8100
8750
|
async function loadPlanContext(memoryDir) {
|
|
8101
|
-
const { loadMemory, readFactRows, readRuleRows } = await import("
|
|
8102
|
-
const { compileDomain, stateFromFacts } = await import("
|
|
8751
|
+
const { loadMemory, readFactRows, readRuleRows } = await import("../adapters/memory/core.mjs");
|
|
8752
|
+
const { compileDomain, stateFromFacts } = await import("../domain/domain.mjs");
|
|
8103
8753
|
const payload = await loadMemory(memoryDir);
|
|
8104
8754
|
const factRows = readFactRows(payload);
|
|
8105
8755
|
const ruleRows = readRuleRows(payload);
|
|
@@ -8117,6 +8767,32 @@ function actionLabel(name, subject, target) {
|
|
|
8117
8767
|
return `${verb} ${subject} ${prep} ${target}`;
|
|
8118
8768
|
}
|
|
8119
8769
|
|
|
8770
|
+
/** Which verb does a verbless locative goal ("every disk on peg-b") mean? The
|
|
8771
|
+
* sentence never says, and a preposition doesn't imply one — "on" reads as
|
|
8772
|
+
* rest-on, stand-on, sit-on or lie-on with equal warrant, so any prep→verb
|
|
8773
|
+
* table here would be invention. The taught facts answer instead: every
|
|
8774
|
+
* locative fact (LOCATIVE_FACT_PREDICATE_RE's closed predicate tail) about a
|
|
8775
|
+
* member of the goal's class whose preposition is the one typed contributes
|
|
8776
|
+
* its verb. Returns the candidates, sorted. Exactly one is an answer; none or
|
|
8777
|
+
* several is the caller's decline. */
|
|
8778
|
+
function goalVerbsFromTaughtFacts(factRows, domain, { universal, term, prep }) {
|
|
8779
|
+
const subjects = new Set(universal ? domain?.classMembers?.[term] || [] : [term]);
|
|
8780
|
+
const verbs = new Set();
|
|
8781
|
+
for (const row of factRows || []) {
|
|
8782
|
+
if (!LOCATIVE_FACT_PREDICATE_RE.test(row.predicate)) continue;
|
|
8783
|
+
if (!subjects.has(row.subject)) continue;
|
|
8784
|
+
const [factVerb, factPrep] = row.predicate.slice("mgx:".length).split("-");
|
|
8785
|
+
if (factPrep === prep) verbs.add(factVerb);
|
|
8786
|
+
}
|
|
8787
|
+
return [...verbs].sort();
|
|
8788
|
+
}
|
|
8789
|
+
|
|
8790
|
+
/** Do two goal specs state the same goal? Every field is already normalized
|
|
8791
|
+
* (normFactTerm on the terms, a lemma + a lowercased preposition on the
|
|
8792
|
+
* predicate), so equality on the four scalars is the whole comparison. */
|
|
8793
|
+
const sameGoalSpec = (a, b) =>
|
|
8794
|
+
a.universal === b.universal && a.term === b.term && a.predicate === b.predicate && a.object === b.object;
|
|
8795
|
+
|
|
8120
8796
|
/** THE PLAN LANE — the closed goal/solve/legal-moves recognizers over the
|
|
8121
8797
|
* taught action rules (PLAN_HANOI's chat surface). Returns
|
|
8122
8798
|
* { text, via, deduced, note, plan? } or null when the query is none of the
|
|
@@ -8124,10 +8800,78 @@ function actionLabel(name, subject, target) {
|
|
|
8124
8800
|
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
8125
8801
|
const q = String(query).trim();
|
|
8126
8802
|
|
|
8803
|
+
// "can you move a disk onto a peg?" — read the taught action signatures back.
|
|
8804
|
+
// Answered HERE rather than beside the other capability readers because
|
|
8805
|
+
// CAN_ASK_RE would otherwise claim the query first, bind the verb to "peg",
|
|
8806
|
+
// find no mgx:capableOf row and miss.
|
|
8807
|
+
const capabilityAsk = q.match(ACTION_SIGNATURE_ASK_RE);
|
|
8808
|
+
if (capabilityAsk) {
|
|
8809
|
+
const { loadMemory, readRuleRows } = await import("../adapters/memory/core.mjs");
|
|
8810
|
+
const { actionFamilies, capabilityFromActionRules } = await import("../domain/router/taught.mjs");
|
|
8811
|
+
// The same lemma authority the teach lane mints the rule name through, so
|
|
8812
|
+
// either taught voicing is found by either asked voicing.
|
|
8813
|
+
const familyName = `${await verbLemma(capabilityAsk[1])} ${capabilityAsk[3].toLowerCase()}`;
|
|
8814
|
+
const subjectClass = capabilityAsk[2].toLowerCase();
|
|
8815
|
+
const targetClass = capabilityAsk[4].toLowerCase();
|
|
8816
|
+
const asked = actionLabel(familyName, `a ${subjectClass}`, `a ${targetClass}`);
|
|
8817
|
+
let family = null;
|
|
8818
|
+
try {
|
|
8819
|
+
family = actionFamilies(readRuleRows(await loadMemory(memoryDir))).get(familyName) || null;
|
|
8820
|
+
} catch { /* an unreadable store reads back like an empty one */ }
|
|
8821
|
+
if (!family) {
|
|
8822
|
+
return {
|
|
8823
|
+
text: `no — nothing you taught me says you can ${asked}. Teach it with "you can ${asked}."`,
|
|
8824
|
+
via: "plan", deduced: "check whether a taught action rule covers an action",
|
|
8825
|
+
note: `CAPABILITY frame — no "${familyName}" action rule in the store, honest decline`,
|
|
8826
|
+
};
|
|
8827
|
+
}
|
|
8828
|
+
const classesFor = (slot) =>
|
|
8829
|
+
capabilityFromActionRules(familyName, family).parameters.find((p) => p.name === slot)?.classes.filter(Boolean) || [];
|
|
8830
|
+
const subjectClasses = classesFor("subject");
|
|
8831
|
+
const targetClasses = classesFor("target");
|
|
8832
|
+
const signature = `subject: ${subjectClasses.join("|") || "?"}, target: ${targetClasses.join("|") || "?"}`;
|
|
8833
|
+
if (!subjectClasses.includes(subjectClass) || !targetClasses.includes(targetClass)) {
|
|
8834
|
+
return {
|
|
8835
|
+
text: `no — the "${familyName}" rule you taught me covers ${signature}, and nothing you taught me says you can ${asked}.`,
|
|
8836
|
+
via: "plan", deduced: "check whether a taught action rule covers an action",
|
|
8837
|
+
note: `CAPABILITY frame — the "${familyName}" family is taught but covers ${signature}, honest decline`,
|
|
8838
|
+
};
|
|
8839
|
+
}
|
|
8840
|
+
return {
|
|
8841
|
+
text: `yes — you can ${asked}. You taught me the "${familyName}" rule (${signature}).`,
|
|
8842
|
+
via: "plan", deduced: "check whether a taught action rule covers an action",
|
|
8843
|
+
note: `CAPABILITY frame — the taught "${familyName}" family covers ${signature}`,
|
|
8844
|
+
};
|
|
8845
|
+
}
|
|
8846
|
+
|
|
8127
8847
|
const thatGoal = q.match(GOAL_TEACH_RE);
|
|
8128
|
-
|
|
8848
|
+
let goalMatch = thatGoal || q.match(GOAL_TEACH_INFINITIVE_RE);
|
|
8849
|
+
// The verbless voicing carries every capture but the verb, so it folds into
|
|
8850
|
+
// the frame below once the store names the verb — same spec, same
|
|
8851
|
+
// confirmation, same fold as its verbed twin.
|
|
8852
|
+
const verblessGoal = goalMatch ? null : q.match(GOAL_TEACH_VERBLESS_RE);
|
|
8853
|
+
if (verblessGoal) {
|
|
8854
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
8855
|
+
const { factRows, domain } = await loadPlanContext(memoryDir);
|
|
8856
|
+
const prep = verblessGoal[3].toLowerCase();
|
|
8857
|
+
const quantified = `${verblessGoal[1] ? `${verblessGoal[1].toLowerCase()} ` : ""}${verblessGoal[2].toLowerCase()}`;
|
|
8858
|
+
const stated = `${quantified} ${prep} ${verblessGoal[4].toLowerCase()}`;
|
|
8859
|
+
const verbs = goalVerbsFromTaughtFacts(factRows, domain, {
|
|
8860
|
+
universal: !!verblessGoal[1], term: normFactTerm(verblessGoal[2]), prep,
|
|
8861
|
+
});
|
|
8862
|
+
if (verbs.length !== 1) {
|
|
8863
|
+
return {
|
|
8864
|
+
text: verbs.length
|
|
8865
|
+
? `"${stated}" leaves the verb out, and what you taught me leaves it open — ${verbs.map((v) => `"${v} ${prep}"`).join(" and ")} both fit. Say which one, e.g. "i want ${quantified} to ${verbs[0]} ${prep} ${verblessGoal[4].toLowerCase()}".`
|
|
8866
|
+
: `"${stated}" leaves the verb out, and nothing you taught me says what ${quantified} does ${prep} anything. Name the verb, e.g. "the goal is that every disk rests on peg-c".`,
|
|
8867
|
+
via: "plan", deduced: "record the goal state for a later plan",
|
|
8868
|
+
note: `GOAL frame — the verbless voicing's "${prep}" matched ${verbs.length} taught locative verbs, honest decline`,
|
|
8869
|
+
};
|
|
8870
|
+
}
|
|
8871
|
+
goalMatch = [verblessGoal[0], verblessGoal[1], verblessGoal[2], verbs[0], verblessGoal[3], verblessGoal[4]];
|
|
8872
|
+
}
|
|
8129
8873
|
if (goalMatch) {
|
|
8130
|
-
const { normFactTerm } = await import("
|
|
8874
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
8131
8875
|
const verb = await verbLemma(goalMatch[3]);
|
|
8132
8876
|
if (!verb) {
|
|
8133
8877
|
return {
|
|
@@ -8147,16 +8891,31 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
8147
8891
|
// own "done — …" line and the confirmation read identically either way.
|
|
8148
8892
|
: `${goalMatch[1] ? `${goalMatch[1].toLowerCase()} ` : ""}${goalMatch[2].toLowerCase()} ${verb}s ${goalMatch[4].toLowerCase()} ${goalMatch[5].toLowerCase()}`;
|
|
8149
8893
|
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
8894
|
+
const heldGoals = prev?.goals ?? [];
|
|
8895
|
+
const heldTexts = prev?.goalTexts ?? [];
|
|
8896
|
+
// Restating a goal you already set is one goal, not two. The spec is four
|
|
8897
|
+
// normalized scalars, so the same goal in either voicing ("the goal is
|
|
8898
|
+
// that …" / "the goal is to …") compiles to the identical object and a
|
|
8899
|
+
// deep-equal catches it. Folded in the STORE, not at the read: deduping in
|
|
8900
|
+
// "solve it" would leave the duplicate sitting in planHolder.state and
|
|
8901
|
+
// leave "(N goals held)" saying something untrue.
|
|
8902
|
+
//
|
|
8903
|
+
// goals and goalTexts move in LOCKSTEP — "solve it" joins goalTexts by
|
|
8904
|
+
// index to describe the specs it compiled, so dropping one without the
|
|
8905
|
+
// other misaligns the plan's own account of what it is solving for.
|
|
8906
|
+
const alreadyHeld = heldGoals.some((g) => sameGoalSpec(g, spec));
|
|
8150
8907
|
planHolder.state = {
|
|
8151
|
-
goals:
|
|
8152
|
-
goalTexts:
|
|
8908
|
+
goals: alreadyHeld ? heldGoals : [...heldGoals, spec],
|
|
8909
|
+
goalTexts: alreadyHeld ? heldTexts : [...heldTexts, tail],
|
|
8153
8910
|
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
8154
8911
|
};
|
|
8155
8912
|
const n = planHolder.state.goals.length;
|
|
8156
8913
|
return {
|
|
8157
|
-
text:
|
|
8914
|
+
text: `${alreadyHeld ? "already noted" : "noted"} — the goal is that ${tail}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
8158
8915
|
via: "plan", deduced: "record the goal state for a later plan",
|
|
8159
|
-
note:
|
|
8916
|
+
note: alreadyHeld
|
|
8917
|
+
? "GOAL frame — the same goal spec was already held, so it folded onto the existing one"
|
|
8918
|
+
: "GOAL frame — goal spec accumulated on the session plan slot",
|
|
8160
8919
|
};
|
|
8161
8920
|
}
|
|
8162
8921
|
|
|
@@ -8183,7 +8942,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
8183
8942
|
via: "plan", deduced: "plan a move sequence (no state yet)", note: "plan lane — honest decline: empty state",
|
|
8184
8943
|
};
|
|
8185
8944
|
}
|
|
8186
|
-
const { movesFromRules, stateKeyFor, compileGoal, PlanBudgetError } = await import("
|
|
8945
|
+
const { movesFromRules, stateKeyFor, compileGoal, PlanBudgetError } = await import("../domain/domain.mjs");
|
|
8187
8946
|
|
|
8188
8947
|
if (wantsLegal) {
|
|
8189
8948
|
let moves;
|
|
@@ -8221,7 +8980,7 @@ async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", })
|
|
|
8221
8980
|
} catch (err) {
|
|
8222
8981
|
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
8223
8982
|
}
|
|
8224
|
-
const { findActionPath } = await import("
|
|
8983
|
+
const { findActionPath } = await import("../domain/planning.mjs");
|
|
8225
8984
|
let found;
|
|
8226
8985
|
try {
|
|
8227
8986
|
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth: 300, stateKey: stateKeyFor });
|
|
@@ -8282,7 +9041,7 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
8282
9041
|
const k = ps.cursor + 1;
|
|
8283
9042
|
const action = ps.actions[ps.cursor];
|
|
8284
9043
|
const rows = ps.states[k];
|
|
8285
|
-
const { appendFact, loadMemory, readFactRows } = await import("
|
|
9044
|
+
const { appendFact, loadMemory, readFactRows } = await import("../adapters/memory/core.mjs");
|
|
8286
9045
|
for (const row of rows) {
|
|
8287
9046
|
await appendFact(memoryDir, {
|
|
8288
9047
|
subject: `${row.subject}@step${k}`, predicate: row.predicate, object: row.object,
|
|
@@ -8298,8 +9057,8 @@ async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
|
8298
9057
|
};
|
|
8299
9058
|
}
|
|
8300
9059
|
// Final step: confirm the goal against the store, from the written facts.
|
|
8301
|
-
const { compileDomain, stateFromFacts, compileGoal } = await import("
|
|
8302
|
-
const { readRuleRows } = await import("
|
|
9060
|
+
const { compileDomain, stateFromFacts, compileGoal } = await import("../domain/domain.mjs");
|
|
9061
|
+
const { readRuleRows } = await import("../adapters/memory/core.mjs");
|
|
8303
9062
|
const payload = await loadMemory(memoryDir);
|
|
8304
9063
|
const factRows = readFactRows(payload);
|
|
8305
9064
|
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
@@ -8380,7 +9139,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8380
9139
|
// binds) OR the previous turn produced a set to refer back to (thread it as
|
|
8381
9140
|
// `prev` for the anaphora node). Builds the SAME delimited envelope dispatchTool
|
|
8382
9141
|
// emits, so the parse below is identical either way.
|
|
8383
|
-
const { ask } = await import("
|
|
9142
|
+
const { ask } = await import("../domain/ask.mjs");
|
|
8384
9143
|
const r = ask(graph, askQuery, { contextId: effectiveContextId, prev });
|
|
8385
9144
|
text = `${r.content}${ASK_ENVELOPE_DELIM}${JSON.stringify(r.tmct_ask, null, 2)}`;
|
|
8386
9145
|
} else {
|
|
@@ -8632,7 +9391,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8632
9391
|
? null : (matchBareHabitualTeach(bareLine) || matchBareCanTeach(bareLine));
|
|
8633
9392
|
if (pm || habitual) {
|
|
8634
9393
|
try {
|
|
8635
|
-
const { loadLexicon, lookupNoun } = await import("
|
|
9394
|
+
const { loadLexicon, lookupNoun } = await import("../domain/grammar/lexicon.mjs");
|
|
8636
9395
|
const lex = loadLexicon();
|
|
8637
9396
|
if (pm) {
|
|
8638
9397
|
const s = singularizeSurface(pm[1].toLowerCase());
|
|
@@ -8656,7 +9415,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8656
9415
|
} else {
|
|
8657
9416
|
habitualGroundingHint = habitualGroundingHintText(
|
|
8658
9417
|
bareLine.replace(/[.!?]+\s*$/, ""),
|
|
8659
|
-
{ subject: subjects[subjects.length - 1]
|
|
9418
|
+
{ ...habitual, subject: subjects[subjects.length - 1] },
|
|
8660
9419
|
);
|
|
8661
9420
|
}
|
|
8662
9421
|
}
|
|
@@ -8690,7 +9449,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8690
9449
|
const relTerm = relationTermOf(String(query), envelope);
|
|
8691
9450
|
if (relTerm) {
|
|
8692
9451
|
try {
|
|
8693
|
-
const { RELATION_TERM } = await import("
|
|
9452
|
+
const { RELATION_TERM } = await import("../domain/concept.mjs");
|
|
8694
9453
|
isVagueRelationTouch = !!RELATION_TERM[relTerm.toLowerCase()];
|
|
8695
9454
|
} catch { /* leave false — the ordinary path decides */ }
|
|
8696
9455
|
}
|
|
@@ -8741,7 +9500,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8741
9500
|
let bareMetaHit = null;
|
|
8742
9501
|
if ((isConversationalCandidate || isBareCamelCaseWhatisCandidate) && (bareWhatisShape || isAdjectiveShape || reversePredicateShape || capabilityAskShape)) {
|
|
8743
9502
|
if (memoryDir) {
|
|
8744
|
-
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache))
|
|
9503
|
+
bareMetaHit = (await factAnswer(memoryDir, gateQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
8745
9504
|
?? (await factReadBack(memoryDir, gateQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
8746
9505
|
// An honest-miss return never diverts the gate — EXCEPT the capability
|
|
8747
9506
|
// family's can't-confirm, which names the subject's real capabilities
|
|
@@ -8765,7 +9524,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8765
9524
|
if (!bareMetaHit && graph) {
|
|
8766
9525
|
const term = metaTermOf(gateQuery, envelope);
|
|
8767
9526
|
if (term) {
|
|
8768
|
-
const { metaFallbackEntityAnswer } = await import("
|
|
9527
|
+
const { metaFallbackEntityAnswer } = await import("../domain/ask.mjs");
|
|
8769
9528
|
const fallback = metaFallbackEntityAnswer(graph, term);
|
|
8770
9529
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
8771
9530
|
}
|
|
@@ -8777,11 +9536,20 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8777
9536
|
// lookup and "divert only on a REAL, UNIQUE hit" discipline: it only
|
|
8778
9537
|
// returns non-null for an EXACT label match, so ordinary small talk is
|
|
8779
9538
|
// unaffected.
|
|
8780
|
-
|
|
8781
|
-
|
|
9539
|
+
//
|
|
9540
|
+
// `isBareCamelCaseEntityCandidate` ORs in for THIS lane the way
|
|
9541
|
+
// isBareCamelCaseWhatisCandidate does for (2b) — a bare "TaskController" is
|
|
9542
|
+
// excluded from isConversationalCandidate solely by the CamelCase transition,
|
|
9543
|
+
// while a bare "task" reaches the lane. Same base gate and same
|
|
9544
|
+
// `!vocabAntecedent`, so it's never looser than the gate it joins.
|
|
9545
|
+
const isBareCamelCaseEntityCandidate = conversationalCandidateBaseGate && !vocabAntecedent
|
|
9546
|
+
&& isBareCamelCaseEntityName(query);
|
|
9547
|
+
if (!bareMetaHit && (isConversationalCandidate || isBareCamelCaseEntityCandidate) && graph) {
|
|
9548
|
+
const { metaFallbackEntityAnswer } = await import("../domain/ask.mjs");
|
|
8782
9549
|
const fallback = metaFallbackEntityAnswer(graph, String(query).trim());
|
|
8783
9550
|
if (fallback) bareMetaHit = { text: fallback.text, replace: true };
|
|
8784
9551
|
}
|
|
9552
|
+
const coldPronounDecline = focus?.label ? null : coldPronounDeclineText(query);
|
|
8785
9553
|
if (bareMetaHit) {
|
|
8786
9554
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
8787
9555
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -8808,6 +9576,16 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8808
9576
|
via = "teach-miss"; handled = true;
|
|
8809
9577
|
note(trace, "lane: (2) BARE TEACH NUDGE — a bare name-verb-name declarative stays wrapper-required; suggested the remember-that form");
|
|
8810
9578
|
note(trace, "goal: teach/remember a new fact (wrapper required for the bare form)");
|
|
9579
|
+
} else if (isConversationalCandidate && coldPronounDecline) {
|
|
9580
|
+
// A subject-position pronoun with no antecedent anywhere: no vocabulary
|
|
9581
|
+
// subject bound upstream, and no code focus for it to mean either. The
|
|
9582
|
+
// orientation card would introduce the tool; naming the pronoun says what
|
|
9583
|
+
// actually went wrong. Still a miss in the record — honest wording, not an
|
|
9584
|
+
// answer.
|
|
9585
|
+
answer = coldPronounDecline;
|
|
9586
|
+
via = "template"; handled = true;
|
|
9587
|
+
note(trace, "lane: (2) COLD PRONOUN — a subject pronoun with no antecedent bound and no focus standing; named the pronoun instead of the orientation card");
|
|
9588
|
+
note(trace, "goal: resolve a pronoun to a subject (nothing named yet)");
|
|
8811
9589
|
} else if (isConversationalCandidate) {
|
|
8812
9590
|
// A conversational miss (a greeting, "what can you do", a very short non-code
|
|
8813
9591
|
// line) gets the friendly orientation (module-aware: empty → --repo/tmct init).
|
|
@@ -8844,10 +9622,10 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
8844
9622
|
// normalization-mangled text reach readers whose guards were written for
|
|
8845
9623
|
// the raw surface (the pronoun-subject identity family).
|
|
8846
9624
|
const normalizedForFacts = envelope ? null : normalizeQuery(String(query));
|
|
8847
|
-
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache))
|
|
9625
|
+
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
8848
9626
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache))
|
|
8849
9627
|
?? (normalizedForFacts && normalizedForFacts !== String(query).trim()
|
|
8850
|
-
? (await factAnswer(memoryDir, normalizedForFacts, envelope, miss, biasByBundle, cache))
|
|
9628
|
+
? (await factAnswer(memoryDir, normalizedForFacts, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
8851
9629
|
?? (await factReadBack(memoryDir, normalizedForFacts, envelope, miss, graph, newFocus?.label, biasByBundle, cache))
|
|
8852
9630
|
: null);
|
|
8853
9631
|
if (fact) {
|
|
@@ -9060,7 +9838,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9060
9838
|
}
|
|
9061
9839
|
}
|
|
9062
9840
|
}
|
|
9063
|
-
// (4e) COMPLETIONS RESCUE — wires src/completions/'s extractive
|
|
9841
|
+
// (4e) COMPLETIONS RESCUE — wires src/domain/completions/'s extractive
|
|
9064
9842
|
// multi-sentence pipeline in as a genuine last-resort lane, tried ONLY here,
|
|
9065
9843
|
// after EVERY lane above has already declined; this lane only fires for an
|
|
9066
9844
|
// EXPLICIT "detailed summary/overview of how X works" phrasing, a shape
|
|
@@ -9069,8 +9847,8 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9069
9847
|
const completed = await completionsRescueAnswer(query, { memoryDir, graph });
|
|
9070
9848
|
if (completed) {
|
|
9071
9849
|
answer = completed.text; via = "completion"; recordMiss = false;
|
|
9072
|
-
note(trace, "lane: (4e) COMPLETIONS RESCUE — a \"detailed summary/overview of how X works\" phrasing matched, answered via src/completions/'s extractive multi-sentence pipeline (generateCompletion())");
|
|
9073
|
-
note(trace, "source: src/completions/complete.mjs generateCompletion() (broadSearch + groupHits + rankSentences + inferRelations + pruneCompletion + finish())");
|
|
9850
|
+
note(trace, "lane: (4e) COMPLETIONS RESCUE — a \"detailed summary/overview of how X works\" phrasing matched, answered via src/domain/completions/'s extractive multi-sentence pipeline (generateCompletion())");
|
|
9851
|
+
note(trace, "source: src/domain/completions/complete.mjs generateCompletion() (broadSearch + groupHits + rankSentences + inferRelations + pruneCompletion + finish())");
|
|
9074
9852
|
note(trace, "goal: produce a grounded, cited, multi-sentence account of the subject (not a single fact/definition)");
|
|
9075
9853
|
}
|
|
9076
9854
|
}
|
|
@@ -9095,6 +9873,37 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9095
9873
|
}
|
|
9096
9874
|
}
|
|
9097
9875
|
}
|
|
9876
|
+
// (4g) FUZZY-VERB DECLINE — the keyword strategy's bounded-edit-distance
|
|
9877
|
+
// tier rewrites ANY word within one edit of a graph verb, with no check that
|
|
9878
|
+
// the typed word is real English: "rest" reads as "test", "during" as
|
|
9879
|
+
// "using", "bigger" as "trigger", "behave" as "have", "ball" as "call".
|
|
9880
|
+
// Whatever the repaired sentence traverses to answers a DIFFERENT question,
|
|
9881
|
+
// and it does not read like one — "does store.mjs rest on app.mjs" comes
|
|
9882
|
+
// back "No — no tests edge found", and a reader takes the No. So name the
|
|
9883
|
+
// rewrite and refuse, dropping both halves of the receipt with it (a receipt
|
|
9884
|
+
// for a question nobody asked is the same wrong answer in smaller type).
|
|
9885
|
+
//
|
|
9886
|
+
// Tried HERE, after every rescue lane above has already declined, for the
|
|
9887
|
+
// same reason (4d)/(4e)/(4f) are: a repaired sentence some other lane can
|
|
9888
|
+
// answer keeps that answer untouched. This only ever replaces the repaired
|
|
9889
|
+
// parse's OWN standing reply.
|
|
9890
|
+
//
|
|
9891
|
+
// A real English word never reaches here: the repair tier's own collision table
|
|
9892
|
+
// (src/domain/real-word-collisions.json) refuses it before any distance is
|
|
9893
|
+
// measured, so "rest" misses as itself. What is left for this lane is a NON-word
|
|
9894
|
+
// that repaired onto a verb whose lemma differs from the word it came from —
|
|
9895
|
+
// "impotr" still repairs to "import" and answers, because they share one.
|
|
9896
|
+
if (miss && recordMiss && via === "composed" && envelope?.parsed?.fuzzyVerb) {
|
|
9897
|
+
const { from, to } = envelope.parsed.fuzzyVerb;
|
|
9898
|
+
if (!(await repairSharesLemma(from, to))) {
|
|
9899
|
+
answer = `I read "${from}" as "${to}", which asks a different question — so I won't answer it. `
|
|
9900
|
+
+ `"${from}" isn't a relation I record. Say the relation you mean, or /help for the query shapes I read.`;
|
|
9901
|
+
via = "miss";
|
|
9902
|
+
canonical = null;
|
|
9903
|
+
deduced = null;
|
|
9904
|
+
note(trace, `lane: (4g) FUZZY-VERB DECLINE — "${from}" only became a verb through the edit-distance repair tier ("${to}"), and the two words are different verbs, so the repaired sentence's graph answer is dropped rather than shown as an answer to what was typed`);
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9098
9907
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
9099
9908
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
9100
9909
|
// WALL KINDNESS: a second consecutive wall collapses to a one-liner whose
|
|
@@ -9123,11 +9932,14 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
9123
9932
|
// without a resolveEntity(graph) gate: it's inherently a MEMORY question,
|
|
9124
9933
|
// so "nothing yet, teach me" is appropriate even when X is also a real
|
|
9125
9934
|
// graph entity.
|
|
9126
|
-
|
|
9127
|
-
|
|
9935
|
+
// Contraction-expanded, so "what's X" earns the same offer "what is X"
|
|
9936
|
+
// does. Both shapes below anchor on the written-out copula.
|
|
9937
|
+
const offerSrc = expandContractions(String(query).trim());
|
|
9938
|
+
const knowAboutTerm = offerSrc.match(KNOW_ABOUT_RE)?.[1]?.trim();
|
|
9939
|
+
const offerTerm = knowAboutTerm || metaTermOf(offerSrc, envelope);
|
|
9128
9940
|
if (offerTerm) {
|
|
9129
9941
|
let normFactTerm;
|
|
9130
|
-
try { ({ normFactTerm } = await import("
|
|
9942
|
+
try { ({ normFactTerm } = await import("../adapters/memory/core.mjs")); } catch { normFactTerm = null; }
|
|
9131
9943
|
if (normFactTerm) {
|
|
9132
9944
|
const cleanTerm = normFactTerm(offerTerm);
|
|
9133
9945
|
const ent = knowAboutTerm ? null : await resolveEntity(graph, offerTerm);
|
|
@@ -9263,6 +10075,8 @@ const GOAL_BY_COMMAND = {
|
|
|
9263
10075
|
history: GOAL_BY_KIND.touches,
|
|
9264
10076
|
exports: GOAL_BY_KIND.reexports,
|
|
9265
10077
|
arch: "understand the overall architecture (package/module boundaries)",
|
|
10078
|
+
capabilities: "see what /plan can plan over — built-in query tools and taught actions",
|
|
10079
|
+
syllogise: "materialize the entailed facts that follow from what's remembered about one term",
|
|
9266
10080
|
};
|
|
9267
10081
|
|
|
9268
10082
|
/** A slash-command → the mapped tool (or the /help, /focus, /narrate, unknown
|
|
@@ -9310,7 +10124,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
9310
10124
|
note(trace, "goal: inspect tmct's memory store (facts/utterances/sessions)");
|
|
9311
10125
|
if (!memoryDir) return mk("no memory store here — /memory works inside a repo session.", { miss: true });
|
|
9312
10126
|
try {
|
|
9313
|
-
const { inspectMemory } = await import("
|
|
10127
|
+
const { inspectMemory } = await import("../adapters/memory/inspect.mjs");
|
|
9314
10128
|
return mk(await inspectMemory(memoryDir, { verbose: /^(?:-v|--verbose|verbose)$/i.test(argText) }));
|
|
9315
10129
|
} catch (e) {
|
|
9316
10130
|
return mk(String(e?.message || e), { miss: true }); // a broken store reads as its own clean error
|
|
@@ -9326,7 +10140,89 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
9326
10140
|
return mk(`focus set to ${ent.label}.`, { resolvedIds: [ent.id], newFocus: ent });
|
|
9327
10141
|
}
|
|
9328
10142
|
|
|
9329
|
-
// /
|
|
10143
|
+
// /capabilities — everything a /plan request can plan over: the built-in
|
|
10144
|
+
// read-only graph-query tools, plus the taught action families read straight
|
|
10145
|
+
// from this store (they only live in the registry inside a /plan request,
|
|
10146
|
+
// so listing them means reading the rules, not the registry).
|
|
10147
|
+
if (name === "capabilities") {
|
|
10148
|
+
note(trace, "goal: see what /plan can plan over — built-in query tools and taught actions");
|
|
10149
|
+
const { declaredCapabilityNames } = await import("../domain/router/drive.mjs");
|
|
10150
|
+
const { actionFamilies, capabilityFromActionRules } = await import("../domain/router/taught.mjs");
|
|
10151
|
+
const lines = [`read-only graph tools: ${declaredCapabilityNames().join(", ")}`];
|
|
10152
|
+
let families = new Map();
|
|
10153
|
+
if (memoryDir) {
|
|
10154
|
+
try {
|
|
10155
|
+
const { loadMemory, readRuleRows } = await import("../adapters/memory/core.mjs");
|
|
10156
|
+
families = actionFamilies(readRuleRows(await loadMemory(memoryDir)));
|
|
10157
|
+
} catch { /* an unreadable store lists like an empty one */ }
|
|
10158
|
+
}
|
|
10159
|
+
if (!families.size) {
|
|
10160
|
+
lines.push('taught actions: none yet — teach one ("you can move a disk onto a peg.") and /plan can use it.');
|
|
10161
|
+
} else {
|
|
10162
|
+
lines.push("taught actions (planned over, never dispatched):");
|
|
10163
|
+
for (const [familyName, family] of [...families.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
10164
|
+
const cap = capabilityFromActionRules(familyName, family);
|
|
10165
|
+
const sig = cap.parameters
|
|
10166
|
+
.map((p) => `${p.name}: ${p.classes.filter(Boolean).join("|") || "?"}`)
|
|
10167
|
+
.join(", ");
|
|
10168
|
+
lines.push(` taught:${familyName} — ${sig}`);
|
|
10169
|
+
}
|
|
10170
|
+
}
|
|
10171
|
+
return mk(lines.join("\n"));
|
|
10172
|
+
}
|
|
10173
|
+
|
|
10174
|
+
// /syllogise <term> — forward-chain what's remembered about <term> into
|
|
10175
|
+
// entailed facts and WRITE them to the store, so a chain too long for the
|
|
10176
|
+
// live isa ladder to walk becomes a single stored step it can read.
|
|
10177
|
+
//
|
|
10178
|
+
// This is the one chat surface that writes derived facts. Every live chase
|
|
10179
|
+
// in this file is read-only on purpose, and that stays true: a slash command
|
|
10180
|
+
// is an explicit request, not the hot path an ordinary question runs down.
|
|
10181
|
+
// Nothing here is a guess — each written fact carries `entailed:*`
|
|
10182
|
+
// provenance, a justification citing its premises, and a trust discounted
|
|
10183
|
+
// below the premises it rode.
|
|
10184
|
+
//
|
|
10185
|
+
// The term is an ARGUMENT rather than the session focus, and the two are
|
|
10186
|
+
// different things wearing the same name: this focus is a set of class
|
|
10187
|
+
// TERM strings, while chat's `focus` is a {id,label} code-graph entity, so
|
|
10188
|
+
// passing the standing focus here would be a category error. Omitting it
|
|
10189
|
+
// is worse than useless — a whole-store pass on a real store spends the
|
|
10190
|
+
// budget on facts nobody asked about and can be truncated before it reaches
|
|
10191
|
+
// the term you cared about. /plan is the precedent: a command that takes an
|
|
10192
|
+
// argument and honestly refuses without one.
|
|
10193
|
+
if (name === "syllogise") {
|
|
10194
|
+
note(trace, "goal: materialize the entailed facts that follow from what's remembered about one term");
|
|
10195
|
+
if (!memoryDir) return mk("no memory store here — /syllogise works inside a repo session.", { miss: true });
|
|
10196
|
+
if (!argText) {
|
|
10197
|
+
return mk('/syllogise needs a term, e.g. `/syllogise poodle` — it closes over what I remember about that term.', { miss: true });
|
|
10198
|
+
}
|
|
10199
|
+
try {
|
|
10200
|
+
const { syllogise } = await import("../domain/syllogise.mjs");
|
|
10201
|
+
const { loadMemory, readFactRows, appendFacts, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
10202
|
+
const res = await syllogise(memoryDir, {
|
|
10203
|
+
focus: [...factTermVariants(normFactTerm, argText)],
|
|
10204
|
+
store: { loadMemory, readFactRows, appendFacts },
|
|
10205
|
+
});
|
|
10206
|
+
note(trace, `result: derived ${res.count} entailed fact(s) (depth ${res.depth}, budget ${res.budget})`);
|
|
10207
|
+
if (!res.count) {
|
|
10208
|
+
return mk(`nothing new follows from what I remember about "${argText}" — no entailed facts derived (depth ${res.depth}, budget ${res.budget}).`, { miss: true });
|
|
10209
|
+
}
|
|
10210
|
+
const lines = [`derived ${res.count} entailed fact(s) from what I remember about "${argText}":`];
|
|
10211
|
+
for (const d of res.derived) lines.push(` ${d.subject} ${d.rule} ${d.object} (via ${d.via})`);
|
|
10212
|
+
// A truncated pass that still can't answer the question is worse than no
|
|
10213
|
+
// offer at all, so the budget wall is stated rather than left implied by
|
|
10214
|
+
// a count that happens to equal it.
|
|
10215
|
+
if (res.truncated) {
|
|
10216
|
+
lines.push(`budget of ${res.budget} reached — more may follow; run \`tmct syllogise --budget <n>\` for a wider pass.`);
|
|
10217
|
+
}
|
|
10218
|
+
lines.push("These are derived, not taught — /memory shows each one's provenance and premises.");
|
|
10219
|
+
return mk(lines.join("\n"));
|
|
10220
|
+
} catch (e) {
|
|
10221
|
+
return mk(String(e?.message || e), { miss: true }); // a broken store reads as its own clean error
|
|
10222
|
+
}
|
|
10223
|
+
}
|
|
10224
|
+
|
|
10225
|
+
// /plan <request> — the capability router (src/domain/router/*): plan+execute a
|
|
9330
10226
|
// compound ("of the modules impacted by X, which are untested", "assess X
|
|
9331
10227
|
// and then check Y") or maintenance-goal ("what most needs a test") request
|
|
9332
10228
|
// over the SAME read-only graph-query tools the other commands dispatch.
|
|
@@ -9335,8 +10231,8 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
9335
10231
|
note(trace, "goal: plan/execute a compound or maintenance-goal request over the graph (the capability router)");
|
|
9336
10232
|
if (!argText) return mk("/plan needs a request, e.g. `/plan of the modules impacted by X, which are untested`.", { miss: true });
|
|
9337
10233
|
if (!graph) return mk("no graph loaded — /plan needs a code graph to plan over.", { miss: true });
|
|
9338
|
-
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("
|
|
9339
|
-
const planCtx = await buildCapabilityPlanCtx({ config, source, tel, graph, memoryDir });
|
|
10234
|
+
const { buildCapabilityPlanCtx, runCapabilityPlan, declaredCapabilityNames } = await import("../domain/router/drive.mjs");
|
|
10235
|
+
const planCtx = await buildCapabilityPlanCtx({ ...capabilityPlanDeps(), config, source, tel, graph, memoryDir });
|
|
9340
10236
|
try {
|
|
9341
10237
|
const result = await runCapabilityPlan(argText, declaredCapabilityNames(), planCtx);
|
|
9342
10238
|
if (result.refused) {
|
|
@@ -9363,7 +10259,7 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
9363
10259
|
|
|
9364
10260
|
const spec = COMMANDS[name];
|
|
9365
10261
|
if (!spec) {
|
|
9366
|
-
note(trace, `pattern: /${name} is not a registered command (see COMMANDS in src/chat.mjs)`);
|
|
10262
|
+
note(trace, `pattern: /${name} is not a registered command (see COMMANDS in src/services/chat.mjs)`);
|
|
9367
10263
|
return mk(`unknown command /${name} — type /help for the list of commands.`, { miss: true });
|
|
9368
10264
|
}
|
|
9369
10265
|
note(trace, `goal: ${spec.help}`);
|
|
@@ -9456,15 +10352,15 @@ function renderAmbiguousAssert(line, ambiguous, normFactTerm) {
|
|
|
9456
10352
|
* the unchanged parseAce path below. */
|
|
9457
10353
|
async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, cache = null }) {
|
|
9458
10354
|
try {
|
|
9459
|
-
const { parseAce, parseAceAmbiguous } = await import("
|
|
10355
|
+
const { parseAce, parseAceAmbiguous } = await import("../domain/grammar/ace.mjs");
|
|
9460
10356
|
// A session handle carries its own loaded lexicon (createSession loads it once);
|
|
9461
10357
|
// a bare runTurn (no handle) lazy-loads the cached core lexicon. The lexicon is
|
|
9462
10358
|
// immutable, so sharing one reference across concurrent handles is re-entrant.
|
|
9463
10359
|
let lex = lexicon;
|
|
9464
|
-
if (!lex) { const { loadLexicon } = await import("
|
|
10360
|
+
if (!lex) { const { loadLexicon } = await import("../domain/grammar/lexicon.mjs"); lex = loadLexicon(); }
|
|
9465
10361
|
const ambiguous = parseAceAmbiguous(line, lex);
|
|
9466
10362
|
if (ambiguous) {
|
|
9467
|
-
const { normFactTerm } = await import("
|
|
10363
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
9468
10364
|
const answer = renderAmbiguousAssert(line, ambiguous, normFactTerm);
|
|
9469
10365
|
// Genuinely ambiguous — no single triple was committed, so the canonical
|
|
9470
10366
|
// form is every surviving reading's own would-be triple set, same idiom
|
|
@@ -9479,12 +10375,13 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
9479
10375
|
}
|
|
9480
10376
|
const parse = parseAce(line, lex);
|
|
9481
10377
|
if (!parse || !parse.triples?.length || parse.residue?.length) return null;
|
|
9482
|
-
const { assertSentence } = await import("
|
|
9483
|
-
const { normFactTerm, appendFact } = await import("
|
|
10378
|
+
const { assertSentence } = await import("../domain/grammar/assert.mjs");
|
|
10379
|
+
const { normFactTerm, appendFact } = await import("../adapters/memory/core.mjs");
|
|
9484
10380
|
const ts = new Date().toISOString();
|
|
9485
10381
|
const res = await assertSentence(memoryDir, line, {
|
|
9486
10382
|
lexicon: lex,
|
|
9487
10383
|
provenance: { source: "chat", sessionId, ts },
|
|
10384
|
+
appendFact,
|
|
9488
10385
|
});
|
|
9489
10386
|
if (!res || !res.ids?.length) return null;
|
|
9490
10387
|
// A plain universal "every X is a Y" ALSO records the "every" quantifier
|
|
@@ -9514,7 +10411,7 @@ async function assertTurn(line, { memoryDir, sessionId, focus, lexicon = null, c
|
|
|
9514
10411
|
let paraphraseSuffix = "";
|
|
9515
10412
|
if (res.triples.length === 1 && res.triples[0].predicate === SUBCLASS_PREDICATE) {
|
|
9516
10413
|
try {
|
|
9517
|
-
const { paraphraseVerifiedSubClass } = await import("
|
|
10414
|
+
const { paraphraseVerifiedSubClass } = await import("../domain/paraphrase.mjs");
|
|
9518
10415
|
// Normalized (same normFactTerm cleanup `shown` above already applies)
|
|
9519
10416
|
// so the generated paraphrase text reads like "cache is a kind of
|
|
9520
10417
|
// component", never a raw lexicon-prefixed form like "tmct:cache".
|
|
@@ -9635,19 +10532,68 @@ function rewriteUsesAsBaseFrame(text) {
|
|
|
9635
10532
|
return null;
|
|
9636
10533
|
}
|
|
9637
10534
|
|
|
10535
|
+
/** The pronouns runTurn's vocabulary binding accepts in SUBJECT position, and
|
|
10536
|
+
* the shapes that put one there. The set is CONTEXT_WORDS — this file's own
|
|
10537
|
+
* closed anaphor table, the one isPronoun and every focus-resolving reader
|
|
10538
|
+
* already trust — plus the plural "they", which the fact readers take as a
|
|
10539
|
+
* bare subject the same way. Only "here" is left out: it stands for a PLACE
|
|
10540
|
+
* ("what's in here" = this repo), never for the thing a fact is about, so
|
|
10541
|
+
* binding it to a subject would be a category error.
|
|
10542
|
+
*
|
|
10543
|
+
* The lead itself is what keeps this to subject position: the pronoun must
|
|
10544
|
+
* directly follow the opening auxiliary ("can it bark") or "what is/are" WITH
|
|
10545
|
+
* a continuation ("what is it used for"), so an idiom carrying a trailing
|
|
10546
|
+
* dummy pronoun ("what time is it") and the bare "what is it" never rewrite. */
|
|
10547
|
+
const VOCAB_PRONOUN_LEAD_SUBJECTS = Object.freeze([...CONTEXT_WORDS].filter((w) => w !== "here").concat("they"));
|
|
10548
|
+
const VOCAB_PRONOUN_LEAD_RE = new RegExp(
|
|
10549
|
+
`^((?:is|are|can|could|does|do)\\s+|what\\s+(?:is|are)\\s+)(${VOCAB_PRONOUN_LEAD_SUBJECTS.join("|")})\\b(\\s+\\S.*)?$`, "i",
|
|
10550
|
+
);
|
|
10551
|
+
|
|
10552
|
+
/** The decline for a subject-position pronoun with nothing to bind it to —
|
|
10553
|
+
* "can it bark" as the very first thing said, before anything named a dog.
|
|
10554
|
+
* Returns the text, or null when the shape isn't a cold pronoun.
|
|
10555
|
+
*
|
|
10556
|
+
* The vocabulary binding above already declines this correctly (no `last`
|
|
10557
|
+
* subject, so no substitution), and the fact readers then decline too, since
|
|
10558
|
+
* no row has "it" as its subject. What was left was the generic orientation
|
|
10559
|
+
* card, which introduces the tool and answers a question nobody asked. Name
|
|
10560
|
+
* the pronoun instead: the sentence was fine, it just arrived with nothing
|
|
10561
|
+
* behind it.
|
|
10562
|
+
*
|
|
10563
|
+
* The example is the "<name>" placeholder nudgeAnswer's own no-focus pronoun
|
|
10564
|
+
* branch uses, not a real term. A concrete "what is a dog" would claim a
|
|
10565
|
+
* vocabulary an unseeded session doesn't have, and a seeding/teaching hint
|
|
10566
|
+
* belongs to the shapes that are ABOUT teaching — this shape is a question
|
|
10567
|
+
* whose subject went missing, and inviting a teach here reads as an offer to
|
|
10568
|
+
* store a fact about the pronoun itself. */
|
|
10569
|
+
function coldPronounDeclineText(query) {
|
|
10570
|
+
const m = String(query || "").trim().match(VOCAB_PRONOUN_LEAD_RE);
|
|
10571
|
+
// The bare "what is it" carries no predicate to answer, so it keeps the
|
|
10572
|
+
// orientation card the same way the binding above leaves it alone.
|
|
10573
|
+
if (!m || (/^what/i.test(m[1]) && !m[3])) return null;
|
|
10574
|
+
return `not sure what "${m[2].toLowerCase()}" refers to yet — name the subject directly, e.g. "what is a <name>".`;
|
|
10575
|
+
}
|
|
10576
|
+
|
|
9638
10577
|
/** The subject of the LAST turn's first fact line, for vocabulary pronoun
|
|
9639
10578
|
* binding ("what is a dog" → "can it bark"). Fact answers render rigidly —
|
|
9640
10579
|
* "<subject> <phrase> <object> (source: …)", optionally behind a "yes — "/
|
|
9641
10580
|
* "no — "/"you told me: " prefix — so a 1–2 word leading subject followed
|
|
9642
10581
|
* by a phrase-table verb is extractable without any NLP. Anything else
|
|
9643
10582
|
* (code answers, walls, conversational text) returns null and no
|
|
9644
|
-
* substitution happens.
|
|
10583
|
+
* substitution happens.
|
|
10584
|
+
*
|
|
10585
|
+
* A pronoun never binds. An honest miss opens first-person ("I can't confirm
|
|
10586
|
+
* that — …"), which fits the subject+verb shape exactly, so without the
|
|
10587
|
+
* isTeachPronoun check the miss lends "I" to the next turn and "is it an
|
|
10588
|
+
* animal" is looked up as "is I an animal". A pronoun is no more a fact
|
|
10589
|
+
* subject here than in the teach frames TEACH_PRONOUNS already guards. */
|
|
9645
10590
|
function vocabAntecedentFrom(last) {
|
|
9646
10591
|
const first = String(last?.answer || "").split("\n")[0]
|
|
9647
10592
|
.replace(/^(?:yes|no) — /i, "")
|
|
9648
10593
|
.replace(/^you told me: /i, "");
|
|
9649
10594
|
const m = first.match(/^([a-z][\w'-]*(?:\s+[a-z][\w'-]*)?)\s+(?:is|are|has|can|causes|wants|requires|involves|means|begins|ends)\b/i);
|
|
9650
|
-
|
|
10595
|
+
if (!m || isTeachPronoun(m[1]) || isPronoun(m[1])) return null;
|
|
10596
|
+
return m[1];
|
|
9651
10597
|
}
|
|
9652
10598
|
|
|
9653
10599
|
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, _noSplit = false } = {}) {
|
|
@@ -9668,6 +10614,14 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
9668
10614
|
// doesn't match one of the four discontiguous shapes.
|
|
9669
10615
|
const baseFrameRewrite = rewriteUsesAsBaseFrame(preRewriteLine);
|
|
9670
10616
|
const frameLine = baseFrameRewrite || preRewriteLine;
|
|
10617
|
+
// The reverse cleft's "it" is scaffolding, so it has to go before the
|
|
10618
|
+
// vocabulary pronoun binding below reads that same "it" as a referring
|
|
10619
|
+
// pronoun and binds the last turn's subject to it ("what is it that calls
|
|
10620
|
+
// loadStore" -> "what is dog that calls loadStore" after "what is a dog").
|
|
10621
|
+
// The pronoun lead's own guard only spares the BARE "what is it", so this
|
|
10622
|
+
// shape has to stop existing before that match runs at all.
|
|
10623
|
+
const cleftRewrite = reverseCleftRewrite(frameLine);
|
|
10624
|
+
const cleftLine = cleftRewrite || frameLine;
|
|
9671
10625
|
// VOCABULARY pronoun antecedent — "what is a dog" then "can it bark". The
|
|
9672
10626
|
// code-graph focus mechanism only ever binds {id,label} GRAPH entities, so
|
|
9673
10627
|
// in a vocabulary conversation "it" resolved to nothing and the question
|
|
@@ -9676,17 +10630,13 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
9676
10630
|
// ONLY when no code focus is standing (a graph session's own pronoun
|
|
9677
10631
|
// resolution is untouched), the turn looks like a fact question, and the
|
|
9678
10632
|
// LAST answer's own first fact line names a subject to bind to.
|
|
9679
|
-
|
|
9680
|
-
// opening auxiliary ("can it bark") or "what is/are" WITH a continuation
|
|
9681
|
-
// ("what is it used for") — so idioms carrying a trailing dummy pronoun
|
|
9682
|
-
// ("what time is it") and the bare "what is it" are never rewritten.
|
|
9683
|
-
const pronounLead = frameLine.match(/^((?:is|are|can|could|does|do)\s+|what\s+(?:is|are)\s+)(?:it|they)\b(\s+\S.*)?$/i);
|
|
10633
|
+
const pronounLead = cleftLine.match(VOCAB_PRONOUN_LEAD_RE);
|
|
9684
10634
|
const vocabAntecedent = (!focus?.id && memoryDir && pronounLead
|
|
9685
|
-
&& !(/^what/i.test(pronounLead[1]) && !pronounLead[
|
|
10635
|
+
&& !(/^what/i.test(pronounLead[1]) && !pronounLead[3]))
|
|
9686
10636
|
? vocabAntecedentFrom(last) : null;
|
|
9687
10637
|
const workingLine = vocabAntecedent
|
|
9688
|
-
? `${pronounLead[1]}${vocabAntecedent}${pronounLead[
|
|
9689
|
-
:
|
|
10638
|
+
? `${pronounLead[1]}${vocabAntecedent}${pronounLead[3] || ""}`
|
|
10639
|
+
: cleftLine;
|
|
9690
10640
|
const templates = await chatTemplates(); // failure-tolerated: null degrades, never throws
|
|
9691
10641
|
const trace = narrate ? [] : null;
|
|
9692
10642
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
@@ -9769,7 +10719,8 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
9769
10719
|
const sentences = splitSentences(workingLine);
|
|
9770
10720
|
if (sentences.length > 1) {
|
|
9771
10721
|
const lastSentence = sentences[sentences.length - 1];
|
|
9772
|
-
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || GOAL_TEACH_INFINITIVE_RE.test(lastSentence)
|
|
10722
|
+
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || GOAL_TEACH_INFINITIVE_RE.test(lastSentence)
|
|
10723
|
+
|| GOAL_TEACH_VERBLESS_RE.test(lastSentence) || LEGAL_MOVES_RE.test(lastSentence)) {
|
|
9773
10724
|
let f = focus; let l = last; let ps = planHolder.state;
|
|
9774
10725
|
const receipts = [];
|
|
9775
10726
|
let finalRec = null;
|
|
@@ -9895,45 +10846,20 @@ export const SEED_PREFER = ["rdfs:subClassOf", "rdf:type", "mgx:usedFor", "mgx:p
|
|
|
9895
10846
|
* corpus seed, so re-runs skip without even reading the slice. */
|
|
9896
10847
|
export const SEED_MARKER_REL = join(".tmct", "memory", "corpus-seed.json");
|
|
9897
10848
|
|
|
9898
|
-
/** Bootstrap <repo> for tmct on a graph-less first run: delegates to the FULL
|
|
9899
|
-
* `initRepo(repo, {persona: PERSONA_PRESETS.human, env})` — the exact same
|
|
9900
|
-
* function `tmct init` calls — so a library consumer gets the SAME
|
|
9901
|
-
* first-run experience: real `.tmct/` scaffold, a written `tmct.toml`,
|
|
9902
|
-
* `.tmct/init.json` provenance, not just a seed marker. `initRepo` only
|
|
9903
|
-
* writes `tmct.toml` when absent, and only (re)seeds when the marker is
|
|
9904
|
-
* absent, so a repeat call after a prior CLI `tmct init` is a safe no-op.
|
|
9905
|
-
* Returns `initRepo`'s own `seedResult` on a fresh seed, null when
|
|
9906
|
-
* skipped/failed. */
|
|
9907
|
-
async function seedBootstrapMemory(repo, env = process.env) {
|
|
9908
|
-
try {
|
|
9909
|
-
const { initRepo, PERSONA_PRESETS } = await import("./init.mjs");
|
|
9910
|
-
const result = await initRepo(repo, { persona: PERSONA_PRESETS.human, env });
|
|
9911
|
-
return result.seeded ? result.seedResult : null;
|
|
9912
|
-
} catch {
|
|
9913
|
-
return null; // repo/corpus unavailable — bootstrap proceeds unseeded
|
|
9914
|
-
}
|
|
9915
|
-
}
|
|
9916
|
-
|
|
9917
|
-
/** The seed banner line — renders every `perBundle` entry that actually
|
|
9918
|
-
* appended facts this run, in the entries' own fixed order (seon,
|
|
9919
|
-
* conceptnet, then the rest sorted by name), joined with " + ". No bundle
|
|
9920
|
-
* is privileged as one of "the first two" — a single active bundle renders
|
|
9921
|
-
* with no " + " at all. */
|
|
9922
|
-
function seedBannerLine(seeded) {
|
|
9923
|
-
const clauses = Object.entries(seeded.perBundle || {})
|
|
9924
|
-
.filter(([, r]) => r && r.appended > 0)
|
|
9925
|
-
.map(([name, r]) => `${r.appended} ${name}`);
|
|
9926
|
-
return `seeded ${seeded.appended} starter facts (${clauses.join(" + ")}) — /memory to inspect`;
|
|
9927
|
-
}
|
|
9928
|
-
|
|
9929
10849
|
/** Whether THIS repo's memory actually carries the corpus seed — the marker is
|
|
9930
10850
|
* authoritative regardless of whether the CURRENT run or an earlier one did
|
|
9931
10851
|
* the seeding. The one signal every "try this vocabulary example" surface
|
|
9932
|
-
* must check before offering a term-specific query — see vocabExampleHint.
|
|
9933
|
-
|
|
10852
|
+
* must check before offering a term-specific query — see vocabExampleHint.
|
|
10853
|
+
* Used both here (runTurn's per-call vocabHint fallback) and by the session
|
|
10854
|
+
* layer's createSession; the readFile is imported lazily so this module stays
|
|
10855
|
+
* free of a static node:fs import. */
|
|
10856
|
+
export async function hasSeededVocabulary(repo) {
|
|
9934
10857
|
if (!repo) return false;
|
|
9935
|
-
try {
|
|
9936
|
-
|
|
10858
|
+
try {
|
|
10859
|
+
const { readFile } = await import("node:fs/promises");
|
|
10860
|
+
await readFile(join(repo, SEED_MARKER_REL), "utf8");
|
|
10861
|
+
return true;
|
|
10862
|
+
} catch { return false; }
|
|
9937
10863
|
}
|
|
9938
10864
|
|
|
9939
10865
|
/** A "try this" vocabulary-example clause that's PROVABLY correct in the session
|
|
@@ -9948,376 +10874,8 @@ async function hasSeededVocabulary(repo) {
|
|
|
9948
10874
|
* with an intuitive-but-unknown word and hit the teach-miss dead-end right
|
|
9949
10875
|
* after being offered the pattern. "every bug is an issue" is confirmed to
|
|
9950
10876
|
* parse and store, so the offer resolves if copied verbatim. */
|
|
9951
|
-
function vocabExampleHint(seeded) {
|
|
10877
|
+
export function vocabExampleHint(seeded) {
|
|
9952
10878
|
return seeded
|
|
9953
10879
|
? 'Try "what is a dog" for general vocabulary.'
|
|
9954
10880
|
: 'Run `tmct init` to seed a starter vocabulary, or teach me directly, e.g. "every bug is an issue".';
|
|
9955
10881
|
}
|
|
9956
|
-
|
|
9957
|
-
/** Trim a focus label for the prompt so a long module path can't run the line off. */
|
|
9958
|
-
const shortLabel = (l) => { const s = String(l); return s.length > 40 ? "…" + s.slice(-39) : s; };
|
|
9959
|
-
const promptFor = (focus) => (focus ? `tmct(${shortLabel(focus.label)})> ` : PROMPT);
|
|
9960
|
-
|
|
9961
|
-
/**
|
|
9962
|
-
* The SESSION SINK — everything a chat shell (readline below, the Ink TUI, any
|
|
9963
|
-
* future surface) must share so the on-disk session contract stays identical
|
|
9964
|
-
* no matter what draws the screen: repo/config resolution + the one-time
|
|
9965
|
-
* graph load, the transcript log + structured sidecar with per-turn
|
|
9966
|
-
* writeLog → writeSidecar → upsertGraph sequencing (ORDER IS LOAD-BEARING:
|
|
9967
|
-
* the memory side-write recovers each turn's ANSWER by re-reading the
|
|
9968
|
-
* transcript, so the log line must be flushed before the graph upsert), and
|
|
9969
|
-
* opt-in telemetry + end-of-session close.
|
|
9970
|
-
*
|
|
9971
|
-
* The returned object IS the caller-owned session handle — created here,
|
|
9972
|
-
* disposed by the caller (`close()`, idempotent), with NO process-global
|
|
9973
|
-
* state. Two handles never clobber each other (each owns its own
|
|
9974
|
-
* focus/lastAnswer/streams/sessionId).
|
|
9975
|
-
*
|
|
9976
|
-
* Returns { repo, config, graph, lexicon, memoryDir, moduleCount, version, sessionId,
|
|
9977
|
-
* logFile, sidecarFile, bannerLines, empty, focus, lastAnswer, turns, promptFor(),
|
|
9978
|
-
* turn(line), close() }. `turn(line)` runs one dispatched turn through runTurn and the
|
|
9979
|
-
* full sink sequencing, returning { answer, end, prompt }; `close()` is idempotent.
|
|
9980
|
-
*/
|
|
9981
|
-
export async function createSession({
|
|
9982
|
-
repoPath,
|
|
9983
|
-
graphPaths,
|
|
9984
|
-
configPath,
|
|
9985
|
-
source = defaultSource,
|
|
9986
|
-
env = process.env,
|
|
9987
|
-
cwd = process.cwd(),
|
|
9988
|
-
gitRoot = gitToplevel,
|
|
9989
|
-
ephemeral = false,
|
|
9990
|
-
narrate = false,
|
|
9991
|
-
// The storage-backend seam: "file" (default) keeps memoryDir a plain
|
|
9992
|
-
// repo-path string (Backend A). "memory" selects Backend B (zero disk I/O,
|
|
9993
|
-
// session-scoped). "sqlite" selects Backend C (a live node:sqlite
|
|
9994
|
-
// connection, lazily imported only when chosen). This is `tmct chat
|
|
9995
|
-
// --memory-backend <...>`'s already-resolved value; full precedence (this
|
|
9996
|
-
// param > TMCT_MEMORY_BACKEND env > tmct.toml > "default") resolved below.
|
|
9997
|
-
memoryBackend = null,
|
|
9998
|
-
} = {}) {
|
|
9999
|
-
// EPHEMERAL mode (--ephemeral, or TMCT_EPHEMERAL=1): read the target graph but
|
|
10000
|
-
// write NOTHING back into it. The shipped examples run this way so a demo never
|
|
10001
|
-
// dirties the committed code graph (`npm run example:mini` used to fold a session
|
|
10002
|
-
// into examples/*/.tmct/graph.json and rewrite it). We still read config.graphFile
|
|
10003
|
-
// for structure; only the WRITE base (logs, memory, sessions) is diverted to an OS
|
|
10004
|
-
// temp dir and the read-time graph upsert is suppressed.
|
|
10005
|
-
ephemeral = ephemeral || /^(1|true|yes)$/i.test(String(env.TMCT_EPHEMERAL || ""));
|
|
10006
|
-
// NARRATE mode (--narrate, or TMCT_NARRATE=1): start the session with
|
|
10007
|
-
// narrate mode already on. Session-scoped and mutable — `/narrate on|off`
|
|
10008
|
-
// flips it turn-to-turn (see `turn()` below). Default OFF.
|
|
10009
|
-
let narrateOn = narrate || /^(1|true|yes)$/i.test(String(env.TMCT_NARRATE || ""));
|
|
10010
|
-
// Graph resolution order (delegates to src/cli-args.mjs's
|
|
10011
|
-
// resolveRuntimeConfig): explicit --graph path(s) win outright; then --repo
|
|
10012
|
-
// (never silently redirected by env); then TMCT_GRAPH_FILE env; then
|
|
10013
|
-
// tmct.toml's graph_file at the resolved repo root; then git root; then cwd.
|
|
10014
|
-
// Defaults to the GIT ROOT, not raw cwd, so running from a nested package
|
|
10015
|
-
// dir doesn't index only that package.
|
|
10016
|
-
let repo;
|
|
10017
|
-
let config;
|
|
10018
|
-
// tmct.toml's normalized knobs, captured alongside `config` — used below
|
|
10019
|
-
// for the memory-backend precedence. `null` when no tmct.toml was readable.
|
|
10020
|
-
let toml = null;
|
|
10021
|
-
const explicitGraphs = (graphPaths || []).filter(Boolean);
|
|
10022
|
-
if (explicitGraphs.length) {
|
|
10023
|
-
repo = repoPath || gitRoot(cwd) || cwd;
|
|
10024
|
-
const resolvedGraphs = explicitGraphs.map((p) => resolve(cwd, p));
|
|
10025
|
-
config = resolvedGraphs.length > 1
|
|
10026
|
-
? { graphFile: resolvedGraphs[0], graphFiles: resolvedGraphs }
|
|
10027
|
-
: { graphFile: resolvedGraphs[0] };
|
|
10028
|
-
try {
|
|
10029
|
-
const argv = ["--repo", repo];
|
|
10030
|
-
if (configPath) argv.push("--config", configPath);
|
|
10031
|
-
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
10032
|
-
} catch { toml = null; }
|
|
10033
|
-
} else if (repoPath) {
|
|
10034
|
-
repo = repoPath;
|
|
10035
|
-
// env is deliberately withheld from resolveRuntimeConfig here (passed as
|
|
10036
|
-
// {}), so its own env-beats-repo-default tier can never fire.
|
|
10037
|
-
const argv = ["--repo", repoPath];
|
|
10038
|
-
if (configPath) argv.push("--config", configPath);
|
|
10039
|
-
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
10040
|
-
} else {
|
|
10041
|
-
const root = gitRoot(cwd);
|
|
10042
|
-
repo = root || cwd;
|
|
10043
|
-
const envGraph = env.TMCT_GRAPH_FILE && String(env.TMCT_GRAPH_FILE).trim();
|
|
10044
|
-
if (envGraph) {
|
|
10045
|
-
config = loadConfig(env, cwd);
|
|
10046
|
-
try {
|
|
10047
|
-
const argv = [];
|
|
10048
|
-
if (configPath) argv.push("--config", configPath);
|
|
10049
|
-
({ toml } = await resolveRuntimeConfig({ argv, cwd, env: {}, gitRoot }));
|
|
10050
|
-
} catch { toml = null; }
|
|
10051
|
-
} else {
|
|
10052
|
-
const argv = [];
|
|
10053
|
-
if (configPath) argv.push("--config", configPath);
|
|
10054
|
-
({ config, toml } = await resolveRuntimeConfig({ argv, cwd, env, gitRoot }));
|
|
10055
|
-
}
|
|
10056
|
-
}
|
|
10057
|
-
|
|
10058
|
-
// Ephemeral: keep config.graphFile pointing at the READ graph, but divert the
|
|
10059
|
-
// write base (repo → logs/memory/sessions) to a throwaway temp dir. The committed
|
|
10060
|
-
// target is never touched; the demo's memory simply doesn't persist across runs.
|
|
10061
|
-
if (ephemeral) repo = await mkdtemp(join(tmpdir(), "tmct-ephemeral-"));
|
|
10062
|
-
|
|
10063
|
-
// Load the graph once up front — the banner needs the module count, and focus/`it`
|
|
10064
|
-
// resolution and contextId threading need it in hand. A missing artifact loads as
|
|
10065
|
-
// the empty bootstrap graph (source.mjs) — the banner says so; never an error.
|
|
10066
|
-
const graph = parseEntities(await source.fetchEntities(config));
|
|
10067
|
-
const moduleCount = graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
10068
|
-
const { version } = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
10069
|
-
|
|
10070
|
-
// Resolve this handle's extension entries + bias table ONCE per session —
|
|
10071
|
-
// no new per-turn I/O. Failure-tolerated: a malformed tmct.toml degrades to
|
|
10072
|
-
// the shipped builtins with an empty bias table, never an error.
|
|
10073
|
-
let extEntries = null;
|
|
10074
|
-
let biasByBundle = {};
|
|
10075
|
-
try { ({ entries: extEntries, biasByBundle } = await resolveExtensions(repo)); }
|
|
10076
|
-
catch { extEntries = null; biasByBundle = {}; }
|
|
10077
|
-
|
|
10078
|
-
// Load this handle's lexicon once, MERGED with any active lexicon/pack
|
|
10079
|
-
// extension entries (ascending-bias merge order so a higher-bias bundle's
|
|
10080
|
-
// same-lemma entry wins deterministically). Failure-tolerated — a broken
|
|
10081
|
-
// lexicon degrades to the lazy per-turn load inside assertTurn.
|
|
10082
|
-
let lexicon = null;
|
|
10083
|
-
try {
|
|
10084
|
-
const { loadLexicon } = await import("./grammar/lexicon.mjs");
|
|
10085
|
-
const extra = extEntries ? await mergedLexiconExtra(extEntries, biasByBundle) : null;
|
|
10086
|
-
lexicon = loadLexicon(extra ?? undefined);
|
|
10087
|
-
} catch { lexicon = null; }
|
|
10088
|
-
|
|
10089
|
-
// Opt-in telemetry (default OFF → null → the sink's `tel?.record` is a no-op, and
|
|
10090
|
-
// nothing is written). The conversational session log + sidecar above stay the
|
|
10091
|
-
// authoritative chat record; this is the machine-readable query telemetry.
|
|
10092
|
-
const tel = createTelemetry({ env, config, surface: "chat" });
|
|
10093
|
-
|
|
10094
|
-
const sessionId = uuidv7();
|
|
10095
|
-
const logDir = join(repo, SESSION_LOG_DIR);
|
|
10096
|
-
const sessionsDir = join(repo, SESSIONS_DIR_REL);
|
|
10097
|
-
await mkdir(logDir, { recursive: true });
|
|
10098
|
-
await mkdir(sessionsDir, { recursive: true });
|
|
10099
|
-
const logFile = join(logDir, `session-${sessionId}.log`);
|
|
10100
|
-
const sidecarFile = join(sessionsDir, `session-${sessionId}.jsonl`);
|
|
10101
|
-
const stream = createWriteStream(logFile, { flags: "a" });
|
|
10102
|
-
const sidecar = createWriteStream(sidecarFile, { flags: "a" });
|
|
10103
|
-
// Awaited writes: each chunk is handed to the OS before the turn completes, so a
|
|
10104
|
-
// killed session keeps everything up to the last completed turn — in both files.
|
|
10105
|
-
const flush = (s, text) =>
|
|
10106
|
-
new Promise((resolve, reject) => s.write(text, (e) => (e ? reject(e) : resolve())));
|
|
10107
|
-
const writeLog = (text) => flush(stream, text);
|
|
10108
|
-
const writeSidecar = (obj) => flush(sidecar, JSON.stringify(obj) + "\n");
|
|
10109
|
-
|
|
10110
|
-
const startIso = new Date().toISOString();
|
|
10111
|
-
await writeLog(`# tmct chat ${version} — session started ${startIso} — repo ${repo}\n\n`);
|
|
10112
|
-
await writeSidecar({ type: "session", id: sessionId, started: startIso, repo, tmctVersion: version });
|
|
10113
|
-
|
|
10114
|
-
// Read-time graph upsert (sessions.mjs): after every turn, the session becomes /
|
|
10115
|
-
// stays a first-class Session individual in graph.json (crash-safe: turn n is in
|
|
10116
|
-
// the graph before turn n+1 runs). Best-effort — a re-index or vanished
|
|
10117
|
-
// artifact mid-session must degrade the recording, never kill the chat.
|
|
10118
|
-
const turnRecords = [];
|
|
10119
|
-
const upsertGraph = async (ended) => {
|
|
10120
|
-
if (ephemeral) return; // a demo/read-only session never writes back to the graph
|
|
10121
|
-
if (!turnRecords.length) return; // a zero-turn session never pollutes the graph
|
|
10122
|
-
try { await appendSessionToGraph(config.graphFile, { id: sessionId, started: startIso, ended, turns: turnRecords }); }
|
|
10123
|
-
catch { /* best-effort — see above */ }
|
|
10124
|
-
};
|
|
10125
|
-
|
|
10126
|
-
// `memoryDir` is the opaque token every memory/core.mjs call in this file
|
|
10127
|
-
// threads through unchanged. Backend A (default) keeps it the plain repo
|
|
10128
|
-
// string; Backend B/C swap in a handle instead. Precedence — CLI flag > env
|
|
10129
|
-
// > tmct.toml > default.
|
|
10130
|
-
const backendChoice = String(memoryBackend || env.TMCT_MEMORY_BACKEND || toml?.memory?.backend || "").trim().toLowerCase();
|
|
10131
|
-
// openMemoryBackend is the ONE shared resolver for this seam — init.mjs's
|
|
10132
|
-
// corpus seed calls the exact same function, so a repo's seeded facts and
|
|
10133
|
-
// its chat-taught facts always land in the same backend.
|
|
10134
|
-
const { openMemoryBackend } = await import("./memory/core.mjs");
|
|
10135
|
-
const { dir: memoryDir, close: closeMemoryStore } = await openMemoryBackend(repo, backendChoice);
|
|
10136
|
-
|
|
10137
|
-
const empty = graph.individuals.length === 0;
|
|
10138
|
-
// W3: FIRST RUN in a graph-less repo seeds a capped ConceptNet slice into
|
|
10139
|
-
// .tmct/memory so vocabulary questions ("what is a cache?") have something
|
|
10140
|
-
// honest to stand on from turn one. Guarded three ways: only the empty
|
|
10141
|
-
// bootstrap (a fixture/provider graph never seeds), only once (the marker),
|
|
10142
|
-
// and never when TMCT_NO_SEED=1 opts out.
|
|
10143
|
-
//
|
|
10144
|
-
// Known Backend B/C limitation: seedBootstrapMemory/hasSeededVocabulary and
|
|
10145
|
-
// sessions.mjs's own per-turn utterance mirror all resolve their marker
|
|
10146
|
-
// file / repoDir directly off the STRING `repo` path, not the actual
|
|
10147
|
-
// Backend B/C handle — so W3 seeding is skipped for a non-default backend,
|
|
10148
|
-
// and a Backend B/C session's Utterance/Session individuals still land in
|
|
10149
|
-
// an ordinary Backend-A .tmct/memory/graph.json. Taught FACTS themselves
|
|
10150
|
-
// are unaffected: only the conversational transcript mirror leaks onto
|
|
10151
|
-
// disk, never the facts.
|
|
10152
|
-
let seeded = null;
|
|
10153
|
-
if (empty && backendChoice === "" && String(env.TMCT_NO_SEED || "") !== "1") {
|
|
10154
|
-
seeded = await seedBootstrapMemory(repo, env);
|
|
10155
|
-
}
|
|
10156
|
-
// vocabHint: computed ONCE per session (not per-turn — see runTurn's own
|
|
10157
|
-
// per-call fallback for direct/library callers). `seeded` is only truthy when
|
|
10158
|
-
// THIS run performed the seeding; a repo seeded by an EARLIER run (or `tmct
|
|
10159
|
-
// init`) still needs the marker check, so this covers both — see
|
|
10160
|
-
// hasSeededVocabulary's docblock.
|
|
10161
|
-
const vocabSeeded = Boolean(seeded) || (await hasSeededVocabulary(repo));
|
|
10162
|
-
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
10163
|
-
// #3/#5: 0 modules means no code graph to answer structure questions from —
|
|
10164
|
-
// whether the graph file is absent (empty bootstrap) OR present with no code
|
|
10165
|
-
// entities (the degenerate trap). Both get orienting, non-over-promising banner
|
|
10166
|
-
// + greeting messaging rather than a silent dead-end.
|
|
10167
|
-
const noCodeGraph = moduleCount === 0;
|
|
10168
|
-
const bannerLines = [
|
|
10169
|
-
noCodeGraph
|
|
10170
|
-
// No code graph: honest, orienting messaging — never an error before the prompt.
|
|
10171
|
-
? `tmct chat — ${repo} — no code graph loaded — ${empty ? "starting empty" : "graph has no code entities"}; ` +
|
|
10172
|
-
`the conversation is remembered to ${DEFAULT_GRAPH_REL} — log ${logFile}`
|
|
10173
|
-
: `tmct chat — ${repo} — ${moduleCount} module(s) — log ${logFile}`,
|
|
10174
|
-
// the honest seed line appears ONLY on the run that actually seeded — the count
|
|
10175
|
-
// is the TOTAL appended, split into the curated SEON ontology + the ConceptNet band
|
|
10176
|
-
// (+ any other active extension bundle, e.g. an activated tier-2 corpus).
|
|
10177
|
-
...(seeded ? [seedBannerLine(seeded)] : []),
|
|
10178
|
-
// no code graph → point at how to GET one (a graph producer / --repo / the shipped
|
|
10179
|
-
// example), and at what IS answerable now — `vocabHint` is only ever a term
|
|
10180
|
-
// confirmed to resolve in THIS session's actual seed state (see vocabExampleHint),
|
|
10181
|
-
// never a hardcoded example that might not have been seeded. tmct reads graphs;
|
|
10182
|
-
// it never indexes code itself.
|
|
10183
|
-
...(noCodeGraph ? [`for code structure, point me at a .tmct/graph.json with --repo <path> or try \`npm run example:mini\` (tmct reads graphs, it doesn't index code). ${vocabHint}`] : []),
|
|
10184
|
-
"pass --repo <path> to target a different repo",
|
|
10185
|
-
"ask a question, or /help for commands (/stats for an overview) — /exit to leave",
|
|
10186
|
-
];
|
|
10187
|
-
|
|
10188
|
-
let turns = 0;
|
|
10189
|
-
let focus = null; // the current focus entity ({id,label}) — threaded turn to turn
|
|
10190
|
-
let last = null; // the last dispatched answer ({query,answer,detail}) — why/say-more re-renders it
|
|
10191
|
-
let planState = null; // the in-progress plan (goals/moves/cursor) — cleared by completion or a fresh goal, never by an aside
|
|
10192
|
-
let closed = false;
|
|
10193
|
-
|
|
10194
|
-
return {
|
|
10195
|
-
repo, config, graph, lexicon, memoryDir, moduleCount, version, sessionId,
|
|
10196
|
-
logFile, sidecarFile, bannerLines, empty, biasByBundle,
|
|
10197
|
-
// Mutable between-turn state — read-only to the caller, so a shell can render the
|
|
10198
|
-
// prompt/expand-hint without reaching into runTurn's threading.
|
|
10199
|
-
get focus() { return focus; },
|
|
10200
|
-
get lastAnswer() { return last; },
|
|
10201
|
-
get planState() { return planState; },
|
|
10202
|
-
get turns() { return turns; },
|
|
10203
|
-
get narrate() { return narrateOn; },
|
|
10204
|
-
promptFor: () => promptFor(focus),
|
|
10205
|
-
|
|
10206
|
-
/** One dispatched turn through the FULL sink sequencing (writeLog → writeSidecar
|
|
10207
|
-
* → telemetry → upsertGraph, in that exact order). Returns { answer, end, prompt }.
|
|
10208
|
-
* A throwing runTurn must never abort the session: a piped/non-interactive
|
|
10209
|
-
* driver has no other chance to see this turn's answer. */
|
|
10210
|
-
async turn(line) {
|
|
10211
|
-
let result;
|
|
10212
|
-
try {
|
|
10213
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle, planState });
|
|
10214
|
-
} catch (e) {
|
|
10215
|
-
const ts = new Date().toISOString();
|
|
10216
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
10217
|
-
await writeLog(`${ts}\n> ${line}\nerror: ${message}\n`);
|
|
10218
|
-
const errorRecord = { type: "error", ts, query: line, error: message };
|
|
10219
|
-
await writeSidecar(errorRecord);
|
|
10220
|
-
turnRecords.push(errorRecord);
|
|
10221
|
-
turns += 1;
|
|
10222
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, prompt: promptFor(focus) };
|
|
10223
|
-
}
|
|
10224
|
-
const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate } = result;
|
|
10225
|
-
focus = nextFocus;
|
|
10226
|
-
last = nextLast;
|
|
10227
|
-
if ("planState" in result) planState = result.planState;
|
|
10228
|
-
// /narrate on|off (runCommand) rides the turn RESULT the same way a focus
|
|
10229
|
-
// update does — apply it to this handle's session-scoped state.
|
|
10230
|
-
if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
|
|
10231
|
-
await writeLog(logLines.join("\n") + "\n");
|
|
10232
|
-
await writeSidecar(record);
|
|
10233
|
-
turnRecords.push(record);
|
|
10234
|
-
// One telemetry line per dispatched turn (OFF by default → no-op). query.raw is
|
|
10235
|
-
// the user's line; `tool` the slash-command if any; count the cited entity ids.
|
|
10236
|
-
tel?.record({
|
|
10237
|
-
tool: record.command,
|
|
10238
|
-
query: { raw: line },
|
|
10239
|
-
response: { count: (record.answeredIds || []).length, node_ids: record.answeredIds || [] },
|
|
10240
|
-
});
|
|
10241
|
-
await upsertGraph(record.ts);
|
|
10242
|
-
turns += 1;
|
|
10243
|
-
return { answer, end: Boolean(end), prompt: promptFor(focus), plan: result.plan ?? null };
|
|
10244
|
-
},
|
|
10245
|
-
|
|
10246
|
-
/** End-of-session close: end lines in both artifacts, the final graph upsert
|
|
10247
|
-
* (which also triggers the memory fold), stream flush, the Backend C
|
|
10248
|
-
* connection close (a no-op for Backend A/B). Idempotent. */
|
|
10249
|
-
async close() {
|
|
10250
|
-
if (closed) return;
|
|
10251
|
-
closed = true;
|
|
10252
|
-
const endIso = new Date().toISOString();
|
|
10253
|
-
await writeLog(`${endIso}\n> /exit\nsession end ${endIso}\n`);
|
|
10254
|
-
await writeSidecar({ type: "end", ts: endIso });
|
|
10255
|
-
await upsertGraph(endIso);
|
|
10256
|
-
await new Promise((resolve) => stream.end(resolve));
|
|
10257
|
-
await new Promise((resolve) => sidecar.end(resolve));
|
|
10258
|
-
await closeMemoryStore();
|
|
10259
|
-
},
|
|
10260
|
-
};
|
|
10261
|
-
}
|
|
10262
|
-
|
|
10263
|
-
/**
|
|
10264
|
-
* The interactive readline shell over createSession — the `--plain` surface and
|
|
10265
|
-
* the scripted-test surface. Streams are injectable so tests run sessions
|
|
10266
|
-
* without a TTY. A repo with NO graph artifact is not an error: the session
|
|
10267
|
-
* starts from the empty bootstrap graph (the banner says so honestly) and the
|
|
10268
|
-
* first turn's fold-in creates .tmct/graph.json from the conversation itself.
|
|
10269
|
-
* Returns { logFile, sidecarFile, turns } once the session ends.
|
|
10270
|
-
*/
|
|
10271
|
-
export async function runChat({
|
|
10272
|
-
repoPath,
|
|
10273
|
-
graphPaths,
|
|
10274
|
-
configPath,
|
|
10275
|
-
input = process.stdin,
|
|
10276
|
-
output = process.stdout,
|
|
10277
|
-
source = defaultSource,
|
|
10278
|
-
env = process.env,
|
|
10279
|
-
cwd = process.cwd(),
|
|
10280
|
-
gitRoot = gitToplevel,
|
|
10281
|
-
ephemeral = false,
|
|
10282
|
-
narrate = false,
|
|
10283
|
-
memoryBackend = null,
|
|
10284
|
-
} = {}) {
|
|
10285
|
-
// createSession's first-run seed (~2-3s) produces ZERO output until it fully
|
|
10286
|
-
// resolves, which otherwise reads as `npm run chat` hanging with total silence.
|
|
10287
|
-
output.write("tmct — starting…\n");
|
|
10288
|
-
const session = await createSession({ repoPath, graphPaths, configPath, source, env, cwd, gitRoot, ephemeral, narrate, memoryBackend });
|
|
10289
|
-
|
|
10290
|
-
const dim = (s) => (env.NO_COLOR || !output.isTTY ? s : `\x1b[2m${s}\x1b[0m`);
|
|
10291
|
-
for (const line of session.bannerLines) output.write(dim(line) + "\n");
|
|
10292
|
-
|
|
10293
|
-
const rl = createInterface({ input, output, prompt: PROMPT });
|
|
10294
|
-
rl.on("SIGINT", () => rl.close()); // Ctrl+C behaves like /exit (clean close, log flushed)
|
|
10295
|
-
let closed = false;
|
|
10296
|
-
rl.on("close", () => { closed = true; });
|
|
10297
|
-
const prompt = () => { if (!closed) rl.prompt(); }; // input may end while a turn is in flight
|
|
10298
|
-
|
|
10299
|
-
prompt();
|
|
10300
|
-
// try/finally: session.close() is the ONLY code path that writes end-markers and
|
|
10301
|
-
// flushes the log/sidecar write streams (stream.end()/sidecar.end()) — an
|
|
10302
|
-
// unhandled throw anywhere in the loop body must still reach it, or a
|
|
10303
|
-
// piped/non-interactive run can lose buffered writes outright, not just this
|
|
10304
|
-
// turn's data. session.turn() now catches its own errors (see createSession),
|
|
10305
|
-
// so this is defense in depth for anything else that might throw here.
|
|
10306
|
-
try {
|
|
10307
|
-
for await (const raw of rl) { // Ctrl+D / closed stdin ends the iteration cleanly
|
|
10308
|
-
const line = raw.trim();
|
|
10309
|
-
if (line === "/exit") break;
|
|
10310
|
-
if (line) {
|
|
10311
|
-
const { answer, end, prompt: nextPrompt } = await session.turn(line);
|
|
10312
|
-
output.write(answer + "\n");
|
|
10313
|
-
rl.setPrompt(nextPrompt);
|
|
10314
|
-
if (end) break; // a conversational "bye"/"goodbye" — clean end, same as /exit
|
|
10315
|
-
}
|
|
10316
|
-
prompt();
|
|
10317
|
-
}
|
|
10318
|
-
} finally {
|
|
10319
|
-
rl.close();
|
|
10320
|
-
await session.close();
|
|
10321
|
-
}
|
|
10322
|
-
return { logFile: session.logFile, sidecarFile: session.sidecarFile, turns: session.turns };
|
|
10323
|
-
}
|