@polycode-projects/the-mechanical-code-talker 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ROADMAP.md +80 -31
- package/data/templates/responses.jsonl +1 -1
- package/package.json +1 -1
- package/src/ask.mjs +138 -14
- package/src/chat.mjs +369 -20
- package/src/codegraph.mjs +109 -1
- package/src/interpret/merge.mjs +16 -1
- package/src/interpret/normalize.mjs +144 -2
- package/src/interpret/pipeline.mjs +18 -3
- package/src/interpret/strategies/ace.mjs +49 -0
- package/src/memory/core.mjs +8 -1
- package/src/memory/fold.mjs +0 -0
- package/src/memory/trust.mjs +8 -4
- package/src/router/call-validator.mjs +45 -0
- package/src/router/goal-reasoner.mjs +364 -0
- package/src/router/guardrail.mjs +1 -1
- package/src/router/planner.mjs +22 -1
- package/src/router/resolver.mjs +49 -11
- package/src/router/set-algebra.mjs +31 -0
package/src/chat.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
45
45
|
import { spawnSync } from "node:child_process";
|
|
46
46
|
import { dispatchTool } from "./server.mjs";
|
|
47
47
|
import { loadConfig, DEFAULT_GRAPH_REL } from "./config.mjs";
|
|
48
|
-
import { parseEntities } from "./codegraph.mjs";
|
|
48
|
+
import { parseEntities, edgesOfKind, renderAuthorCard, renderAuthorTouches, renderCommitAuthor } from "./codegraph.mjs";
|
|
49
49
|
import { SESSIONS_DIR_REL, appendSessionToGraph } from "./sessions.mjs";
|
|
50
50
|
import { uuidv7 } from "./uuid.mjs";
|
|
51
51
|
import { createTelemetry } from "./telemetry.mjs";
|
|
@@ -518,10 +518,48 @@ export function moduleCountOf(graph) {
|
|
|
518
518
|
* orientation/greeting only fires when we actually hold an empty graph. */
|
|
519
519
|
const noCodeGraph = (graph) => !!graph && moduleCountOf(graph) === 0;
|
|
520
520
|
|
|
521
|
+
/** LIVE orientation examples (0.8.2 WS4 wall kindness): the example queries on the
|
|
522
|
+
* orientation card name entities from the LOADED graph — the sorted-first Module
|
|
523
|
+
* label and the sorted-first Function/Method label, deterministically — so a
|
|
524
|
+
* stranger who types them verbatim gets a real answer on ANY graph (the old
|
|
525
|
+
* hardcoded walk.mjs/buildContextBundle examples miss on every non-tmct graph).
|
|
526
|
+
* A null (unknown) graph keeps the generic pair byte-for-byte. */
|
|
527
|
+
function orientationExamples(graph) {
|
|
528
|
+
const generic = { example1: "walk.mjs", example2: "buildContextBundle" };
|
|
529
|
+
if (!graph || !Array.isArray(graph.individuals)) return generic;
|
|
530
|
+
const minLabel = (labels) => {
|
|
531
|
+
let best = null;
|
|
532
|
+
for (const l of labels) { const s = String(l || ""); if (s && (best === null || s < best)) best = s; }
|
|
533
|
+
return best;
|
|
534
|
+
};
|
|
535
|
+
// "which modules import <example1>" must ANSWER, so prefer a module that IS
|
|
536
|
+
// imported (an `imports` edge object); any module label as the fallback.
|
|
537
|
+
const importedMod = minLabel(edgesOfKind(graph, "imports")
|
|
538
|
+
.filter((e) => (graph.byId?.get?.(e.object)?.class || "") === "Module")
|
|
539
|
+
.map((e) => e.objectLabel || ""));
|
|
540
|
+
const anyMod = minLabel(graph.individuals.filter((i) => (i.class || "") === "Module").map((i) => i.label));
|
|
541
|
+
const example1 = importedMod ?? anyMod ?? generic.example1;
|
|
542
|
+
// "what calls <example2>" must ANSWER, so prefer a Function/Method that HAS a
|
|
543
|
+
// recorded caller (a `callsSymbol` edge object with a real individual), then a
|
|
544
|
+
// module-coarse called module, then any callable label, then example1.
|
|
545
|
+
const calledSym = minLabel(edgesOfKind(graph, "callsSymbol")
|
|
546
|
+
.filter((e) => ["Function", "Method"].includes(graph.byId?.get?.(e.object)?.class || ""))
|
|
547
|
+
.map((e) => e.objectLabel || graph.byId?.get?.(e.object)?.label || ""));
|
|
548
|
+
const calledMod = minLabel(edgesOfKind(graph, "calls")
|
|
549
|
+
.filter((e) => (graph.byId?.get?.(e.object)?.class || "") === "Module")
|
|
550
|
+
.map((e) => e.objectLabel || ""));
|
|
551
|
+
const anyFn = minLabel(graph.individuals
|
|
552
|
+
.filter((i) => i.class === "Function" || i.class === "Method").map((i) => i.label));
|
|
553
|
+
const example2 = calledSym ?? calledMod ?? anyFn ?? example1;
|
|
554
|
+
return { example1, example2 };
|
|
555
|
+
}
|
|
556
|
+
|
|
521
557
|
/** The orientation surface, module-aware: the empty variant (→ --repo/tmct init +
|
|
522
|
-
* seeded vocabulary) when there's no code graph, the standard one
|
|
558
|
+
* seeded vocabulary) when there's no code graph, the standard one (with live
|
|
559
|
+
* {example1}/{example2} query examples from the loaded graph) otherwise. */
|
|
523
560
|
function orientationAnswer(templates, graph) {
|
|
524
|
-
|
|
561
|
+
if (noCodeGraph(graph)) return tRender(templates, T_ORIENTATION_EMPTY) ?? TEMPLATES_UNAVAILABLE;
|
|
562
|
+
return tRender(templates, T_ORIENTATION, orientationExamples(graph)) ?? TEMPLATES_UNAVAILABLE;
|
|
525
563
|
}
|
|
526
564
|
|
|
527
565
|
/** A dynamic orientation string for the meta/self lane: a /stats-style overview
|
|
@@ -589,19 +627,70 @@ export function shortMissHint(query) {
|
|
|
589
627
|
/** The exact opening of the engine's full grammar-wall miss — the ONLY miss the
|
|
590
628
|
* short-miss rewrites. Receipt-bearing misses (honest empties, unresolved terms,
|
|
591
629
|
* the empty-graph bootstrap note, compositional misses) never match, so their
|
|
592
|
-
* specific wording + traversal receipts stand.
|
|
593
|
-
|
|
630
|
+
* specific wording + traversal receipts stand. Exported: the recall hygiene
|
|
631
|
+
* (bestQaPair) reuses it so a folded wall answer is never replayed as a memory
|
|
632
|
+
* (fold.mjs carries its own local copy — the memory layer stays decoupled). */
|
|
633
|
+
export const WALL_MISS_RE = /^couldn't parse this as a graph question\. Try:/;
|
|
594
634
|
|
|
595
635
|
// #2 INTENT LANE — MEMORY/TEACH. "remember that X is a Y", "note that …", or a
|
|
596
636
|
// bare "X is a Y" declarative the graph parser couldn't handle → route to the
|
|
597
637
|
// assert/memory path; when it can't be stored, say what CAN be remembered
|
|
598
638
|
// (LOUD, the working shape) — never the grammar wall, never a silent data loss.
|
|
639
|
+
// 0.8.2 widens the lane with two NATURAL frames, both reified via appendFact
|
|
640
|
+
// with a distinct teach:chat provenance (its own "teach" trust prior):
|
|
641
|
+
// - "remember/note that <X> is <adjective>" → an mgx:hasProperty fact —
|
|
642
|
+
// ONLY under the explicit wrapper (a bare "X is deprecated" is never
|
|
643
|
+
// silently swallowed);
|
|
644
|
+
// - "<Name> owns/maintains <X>" (bare declarative or wrapped) → an
|
|
645
|
+
// mgx:ownedBy fact, read back by "who owns <X>" (factReadBack).
|
|
599
646
|
const TEACH_RE = /^(?:please\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi)\b[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
600
647
|
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+ (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
601
648
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
602
649
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
603
650
|
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
604
651
|
|
|
652
|
+
// The teach lane's fact predicates (rendered via FACT_PREDICATE_PHRASES).
|
|
653
|
+
const OWNED_BY_PREDICATE = "mgx:ownedBy";
|
|
654
|
+
const HAS_PROPERTY_PREDICATE = "mgx:hasProperty";
|
|
655
|
+
|
|
656
|
+
/** "<Name> owns/maintains <X>" — the ownership teach declarative. <Name> is one
|
|
657
|
+
* or two name tokens, <X> one code-ish token (a path, a file, a symbol). The
|
|
658
|
+
* BARE form additionally requires a Capitalized name (see teachLane), so
|
|
659
|
+
* ordinary lowercase prose never lands a fact without the explicit wrapper. */
|
|
660
|
+
const OWNS_TEACH_RE = /^([A-Za-z][\w'-]*(?:\s+[A-Z][\w'-]*)?)\s+(?:owns|maintains)\s+(\S+?)[.!?]*$/;
|
|
661
|
+
|
|
662
|
+
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
663
|
+
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
664
|
+
* membership shape (that stays the ACE grammar's), so "remember that cache is
|
|
665
|
+
* a store" still lands as rdfs:subClassOf, not a property. */
|
|
666
|
+
const TEACH_PROPERTY_RE = /^(?:every\s+|each\s+|all\s+|the\s+)?(.+?)\s+(?:is|are)\s+(?!an?\b|the\b)([A-Za-z][\w-]*)$/i;
|
|
667
|
+
|
|
668
|
+
/** The teach lane's provenance tag — mirrors grammar/assert.mjs's provenanceTag
|
|
669
|
+
* shape under a distinct "teach:" family, so a taught fact is auditable apart
|
|
670
|
+
* from the ACE-parsed asserts: teach:chat:<sessionId>@<ts>. core.mjs maps the
|
|
671
|
+
* tag to a "teach" Source (trust prior in memory/trust.mjs). */
|
|
672
|
+
const teachProvenanceTag = (sessionId, ts) => `teach:chat${sessionId ? `:${sessionId}` : ""}${ts ? `@${ts}` : ""}`;
|
|
673
|
+
|
|
674
|
+
/** Reify one teach-lane fact + confirm (shared by the property and ownership
|
|
675
|
+
* frames). Lazy + failure-tolerated: a write failure degrades to null (the
|
|
676
|
+
* teach-miss text stands), never a crash. */
|
|
677
|
+
async function teachFact(memoryDir, sessionId, { subject, predicate, object }) {
|
|
678
|
+
try {
|
|
679
|
+
const { appendFact, normFactTerm } = await import("./memory/core.mjs");
|
|
680
|
+
const s = normFactTerm(subject);
|
|
681
|
+
const o = normFactTerm(object);
|
|
682
|
+
if (!s || !o) return null;
|
|
683
|
+
await appendFact(memoryDir, {
|
|
684
|
+
subject: s, predicate, object: o,
|
|
685
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
686
|
+
});
|
|
687
|
+
const phrase = FACT_PREDICATE_PHRASES[predicate] || predicate;
|
|
688
|
+
return { text: `noted — remembered: ${s} ${phrase} ${o}`, via: "assert", miss: false };
|
|
689
|
+
} catch {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
605
694
|
/** Sentence forms to try asserting for a teach payload: the payload as-is, and
|
|
606
695
|
* (if it carries no determiner) its "every …" universal — the ACE-OWL shape the
|
|
607
696
|
* grammar actually lands. */
|
|
@@ -619,9 +708,23 @@ function teachSuggestion(payload) {
|
|
|
619
708
|
|
|
620
709
|
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
621
710
|
const raw = String(query).trim();
|
|
622
|
-
let payload = null;
|
|
623
711
|
const m = raw.match(TEACH_RE);
|
|
624
|
-
|
|
712
|
+
const wrapped = m ? m[1].trim() : null;
|
|
713
|
+
|
|
714
|
+
// OWNERSHIP — "<Name> owns/maintains <X>", bare or remember-wrapped. The bare
|
|
715
|
+
// form is double-gated: a Capitalized name AND no interrogative lead, so the
|
|
716
|
+
// "who owns <X>" READ question and ordinary prose never land a fact here.
|
|
717
|
+
const ownSrc = wrapped ?? raw.replace(/[.!?]+\s*$/, "");
|
|
718
|
+
const own = ownSrc.match(OWNS_TEACH_RE);
|
|
719
|
+
if (own && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && (wrapped || /^[A-Z]/.test(own[1]))) {
|
|
720
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
721
|
+
subject: own[2], predicate: OWNED_BY_PREDICATE, object: own[1],
|
|
722
|
+
});
|
|
723
|
+
if (stored) return stored;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
let payload = null;
|
|
727
|
+
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
625
728
|
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw)) payload = raw;
|
|
626
729
|
if (!payload) return null;
|
|
627
730
|
// Try to store it (a live session provides the write target). assertTurn returns
|
|
@@ -631,6 +734,19 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
631
734
|
const stored = await assertTurn(cand, { memoryDir, sessionId, focus: null, lexicon });
|
|
632
735
|
if (stored) return { text: stored.answer, via: "assert", miss: false };
|
|
633
736
|
}
|
|
737
|
+
// PROPERTY teach — "remember/note that <X> is <adjective>": wrapper-REQUIRED
|
|
738
|
+
// (a bare "X is deprecated" is never silently reified), and only after the
|
|
739
|
+
// ACE grammar declined (unknown words / not the membership shape), so a
|
|
740
|
+
// wrapped "X is a Y" over known lexicon still lands as rdfs:subClassOf.
|
|
741
|
+
if (wrapped) {
|
|
742
|
+
const prop = wrapped.match(TEACH_PROPERTY_RE);
|
|
743
|
+
if (prop) {
|
|
744
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
745
|
+
subject: prop[1], predicate: HAS_PROPERTY_PREDICATE, object: prop[2],
|
|
746
|
+
});
|
|
747
|
+
if (stored) return stored;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
634
750
|
}
|
|
635
751
|
const suggestion = teachSuggestion(payload);
|
|
636
752
|
const did = suggestion && suggestion !== payload.toLowerCase() ? ` Did you mean: "${suggestion}"?` : "";
|
|
@@ -647,7 +763,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null }) {
|
|
|
647
763
|
// reference with no graph entity or predicate, so real graph queries ("what does X
|
|
648
764
|
// import", the meta "what does imports mean", "what did i ask before") never match.
|
|
649
765
|
const WHAT_KNOW_RE = /^what\s+(?:do\s+you|d'?you)\s+know(?:\s+so\s+far)?$/;
|
|
650
|
-
|
|
766
|
+
// 0.8.2 WS4 wall kindness (c): the most likely stranger openers — "what does this
|
|
767
|
+
// app/codebase do", "what is this app (for)" — join the orientation lane, so a
|
|
768
|
+
// first-touch question gets the live overview instead of the grammar wall.
|
|
769
|
+
const META_ORIENT_RE = /^(?:what(?:'s| is| are)?\s+this(?:\s+(?:app|codebase|repo|repository|project|code|thing))?|what\s+(?:codebase|repo|repository|project)\s+is\s+this|what\s+does\s+(?:this|the)\s+(?:app|code|codebase|project|repo)\s+do|what\s+is\s+(?:this|the)\s+app(?:\s+for)?|what\s+am\s+i\s+looking\s+at|what\s+is\s+tmct|how\s+do\s+i\s+(?:start|begin|get\s+started|get\s+going|load\s+(?:my\s+)?code|index\s+(?:my\s+)?(?:code|repo|repository)|use\s+(?:this|you|tmct))|where\s+do\s+i\s+(?:start|begin))$/;
|
|
651
770
|
|
|
652
771
|
/** A SHORT memory summary (never a fact dump) for the bare "what do you know". */
|
|
653
772
|
async function memorySummary(memoryDir, graph) {
|
|
@@ -670,9 +789,112 @@ async function metaLane(query, { graph, memoryDir }) {
|
|
|
670
789
|
return { text: await memorySummary(memoryDir, graph), via: "meta" };
|
|
671
790
|
}
|
|
672
791
|
if (META_ORIENT_RE.test(q)) return { text: orientationText(graph), via: "meta" };
|
|
792
|
+
// 0.8.2 WS4: the sha-authorship form ("who authored a1b2c3d") can be as short as
|
|
793
|
+
// THREE words, which the conversational-orientation branch (step 2) would grab
|
|
794
|
+
// before the author step (4b) is reached — a bare hex sha is not "code-ish" to
|
|
795
|
+
// isConversational. The form is closed + unambiguous (7-40 hex chars), so the
|
|
796
|
+
// meta lane delegates it to the author lane here. Unknown/ambiguous shas return
|
|
797
|
+
// null and fall through unchanged.
|
|
798
|
+
if (AUTHOR_SHA_RE.test(q)) return authorLane(q, { graph });
|
|
673
799
|
return null;
|
|
674
800
|
}
|
|
675
801
|
|
|
802
|
+
// #4 INTENT LANE — AUTHOR (0.8.2 WS4). Author is a Commit ATTRIBUTE (key
|
|
803
|
+
// "author"/mgx:commitAuthor), never an individual, so "who is Grace Hopper" can't
|
|
804
|
+
// resolve as an entity — this lane reads the attribute through codegraph.mjs's
|
|
805
|
+
// authorIndex renderers instead. WOULD-MISS gated (the ladder consults it only on
|
|
806
|
+
// a miss) + CLOSED whole-line regexes + an EXACT case-insensitive author-name hit:
|
|
807
|
+
// an unknown name renders null here and falls through to the ordinary honest miss
|
|
808
|
+
// (never a guess, never a hijacked graph query).
|
|
809
|
+
const AUTHOR_NAME_SRC = "([A-Za-z][\\w'.-]*(?:\\s+[A-Za-z][\\w'.-]*){0,3})";
|
|
810
|
+
const AUTHOR_WHO_IS_RE = new RegExp(`^who\\s+is\\s+${AUTHOR_NAME_SRC}$`, "i");
|
|
811
|
+
const AUTHOR_TOUCHED_RE = new RegExp(
|
|
812
|
+
`^what\\s+(?:did|has)\\s+${AUTHOR_NAME_SRC}\\s+(?:touch(?:ed)?|chang(?:e|ed)|work(?:ed)?\\s+on|commit(?:ted)?)$`, "i");
|
|
813
|
+
// The sha authorship forms — the interpret layer no longer rewrites these (WS2 guard).
|
|
814
|
+
const AUTHOR_SHA_RE = /^who\s+(?:authored|wrote|is\s+the\s+author\s+of)\s+(?:commit\s+)?([0-9a-fA-F]{7,40})$/i;
|
|
815
|
+
|
|
816
|
+
function authorLane(query, { graph }) {
|
|
817
|
+
if (!graph) return null;
|
|
818
|
+
const q = String(query).trim().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
819
|
+
const sha = q.match(AUTHOR_SHA_RE);
|
|
820
|
+
if (sha) {
|
|
821
|
+
const line = renderCommitAuthor(graph, sha[1]);
|
|
822
|
+
return line ? { text: line, via: "author" } : null;
|
|
823
|
+
}
|
|
824
|
+
const touched = q.match(AUTHOR_TOUCHED_RE);
|
|
825
|
+
if (touched) {
|
|
826
|
+
const text = renderAuthorTouches(graph, touched[1]);
|
|
827
|
+
if (text) return { text, via: "author" };
|
|
828
|
+
}
|
|
829
|
+
const who = q.match(AUTHOR_WHO_IS_RE);
|
|
830
|
+
if (who) {
|
|
831
|
+
const text = renderAuthorCard(graph, who[1]);
|
|
832
|
+
if (text) return { text, via: "author" };
|
|
833
|
+
}
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// #5(d,e)/#8 CAPABILITY NUDGES (0.8.2 WS4) — closed regexes on the would-miss path
|
|
838
|
+
// for asks the graph genuinely cannot answer: risk scoring, code opinions, writing
|
|
839
|
+
// code, and motive-"why". Each renders an HONEST wall pointing at the nearest real
|
|
840
|
+
// query shapes. These REMAIN recorded as misses (recordMiss stays TRUE): a
|
|
841
|
+
// capability wall must never fold into a recallable answer — WS3's fold hygiene is
|
|
842
|
+
// the second belt, this gate is the braces.
|
|
843
|
+
const RISK_NUDGE_RE = /\brisk(?:iest|y)\b/i;
|
|
844
|
+
const OPINION_ADJ_SRC =
|
|
845
|
+
"(?:good|bad|clean|messy|ugly|nice|great|terrible|awful|solid|elegant|readable|maintainable|well[- ]written|well[- ]structured|spaghetti|ok|okay|decent|healthy)";
|
|
846
|
+
const OPINION_NUDGE_RE = new RegExp(`^is\\s+(?:this|the)\\s+code(?:base)?\\s+(?:any\\s+)?${OPINION_ADJ_SRC}\\b`, "i");
|
|
847
|
+
// Imperative "write code for me": a leading make/write/create/add/generate/
|
|
848
|
+
// implement/fix/refactor (optionally "can you …"-wrapped) aimed at a code noun (or
|
|
849
|
+
// a focus-resolvable "it"). "tell" is deliberately NOT a verb here — "tell me a
|
|
850
|
+
// joke" (the graded hm-joke case) must keep its ordinary honest miss.
|
|
851
|
+
const IMPERATIVE_NUDGE_RE =
|
|
852
|
+
/^(?:please\s+)?(?:(?:can|could|would|will)\s+you\s+(?:please\s+)?)?(?:make|write|create|add|generate|implement|fix|refactor)\b(?=.*\b(?:tests?|code|functions?|methods?|modules?|class(?:es)?|files?|it)\b)/i;
|
|
853
|
+
const WHY_UNTESTED_RE = /^why\s+(?:is|are)(?:n't|\s+not)?\s+(.+?)\s+(?:untested|not\s+tested|uncovered)$/i;
|
|
854
|
+
|
|
855
|
+
/** The <name> a nudge shows: the focus label when the query leans on a pronoun (or
|
|
856
|
+
* gave us nothing better), else the captured subject; "<name>" as the placeholder. */
|
|
857
|
+
function nudgeName(captured, focus) {
|
|
858
|
+
const c = String(captured || "").trim();
|
|
859
|
+
if (c && !/^(?:it|this|that|they|them)$/i.test(c)) return c;
|
|
860
|
+
return focus?.label || "<name>";
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/** The capability-nudge answer for a would-miss query, or null. Order matters only
|
|
864
|
+
* for the opinion gate: it must fire BEFORE the short-miss's "is a <thing> a
|
|
865
|
+
* <kind>" membership hint would (the caller runs this whole step before the
|
|
866
|
+
* short-miss rewrite). */
|
|
867
|
+
function nudgeAnswer(query, focus) {
|
|
868
|
+
const q = String(query).trim().replace(/[?.!]+$/, "").replace(/\s+/g, " ");
|
|
869
|
+
if (OPINION_NUDGE_RE.test(q)) {
|
|
870
|
+
const name = focus?.label || "<name>";
|
|
871
|
+
return "I don't hold opinions — I read structure, not quality. I can show what an opinion would rest on: "
|
|
872
|
+
+ `/stats (shape), "untested modules" (coverage), "who touched ${name}" (churn).`;
|
|
873
|
+
}
|
|
874
|
+
if (RISK_NUDGE_RE.test(q)) {
|
|
875
|
+
const name = focus?.label || "<name>";
|
|
876
|
+
return "I don't score risk — but two honest proxies live in the graph: "
|
|
877
|
+
+ `"/impact ${name}" (what a change reaches) and "who touched ${name}" (churn).`;
|
|
878
|
+
}
|
|
879
|
+
const why = q.match(WHY_UNTESTED_RE);
|
|
880
|
+
if (why) {
|
|
881
|
+
const name = nudgeName(why[1], focus);
|
|
882
|
+
return "I can't know why — the graph records what IS, not intent. "
|
|
883
|
+
+ `"what tests ${name}" and "untested modules" show the coverage facts.`;
|
|
884
|
+
}
|
|
885
|
+
if (IMPERATIVE_NUDGE_RE.test(q)) {
|
|
886
|
+
const name = nudgeName(/\b(?:it|this)\b/i.test(q) ? "it" : "", focus);
|
|
887
|
+
return "I don't write code — I read a graph of it. "
|
|
888
|
+
+ `/tests ${name} shows what covers it; "untested modules" shows the gaps.`;
|
|
889
|
+
}
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** The wall-repeat one-liner (0.8.2 WS4 wall kindness (a)). MUST NOT match
|
|
894
|
+
* WALL_MISS_RE: the suppression keys on the PREVIOUS answer matching it, so this
|
|
895
|
+
* text self-limits — a third consecutive miss re-offers the tailored hint. */
|
|
896
|
+
const WALL_REPEAT_ONELINER = "still couldn't parse that — /help lists every query shape.";
|
|
897
|
+
|
|
676
898
|
// ---- repo-root resolution: default the target to the GIT ROOT, not raw cwd ----
|
|
677
899
|
|
|
678
900
|
/** The git top-level for `cwd`, or null if not in a repo (or git is unavailable).
|
|
@@ -747,12 +969,15 @@ const RECALL_TOP_K = 2;
|
|
|
747
969
|
|
|
748
970
|
/** Frame/stop words ignored when checking that a recalled Q genuinely shares
|
|
749
971
|
* vocabulary with the live query — at least one shared CONTENT word is required,
|
|
750
|
-
* so "which …" alone can never masquerade as a memory.
|
|
972
|
+
* so "which …" alone can never masquerade as a memory. Includes PATH-NOISE
|
|
973
|
+
* tokens (src, lib, mjs, …): "who owns src/handlers/tasks.mjs" must never
|
|
974
|
+
* recall "who touched src/core/store.mjs" off "src" alone. */
|
|
751
975
|
const RECALL_STOPWORDS = new Set([
|
|
752
976
|
"the", "a", "an", "and", "or", "for", "with", "about", "into", "from",
|
|
753
977
|
"which", "what", "who", "how", "when", "where", "why",
|
|
754
978
|
"does", "do", "did", "is", "are", "was", "were", "there",
|
|
755
979
|
"me", "my", "we", "i", "you", "it", "this", "that", "in", "of", "to",
|
|
980
|
+
"src", "lib", "app", "mjs", "cjs", "js", "ts", "py", "index", "main", "test",
|
|
756
981
|
]);
|
|
757
982
|
const recallWords = (s) => new Set(
|
|
758
983
|
String(s).toLowerCase().split(/[^a-z0-9.]+/).filter((w) => w.length >= 3 && !RECALL_STOPWORDS.has(w)),
|
|
@@ -767,9 +992,18 @@ function uuidv7Day(id) {
|
|
|
767
992
|
return ms > 0 && Number.isFinite(ms) ? new Date(ms).toISOString().slice(0, 10) : null;
|
|
768
993
|
}
|
|
769
994
|
|
|
995
|
+
/** A recall frame's opening — a stored ANSWER carrying it is a replay of an
|
|
996
|
+
* earlier recall, never fresh content (nested recall-of-recall hygiene). */
|
|
997
|
+
const RECALL_PREAMBLE_RE = /^you asked about this before/;
|
|
998
|
+
|
|
770
999
|
/** Pick the recalled block's Q/A pair most relevant to the query (content-word
|
|
771
|
-
* overlap; ties → first). Null when
|
|
772
|
-
*
|
|
1000
|
+
* overlap; ties → first). Null when nothing qualifies — the block matched on
|
|
1001
|
+
* packaging, not substance, so the honest miss must stand. Recall HYGIENE
|
|
1002
|
+
* (0.8.2): only a pair with a SUBSTANTIVE answer is recallable — a Q-only pair,
|
|
1003
|
+
* a grammar-wall answer (WALL_MISS_RE) or a prior recall frame (nested
|
|
1004
|
+
* recall-of-recall) is skipped; and the shared overlap must carry at least one
|
|
1005
|
+
* dotted path/file token or ≥2 plain content words, so "src"/"mjs"-grade noise
|
|
1006
|
+
* never bridges two unrelated questions. */
|
|
773
1007
|
function bestQaPair(blockText, query) {
|
|
774
1008
|
const qWords = recallWords(query);
|
|
775
1009
|
const pairs = [];
|
|
@@ -781,17 +1015,20 @@ function bestQaPair(blockText, query) {
|
|
|
781
1015
|
let best = null;
|
|
782
1016
|
let bestScore = 0;
|
|
783
1017
|
for (const p of pairs) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
if (
|
|
1018
|
+
if (!p.a || WALL_MISS_RE.test(p.a) || RECALL_PREAMBLE_RE.test(p.a)) continue;
|
|
1019
|
+
const shared = [...recallWords(p.q)].filter((w) => qWords.has(w));
|
|
1020
|
+
if (!shared.some((w) => w.includes(".")) && shared.length < 2) continue;
|
|
1021
|
+
if (shared.length > bestScore) { best = p; bestScore = shared.length; }
|
|
787
1022
|
}
|
|
788
1023
|
return best;
|
|
789
1024
|
}
|
|
790
1025
|
|
|
791
1026
|
/** W2 seam: consult the folded-session block index for an honest miss. A
|
|
792
1027
|
* sufficiently-relevant hit returns the recalled Q/A, framed and cited to its
|
|
793
|
-
* session; anything less returns null and the miss stands
|
|
794
|
-
*
|
|
1028
|
+
* session; anything less returns null and the miss stands BYTE-UNCHANGED. A
|
|
1029
|
+
* recall only ever fires with a substantive recalled A (bestQaPair's hygiene),
|
|
1030
|
+
* so it is never prepended to a reply that is itself a miss going to record.
|
|
1031
|
+
* Lazy + failure-tolerated (chat.mjs ethos): a broken store degrades to null. */
|
|
795
1032
|
async function recallFromBlocks(memoryDir, query) {
|
|
796
1033
|
try {
|
|
797
1034
|
const { retrieveBlocks } = await import("./memory/blocks.mjs");
|
|
@@ -802,8 +1039,7 @@ async function recallFromBlocks(memoryDir, query) {
|
|
|
802
1039
|
if (!pair) return null;
|
|
803
1040
|
const day = uuidv7Day(best.id);
|
|
804
1041
|
const cite = `session ${String(best.id).slice(0, 8)}${day ? `, ${day}` : ""}`;
|
|
805
|
-
|
|
806
|
-
return `you asked about this before (${cite}):\n ${qa}`;
|
|
1042
|
+
return `you asked about this before (${cite}):\n Q: ${pair.q}\n A: ${pair.a}`;
|
|
807
1043
|
} catch {
|
|
808
1044
|
return null;
|
|
809
1045
|
}
|
|
@@ -838,6 +1074,7 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
838
1074
|
"mgx:hasFirstSubevent": "begins with",
|
|
839
1075
|
"mgx:hasLastSubevent": "ends with",
|
|
840
1076
|
"mgx:hasPrerequisite": "requires",
|
|
1077
|
+
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
841
1078
|
};
|
|
842
1079
|
const factPhrase = (f) => `${f.subject} ${FACT_PREDICATE_PHRASES[f.predicate] || f.predicate} ${f.object}`;
|
|
843
1080
|
|
|
@@ -848,7 +1085,9 @@ const factPhrase = (f) => `${f.subject} ${FACT_PREDICATE_PHRASES[f.predicate] ||
|
|
|
848
1085
|
* for themselves. Provenance stays VERBATIM either way. */
|
|
849
1086
|
function renderFactLine(f) {
|
|
850
1087
|
const cite = f.provenance ? ` (source: ${f.provenance})` : "";
|
|
851
|
-
|
|
1088
|
+
// ace:chat = the ACE-parsed operator assert; teach:chat = the teach lane's
|
|
1089
|
+
// natural frames — both are things the operator SAID, so both read first-person.
|
|
1090
|
+
if (f.provenance.includes("ace:chat") || f.provenance.includes("teach:chat")) return `you told me: ${factPhrase(f)}${cite}`;
|
|
852
1091
|
// CORPUS facts are background DATA — present the relation plainly, cited to its
|
|
853
1092
|
// source, NEVER "i learned: …" (the footgun: a first-person claim over corpus noise).
|
|
854
1093
|
if (f.provenance.includes("corpus:")) return `${factPhrase(f)}${cite}`;
|
|
@@ -975,6 +1214,9 @@ const TOLD_ABOUT_RE = /^what\s+(?:did|have)\s+(?:i|we|you)\s+(?:told|tell|said|s
|
|
|
975
1214
|
/** "what kind of thing is an X" — the subject-side membership phrasing the grammar
|
|
976
1215
|
* doesn't parse: reports X's OWN remembered type (falling back to X's members). */
|
|
977
1216
|
const KIND_OF_RE = /^what\s+kind\s+of\s+(?:thing|class|type|category|entity)?\s*(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
1217
|
+
/** "who owns <X>" / "who maintains <X>" — the closed ownership read-back over
|
|
1218
|
+
* the teach lane's mgx:ownedBy facts. */
|
|
1219
|
+
const WHO_OWNS_RE = /^who\s+(?:owns|maintains)\s+(.+?)[?.!\s]*$/i;
|
|
978
1220
|
/** WHOLE-STORE recall (CHATBENCH_006 lever 3): "what did i tell you [last time]",
|
|
979
1221
|
* "what facts do you know", "what do you remember" — list EVERY remembered fact
|
|
980
1222
|
* (no subject/object term to filter on), cited, higher-trust first. The multi-turn
|
|
@@ -992,6 +1234,39 @@ async function entityClassNoun(graph, term) {
|
|
|
992
1234
|
return cls && CLASS_LABELS[cls] ? CLASS_LABELS[cls][0] : null;
|
|
993
1235
|
}
|
|
994
1236
|
|
|
1237
|
+
/** A relation group that carries class-inheritance edges — the same token family
|
|
1238
|
+
* codegraph.mjs's relationKind classifies as "inherits" (checked locally over
|
|
1239
|
+
* prop+predicate so this file adds no codegraph import surface). */
|
|
1240
|
+
const INHERITS_GROUP_RE = /inherit|supertype|subclass|extend|specializ/i;
|
|
1241
|
+
/** How far up an inheritance chain the class↔instance bridge walks. */
|
|
1242
|
+
const INHERITS_MAX_HOPS = 8;
|
|
1243
|
+
|
|
1244
|
+
/** Walk the code graph's `inherits` chain UPWARD from an entity id — each
|
|
1245
|
+
* superclass as { id, label }, bounded (≤ INHERITS_MAX_HOPS) and cycle-safe.
|
|
1246
|
+
* Read-only over graph.relations; feeds the class↔instance bridge so a taught
|
|
1247
|
+
* "controller ⊑ handler" composes with a graph "TaskController inherits
|
|
1248
|
+
* Controller". Follows the FIRST outgoing inherits edge per hop (single
|
|
1249
|
+
* inheritance is the emitted shape). */
|
|
1250
|
+
function inheritsChain(graph, startId) {
|
|
1251
|
+
const out = [];
|
|
1252
|
+
if (!graph || !startId) return out;
|
|
1253
|
+
const seen = new Set([startId]);
|
|
1254
|
+
let cur = startId;
|
|
1255
|
+
for (let hop = 0; hop < INHERITS_MAX_HOPS; hop += 1) {
|
|
1256
|
+
let edge = null;
|
|
1257
|
+
for (const g of graph.relations || []) {
|
|
1258
|
+
if (!INHERITS_GROUP_RE.test(`${g?.prop || ""} ${g?.predicate || ""}`)) continue;
|
|
1259
|
+
edge = (g.edges || []).find((e) => e?.subject === cur) || null;
|
|
1260
|
+
if (edge) break;
|
|
1261
|
+
}
|
|
1262
|
+
if (!edge || seen.has(edge.object)) break;
|
|
1263
|
+
seen.add(edge.object);
|
|
1264
|
+
out.push({ id: edge.object, label: edge.objectLabel || edge.object });
|
|
1265
|
+
cur = edge.object;
|
|
1266
|
+
}
|
|
1267
|
+
return out;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
995
1270
|
/** ASSERT-RECALL MULTI-TURN READ-BACK (PLAN_CYCLE_4 tail → cycle-005 lever 2):
|
|
996
1271
|
* once "every X is a Y" is asserted in an earlier turn, the graded assert-recall
|
|
997
1272
|
* cells (B2/C1 assert) query it back across turns in shapes the graph grammar
|
|
@@ -1046,9 +1321,45 @@ async function factReadBack(memoryDir, query, envelope, miss, graph = null) {
|
|
|
1046
1321
|
.filter((f) => subjCandidates.has(f.subject) && objVariants.has(f.object))
|
|
1047
1322
|
.sort(byTrust)[0];
|
|
1048
1323
|
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
1324
|
+
// CLASS↔INSTANCE BRIDGE (0.8.2): when X resolves to a graph entity, its
|
|
1325
|
+
// inherits chain's superclass LABELS are subject candidates too — a taught
|
|
1326
|
+
// "controller ⊑ handler" composes with a graph "TaskController inherits
|
|
1327
|
+
// Controller" so "is TaskController a handler" answers yes, naming BOTH
|
|
1328
|
+
// sources (the graph edge + the taught fact with its provenance).
|
|
1329
|
+
const ent = await resolveEntity(graph, isaAsk[1]);
|
|
1330
|
+
if (ent) {
|
|
1331
|
+
const bridgeSubjects = new Map(); // fact-term variant → the superclass label as spelled in the graph
|
|
1332
|
+
for (const sup of inheritsChain(graph, ent.id)) {
|
|
1333
|
+
for (const v of factTermVariants(normFactTerm, sup.label)) {
|
|
1334
|
+
if (!subjCandidates.has(v) && !bridgeSubjects.has(v)) bridgeSubjects.set(v, sup.label);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const bridged = isa
|
|
1338
|
+
.filter((f) => bridgeSubjects.has(f.subject) && objVariants.has(f.object))
|
|
1339
|
+
.sort(byTrust)[0];
|
|
1340
|
+
if (bridged) {
|
|
1341
|
+
return {
|
|
1342
|
+
text: `yes — the code graph says ${ent.label} inherits ${bridgeSubjects.get(bridged.subject)}, and ${renderFactLine(bridged)}`,
|
|
1343
|
+
replace: true,
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1049
1347
|
return null; // no remembered fact — the honest miss stands (never a guessed "no")
|
|
1050
1348
|
}
|
|
1051
1349
|
|
|
1350
|
+
// (a2) OWNERSHIP read-back — "who owns/maintains <X>": the teach lane's
|
|
1351
|
+
// mgx:ownedBy facts about X, trust-ranked, each cited (the source receipt
|
|
1352
|
+
// stays in the render). No fact → null, the honest miss stands.
|
|
1353
|
+
const owns = q.match(WHO_OWNS_RE);
|
|
1354
|
+
if (owns) {
|
|
1355
|
+
const variants = factTermVariants(normFactTerm, owns[1]);
|
|
1356
|
+
const hits = rows
|
|
1357
|
+
.filter((f) => f.predicate === OWNED_BY_PREDICATE && variants.has(f.subject))
|
|
1358
|
+
.sort(byTrust);
|
|
1359
|
+
if (!hits.length) return null;
|
|
1360
|
+
return renderMany(hits);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1052
1363
|
// (b) RECALL — "what did i tell you about X": every remembered fact mentioning X.
|
|
1053
1364
|
const told = q.match(TOLD_ABOUT_RE);
|
|
1054
1365
|
if (told) {
|
|
@@ -1332,6 +1643,19 @@ function relationTermOf(query, envelope) {
|
|
|
1332
1643
|
if ((m = q.match(/^what\s+([a-z][a-z-]*?)\s+are\s+there$/))) return m[1];
|
|
1333
1644
|
// "what is calling", "what is importing" (bare gerund, no object)
|
|
1334
1645
|
if ((m = q.match(/^what\s+(?:is|are)\s+([a-z][a-z-]*ing)$/))) return m[1];
|
|
1646
|
+
// THE SINGULAR META FORM — "what is a test" / "what is an import". The whole meta
|
|
1647
|
+
// shape used to be excluded here to keep the frozen am-meta-imports ambiguity case
|
|
1648
|
+
// ("what does imports mean") out; but that case is a DIFFERENT shape (ambiguousParse
|
|
1649
|
+
// → envelope.parsed is null), and a relation word whose SINGULAR reads as a real
|
|
1650
|
+
// graph-schema class/predicate ("what is a contains"/"cochange") answers non-miss
|
|
1651
|
+
// from the ordinary meta path. So admit the article meta form ONLY when the ordinary
|
|
1652
|
+
// path MISSED on a plain definitional parse (envelope.miss on a shape:"meta" object):
|
|
1653
|
+
// an unambiguous relation word like "test" — no schema reading, no ambiguity — then
|
|
1654
|
+
// reaches the relation-concept force exactly as its plural "what are the tests" does.
|
|
1655
|
+
// RELATION_TERM still gates the term downstream, so a non-relation miss is untouched.
|
|
1656
|
+
if (envelope?.miss === true && envelope?.parsed?.shape === "meta" && envelope.parsed.object) {
|
|
1657
|
+
return envelope.parsed.object;
|
|
1658
|
+
}
|
|
1335
1659
|
return null;
|
|
1336
1660
|
}
|
|
1337
1661
|
|
|
@@ -1590,10 +1914,35 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
1590
1914
|
const taught = await teachLane(query, { memoryDir, sessionId, lexicon });
|
|
1591
1915
|
if (taught) { answer = taught.text; via = taught.via; recordMiss = taught.miss; }
|
|
1592
1916
|
}
|
|
1917
|
+
// (4b) #4 AUTHOR lane (0.8.2 WS4) — "who is <Name>", "what did <Name> touch",
|
|
1918
|
+
// "who authored <sha>": the Commit author ATTRIBUTE answered as a person, off
|
|
1919
|
+
// codegraph.mjs's authorIndex renderers. Closed regexes + an exact case-
|
|
1920
|
+
// insensitive author hit only; an unknown name falls through to the ordinary
|
|
1921
|
+
// honest miss below (never a guess).
|
|
1922
|
+
if (miss && recordMiss && via === "composed") {
|
|
1923
|
+
const authored = authorLane(query, { graph });
|
|
1924
|
+
if (authored) { answer = authored.text; via = authored.via; recordMiss = false; }
|
|
1925
|
+
}
|
|
1926
|
+
// (4c) CAPABILITY NUDGES (0.8.2 WS4) — risk scoring / code opinions / "write me
|
|
1927
|
+
// code" imperatives / motive-"why": an honest wall pointing at the nearest real
|
|
1928
|
+
// query shapes. recordMiss stays TRUE — a capability wall is still a miss and
|
|
1929
|
+
// must never become a recallable answer. The opinion gate fires HERE, before the
|
|
1930
|
+
// short-miss's "is a <thing> a <kind>" membership hint could claim the line.
|
|
1931
|
+
if (miss && recordMiss && via === "composed") {
|
|
1932
|
+
const nudged = nudgeAnswer(query, newFocus);
|
|
1933
|
+
if (nudged) { answer = nudged; via = "miss"; }
|
|
1934
|
+
}
|
|
1593
1935
|
// (5) #1 SHORT TAILORED MISS — replace ONLY the engine's full grammar cheat-sheet
|
|
1594
1936
|
// wall (WALL_MISS_RE). Receipt-bearing misses keep their specific wording.
|
|
1937
|
+
// WALL KINDNESS (0.8.2 WS4 (a)): when the PREVIOUS turn's answer was already a
|
|
1938
|
+
// wall/short-miss (it matched WALL_MISS_RE), the second consecutive wall collapses
|
|
1939
|
+
// to a one-liner whose text does NOT match WALL_MISS_RE — self-limiting, so a
|
|
1940
|
+
// third consecutive miss re-offers the tailored hint instead of droning.
|
|
1595
1941
|
if (miss && recordMiss && via === "composed" && WALL_MISS_RE.test(answer)) {
|
|
1596
|
-
answer =
|
|
1942
|
+
answer = (last?.answer && WALL_MISS_RE.test(String(last.answer)))
|
|
1943
|
+
? WALL_REPEAT_ONELINER
|
|
1944
|
+
: shortMissHint(query);
|
|
1945
|
+
via = "miss";
|
|
1597
1946
|
}
|
|
1598
1947
|
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
1599
1948
|
// dead-end (an honest empty, the short miss, the bootstrap note) carries the exit
|