@polycode-projects/the-mechanical-code-talker 0.8.1 → 0.9.0
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 +14 -0
- package/ROADMAP.md +113 -38
- package/corpus/seon/concepts.jsonl +42 -0
- package/data/templates/responses.jsonl +1 -1
- package/package.json +2 -1
- package/src/ask.mjs +574 -31
- package/src/chat.mjs +864 -43
- package/src/codegraph.mjs +109 -1
- package/src/grammar/lexicon-core.json +7 -0
- package/src/interpret/merge.mjs +16 -1
- package/src/interpret/normalize.mjs +245 -4
- 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 +148 -50
- package/src/router/guardrail.mjs +1 -1
- package/src/router/planner.mjs +22 -1
- package/src/router/resolver.mjs +1 -1
- package/src/router/set-algebra.mjs +31 -0
package/src/codegraph.mjs
CHANGED
|
@@ -1173,7 +1173,13 @@ export function renderSearch(graph, query, { limit = SEARCH_LIMIT, kind = "", de
|
|
|
1173
1173
|
* mechanical NL-query engine (PLAN_MECHANICAL_CHAT.md) to orchestrate rather than duplicate. */
|
|
1174
1174
|
export function edgesOfKind(graph, kind) {
|
|
1175
1175
|
const out = [];
|
|
1176
|
-
|
|
1176
|
+
// Plain-loop append, NOT out.push(...g.edges): argument spread materialises every
|
|
1177
|
+
// element as a call argument and overflows the stack past ~100k edges (live report:
|
|
1178
|
+
// 27,770-module repo, "list modules in <dir>" → "Maximum call stack size exceeded").
|
|
1179
|
+
for (const g of graph.relations) {
|
|
1180
|
+
if (relationKind(g) !== kind) continue;
|
|
1181
|
+
for (const e of g.edges) out.push(e);
|
|
1182
|
+
}
|
|
1177
1183
|
return out;
|
|
1178
1184
|
}
|
|
1179
1185
|
|
|
@@ -1517,6 +1523,108 @@ export function renderClassHistory(graph, ind) {
|
|
|
1517
1523
|
return renderSymbolHistory(graph, ind);
|
|
1518
1524
|
}
|
|
1519
1525
|
|
|
1526
|
+
// ---- author identity (0.8.2 WS4): the Commit "author" attribute answered as a
|
|
1527
|
+
// person — "who is <Name>", "what did <Name> touch". Author is an ATTRIBUTE
|
|
1528
|
+
// (key "author"/mgx:commitAuthor), never an individual, so these read the
|
|
1529
|
+
// attribute off every Commit and aggregate. All renderers return null on an
|
|
1530
|
+
// unknown name — the chat lane falls through to the ordinary honest miss. ----
|
|
1531
|
+
|
|
1532
|
+
const AUTHOR_TOUCH_CAP = 15;
|
|
1533
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1534
|
+
|
|
1535
|
+
/** Map of lowercased author name → that author's Commit individuals (payload order).
|
|
1536
|
+
* Tolerates both attribute-key conventions (author / commitAuthor), like commitLine. */
|
|
1537
|
+
export function authorIndex(graph) {
|
|
1538
|
+
const idx = new Map();
|
|
1539
|
+
for (const ind of graph?.individuals || []) {
|
|
1540
|
+
if ((ind.class || "") !== "Commit") continue;
|
|
1541
|
+
const author = String(attrVal(ind, "author") || attrVal(ind, "commitAuthor")).trim();
|
|
1542
|
+
if (!author) continue;
|
|
1543
|
+
const key = author.toLowerCase();
|
|
1544
|
+
if (!idx.has(key)) idx.set(key, []);
|
|
1545
|
+
idx.get(key).push(ind);
|
|
1546
|
+
}
|
|
1547
|
+
return idx;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/** "May–Jun 2026"-style range over the commits' date attributes ("" when undated). */
|
|
1551
|
+
function commitDateRange(commits) {
|
|
1552
|
+
const dates = commits
|
|
1553
|
+
.map((c) => new Date(attrVal(c, "date") || attrVal(c, "commitDate")))
|
|
1554
|
+
.filter((d) => !Number.isNaN(d.getTime()))
|
|
1555
|
+
.sort((a, b) => a - b);
|
|
1556
|
+
if (!dates.length) return "";
|
|
1557
|
+
const fmt = (d) => `${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
1558
|
+
const lo = fmt(dates[0]);
|
|
1559
|
+
const hi = fmt(dates[dates.length - 1]);
|
|
1560
|
+
if (lo === hi) return lo;
|
|
1561
|
+
if (dates[0].getUTCFullYear() === dates[dates.length - 1].getUTCFullYear()) {
|
|
1562
|
+
return `${MONTHS[dates[0].getUTCMonth()]}–${hi}`;
|
|
1563
|
+
}
|
|
1564
|
+
return `${lo}–${hi}`;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
/** The deduped labels of everything an author's commits touched (touches +
|
|
1568
|
+
* touchesSymbol edge OBJECTS), in edge order. [] on an unknown author. */
|
|
1569
|
+
function authorTouchedLabels(graph, commits) {
|
|
1570
|
+
const ids = new Set(commits.map((c) => c.id));
|
|
1571
|
+
const labels = [];
|
|
1572
|
+
const seen = new Set();
|
|
1573
|
+
for (const kind of ["touches", "touchesSymbol"]) {
|
|
1574
|
+
for (const e of edgesOfKind(graph, kind)) {
|
|
1575
|
+
if (!ids.has(e.subject)) continue;
|
|
1576
|
+
const label = e.objectLabel || graph.byId?.get?.(e.object)?.label || e.object;
|
|
1577
|
+
if (seen.has(label)) continue;
|
|
1578
|
+
seen.add(label);
|
|
1579
|
+
labels.push(label);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
return labels;
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
/** Identity card for an author name — "Grace Hopper — 2 commits in this index
|
|
1586
|
+
* (May–Jun 2026), touching: <labels, capped>". Null on an unknown name. */
|
|
1587
|
+
export function renderAuthorCard(graph, name) {
|
|
1588
|
+
const commits = authorIndex(graph).get(String(name || "").trim().toLowerCase());
|
|
1589
|
+
if (!commits?.length) return null;
|
|
1590
|
+
const display = String(attrVal(commits[0], "author") || attrVal(commits[0], "commitAuthor")).trim();
|
|
1591
|
+
const range = commitDateRange(commits);
|
|
1592
|
+
const touched = authorTouchedLabels(graph, commits);
|
|
1593
|
+
const touching = touched.length ? `, touching: ${capJoin(touched, AUTHOR_TOUCH_CAP)}` : "";
|
|
1594
|
+
return `${display} — ${commits.length} commit${commits.length === 1 ? "" : "s"} in this index${range ? ` (${range})` : ""}${touching}.`;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** What an author touched — the deduped entity list off her commits' touches/
|
|
1598
|
+
* touchesSymbol edges, capped like the other bounded renders. Null on an unknown
|
|
1599
|
+
* name; an honest "no touch edges" line when the commits carry none. */
|
|
1600
|
+
export function renderAuthorTouches(graph, name) {
|
|
1601
|
+
const commits = authorIndex(graph).get(String(name || "").trim().toLowerCase());
|
|
1602
|
+
if (!commits?.length) return null;
|
|
1603
|
+
const display = String(attrVal(commits[0], "author") || attrVal(commits[0], "commitAuthor")).trim();
|
|
1604
|
+
const touched = authorTouchedLabels(graph, commits);
|
|
1605
|
+
if (!touched.length) return `${display}: ${commits.length} commit${commits.length === 1 ? "" : "s"} in this index, but no touch edges recorded for them.`;
|
|
1606
|
+
return `${display} touched ${touched.length} entit${touched.length === 1 ? "y" : "ies"} across ${commits.length} commit${commits.length === 1 ? "" : "s"}:\n ${capJoin(touched, AUTHOR_TOUCH_CAP, "\n ")}`;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/** "authored by <author> (<date>)" for a commit named by sha (7-40 hex chars; the
|
|
1610
|
+
* graph label and the typed sha may each be a prefix of the other). Null when the
|
|
1611
|
+
* sha matches no commit or more than one — never a guess. */
|
|
1612
|
+
export function renderCommitAuthor(graph, sha) {
|
|
1613
|
+
const s = String(sha || "").trim().toLowerCase();
|
|
1614
|
+
if (!/^[0-9a-f]{7,40}$/.test(s)) return null;
|
|
1615
|
+
const hits = (graph?.individuals || []).filter((i) => {
|
|
1616
|
+
if ((i.class || "") !== "Commit") return false;
|
|
1617
|
+
const label = String(i.label || "").toLowerCase();
|
|
1618
|
+
return label.startsWith(s) || s.startsWith(label);
|
|
1619
|
+
});
|
|
1620
|
+
if (hits.length !== 1) return null;
|
|
1621
|
+
const c = hits[0];
|
|
1622
|
+
const author = String(attrVal(c, "author") || attrVal(c, "commitAuthor")).trim();
|
|
1623
|
+
if (!author) return null;
|
|
1624
|
+
const date = attrVal(c, "date") || attrVal(c, "commitDate");
|
|
1625
|
+
return `${c.label}: authored by ${author}${date ? ` (${date})` : ""}.`;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1520
1628
|
// ---- symbol search (kind=function/class/method/attribute, with name/decorator filters)
|
|
1521
1629
|
|
|
1522
1630
|
const SYMBOL_CLASSES = { function: "Function", class: "Class", method: "Method", attribute: "Attribute" };
|
|
@@ -168,6 +168,13 @@
|
|
|
168
168
|
"status": { "property": "data" },
|
|
169
169
|
"language": { "property": "data" },
|
|
170
170
|
"extension": { "property": "data" },
|
|
171
|
+
"churn": { "property": "data" },
|
|
172
|
+
"impact": { "property": "data" },
|
|
173
|
+
"complexity": { "property": "data" },
|
|
174
|
+
"latency": { "property": "data" },
|
|
175
|
+
"duration": { "property": "data" },
|
|
176
|
+
"frequency": { "property": "data" },
|
|
177
|
+
"severity": { "property": "data" },
|
|
171
178
|
"owner": { "property": "object" },
|
|
172
179
|
"maintainer": { "property": "object" },
|
|
173
180
|
"author": { "property": "object" },
|
package/src/interpret/merge.mjs
CHANGED
|
@@ -25,7 +25,18 @@ const DEFAULT_CONFIDENCE = 0.5;
|
|
|
25
25
|
// commit-sha tier strips the noun — the anchored strategy captures the noun inside
|
|
26
26
|
// its object span while keyword-spot consumes it as the entity keyword, so without
|
|
27
27
|
// this the two strategies would "disagree" over a word that names no different thing.
|
|
28
|
-
|
|
28
|
+
// A leading DETERMINER is the same kind of non-difference (0.8.2 feel wave): the
|
|
29
|
+
// anchored grammar captures "the logger" while keyword-spot captures "logger", and
|
|
30
|
+
// the resulting "ambiguity" asked the user to choose between identical readings.
|
|
31
|
+
const cmpTerm = (s) => String(s || "").trim().toLowerCase().replace(/\s+/g, " ")
|
|
32
|
+
.replace(/^(?:the|a|an)\s+/, "")
|
|
33
|
+
.replace(/^commit\s+(?=[0-9a-f]{7,40}$)/, "");
|
|
34
|
+
|
|
35
|
+
// Leading-determiner probe over a parse's term slots — the dedupe below keeps the
|
|
36
|
+
// det-LESS twin so downstream term resolution sees the bare object ("logger",
|
|
37
|
+
// never "the logger").
|
|
38
|
+
const LEADING_DET_RE = /^\s*(?:the|a|an)\s+/i;
|
|
39
|
+
const detCount = (p) => [p?.subject, p?.object].filter((t) => LEADING_DET_RE.test(String(t || ""))).length;
|
|
29
40
|
|
|
30
41
|
/** Do two independently-produced parses mean the same graph query? Same
|
|
31
42
|
* shape, same relation kind, and matching term(s) (both subject and object
|
|
@@ -99,6 +110,10 @@ export function mergeStrategyResults(results) {
|
|
|
99
110
|
if (dup) {
|
|
100
111
|
dup.agreed += 1;
|
|
101
112
|
dup.confidence = Math.max(dup.confidence, c.confidence);
|
|
113
|
+
// determiner-insensitive collapse: when the agreeing parses differ only
|
|
114
|
+
// by a leading determiner, the det-less reading's parse survives (the
|
|
115
|
+
// representative's strategy/confidence standing is unchanged).
|
|
116
|
+
if (detCount(c.parsed) < detCount(dup.parsed)) dup.parsed = c.parsed;
|
|
102
117
|
continue;
|
|
103
118
|
}
|
|
104
119
|
distinct.push({ ...c, agreed: 1 });
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
CONTRACTIONS, MISSPELLINGS, WRONG_WORDS, G_DROP, FILLER_WORDS,
|
|
16
|
-
NEGATION_FRAMES, COMMIT_CONTENT_FRAMES,
|
|
16
|
+
NEGATION_FRAMES, COMMIT_CONTENT_FRAMES, VERB_TO_KIND,
|
|
17
17
|
} from "../ask-vocab.mjs";
|
|
18
18
|
|
|
19
19
|
export function escapeRegex(s) {
|
|
@@ -44,8 +44,219 @@ const correctionRe = (table) => new RegExp(
|
|
|
44
44
|
const MISSPELLING_RE = correctionRe(MISSPELLINGS);
|
|
45
45
|
const WRONG_WORD_RE = correctionRe(WRONG_WORDS);
|
|
46
46
|
|
|
47
|
+
// ---- closed PREAMBLE frames (0.8.2 feel wave, PLAN_CHAT_FEEL item 2) — the
|
|
48
|
+
// conversational wrapping a developer puts AROUND a real question: a greeting
|
|
49
|
+
// lead-in with a delimiter ("hey there, quick question - …"), a thanks lead-in
|
|
50
|
+
// with a delimiter ("thanks so much, …" — Bug B2, 0.8.2 follow-up), the modal
|
|
51
|
+
// politeness wrapper ("can you … please"), and the show/give-me presentation
|
|
52
|
+
// bridge. These are DELIMITER- and PHRASE-anchored, so they must run BEFORE the
|
|
53
|
+
// FILLER-strip pass below: FILLER_WORDS strips "hey"/"can you" as bare words, so
|
|
54
|
+
// after that pass the frames' anchors are gone while the punctuation debris
|
|
55
|
+
// ("there, quick question -") still poisons the parse (the playtest wall).
|
|
56
|
+
// Applied inside normalizeQuery — the one seam BOTH composition sites (ask.mjs
|
|
57
|
+
// parseQuery and interpret/pipeline.mjs normalizeInput) run first — AFTER the
|
|
58
|
+
// word-restoring correction tables (so "gimme"/"shwo me" are already "give me"/
|
|
59
|
+
// "show me") and BEFORE the filler strip. Closed patterns, applied in order to a
|
|
60
|
+
// small fixpoint; unmatched text passes through byte-unchanged. ----
|
|
61
|
+
|
|
62
|
+
/** Any relation verb phrase from the shared vocabulary — the show/give-me
|
|
63
|
+
* bridge's "this remainder is a real relation query" probe. */
|
|
64
|
+
const RELATION_VERB_RE = new RegExp(
|
|
65
|
+
"\\b(?:" + Object.keys(VERB_TO_KIND).sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
66
|
+
"i",
|
|
67
|
+
);
|
|
68
|
+
/** A remainder that opens interrogatively is already a question — unwrap it. */
|
|
69
|
+
const INTERROGATIVE_LEAD_RE = /^(?:which|what|who|whose|where|when|why|how)\b/i;
|
|
70
|
+
/** A remainder that is a KIND listing ("show me [the] untested modules", "show
|
|
71
|
+
* me the tests") already belongs to the compositional list/qualifier grammar,
|
|
72
|
+
* whose LIST_TRIGGERS include "show me"/"give me" — leave the WHOLE text
|
|
73
|
+
* untouched so that working path keeps it. Two shapes: a plural kind noun in
|
|
74
|
+
* tail position, or a bare (det +) singular kind noun and nothing else. */
|
|
75
|
+
const LISTING_TAIL_KINDS = new Set([
|
|
76
|
+
"modules", "files", "functions", "methods", "classes", "attributes", "fields",
|
|
77
|
+
"properties", "variables", "globals", "commits", "changes", "tests", "members",
|
|
78
|
+
]);
|
|
79
|
+
const BARE_KIND_RE = /^(?:all\s+|the\s+)?(?:module|file|function|method|class|attribute|field|property|variable|global|commit|change|test|member)\??$/i;
|
|
80
|
+
const isListingRemainder = (rest) => {
|
|
81
|
+
if (BARE_KIND_RE.test(rest)) return true;
|
|
82
|
+
const words = rest.replace(/\?+\s*$/, "").trim().split(/\s+/);
|
|
83
|
+
return LISTING_TAIL_KINDS.has((words[words.length - 1] || "").toLowerCase());
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Greeting lead-in with a delimiter (+ optional "quick question" bridge):
|
|
87
|
+
* "hey there, quick question - <Q>" -> "<Q>". The delimiter and the non-empty
|
|
88
|
+
* remainder are REQUIRED, so a bare "hey there" stays a greeting for chat's
|
|
89
|
+
* conversational lane, and "hey tmct, …" (a vocative, no delimiter after the
|
|
90
|
+
* greeting word) is left for the noise-strip tier that already owns it. */
|
|
91
|
+
const GREETING_PREAMBLE_RE = /^(?:hi|hiya|hello|hey|yo|howdy)(?:\s+there)?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
|
|
92
|
+
/** Thanks lead-in with a delimiter (+ optional "quick question" bridge), the
|
|
93
|
+
* sibling of GREETING_PREAMBLE_RE for the "thanks" word family (Bug B2, 0.8.2
|
|
94
|
+
* follow-up): "thanks, <Q>" / "thanks so much, <Q>" -> "<Q>". chat.mjs's
|
|
95
|
+
* GREETINGS set already treats a BARE "thanks"/"thank you"/"cheers" as
|
|
96
|
+
* small-talk, and noise-strip.mjs's CASCADE_NOISE strips a single bare
|
|
97
|
+
* token — but a multi-word lead-in ("thanks so much, X", "thanks a lot, X")
|
|
98
|
+
* left "so"/"much"/"a"/"lot" debris after the noise strip that corrupted
|
|
99
|
+
* re-parse (the object term inherited the debris). Same delimiter- and
|
|
100
|
+
* non-empty-remainder-REQUIRED discipline as the greeting frame, so a bare
|
|
101
|
+
* "thanks so much" (no delimiter, no question) stays small-talk. */
|
|
102
|
+
const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(?:\s+(?:so\s+much|a\s+lot|very\s+much|a\s+bunch))?\s*[,—–-]\s*(?:(?:just\s+a\s+)?quick\s+question\s*[,:—–-]?\s*)?(.+)$/i;
|
|
103
|
+
/** Modal politeness wrapper: "can/could/would/will you [please] <Q>[, please][?]"
|
|
104
|
+
* -> "<Q>". FILLER_WORDS already ate "can you"/"please" as words; this frame
|
|
105
|
+
* removes them as a WRAPPER so the ", please" comma never survives into the
|
|
106
|
+
* parsed object term. The unwrapped remainder flows on through the ordinary
|
|
107
|
+
* passes, so "can you tell me a joke" -> "tell me a joke" -> (FILLER) "a joke"
|
|
108
|
+
* — byte-identical to what the bare form normalizes to (the hm-joke wall). */
|
|
109
|
+
const MODAL_WRAPPER_RE = /^(?:can|could|would|will)\s+you\s+(?:please\s+)?(.+?)(?:[,\s]+please)?\??$/i;
|
|
110
|
+
/** show/give-me presentation bridge: "show me [the] <thing>". Three-way:
|
|
111
|
+
* a KIND-listing remainder is left untouched (the compositional list grammar
|
|
112
|
+
* owns "show me untested modules"); a remainder carrying a relation verb or an
|
|
113
|
+
* interrogative lead is a real query merely presented — unwrap to it ("show me
|
|
114
|
+
* which modules import X" -> "which modules import X"); anything else is an
|
|
115
|
+
* entity presentation — bridge to the describe surface ("show me the store
|
|
116
|
+
* module" -> "describe store module"). */
|
|
117
|
+
const SHOW_GIVE_ME_RE = /^(?:show|give)\s+me\s+(?:the\s+)?(.+?)\??$/i;
|
|
118
|
+
|
|
119
|
+
/** Apply the closed preamble frames in order (greeting -> modal -> show/give-me),
|
|
120
|
+
* repeated to a small fixpoint so stacked wrappers ("hey, can you show me X
|
|
121
|
+
* please") peel fully. Pure and idempotent; unmatched text passes through. */
|
|
122
|
+
export function applyPreambleFrames(text) {
|
|
123
|
+
let q = String(text || "");
|
|
124
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
125
|
+
const before = q;
|
|
126
|
+
let m = q.match(GREETING_PREAMBLE_RE);
|
|
127
|
+
if (m) q = m[1].trim();
|
|
128
|
+
m = q.match(THANKS_PREAMBLE_RE);
|
|
129
|
+
if (m) q = m[1].trim();
|
|
130
|
+
m = q.match(MODAL_WRAPPER_RE);
|
|
131
|
+
if (m) q = m[1].trim();
|
|
132
|
+
m = q.match(SHOW_GIVE_ME_RE);
|
|
133
|
+
if (m) {
|
|
134
|
+
const rest = m[1].trim();
|
|
135
|
+
if (!isListingRemainder(rest)) {
|
|
136
|
+
q = (RELATION_VERB_RE.test(rest) || INTERROGATIVE_LEAD_RE.test(rest)) ? rest : `describe ${rest}`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (q === before) break;
|
|
140
|
+
}
|
|
141
|
+
return q;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- ADVANCED_GRAMMAR track (a) (PLAN_ADVANCED_GRAMMAR.md §2): closed-frame
|
|
145
|
+
// subordination + conditionals — the proven 0.8.2 preamble-frame method (closed,
|
|
146
|
+
// delimiter-anchored, first-match-wins, unmatched text passes through
|
|
147
|
+
// byte-unchanged) at its next size up. Two families:
|
|
148
|
+
// SUBORDINATION_FRAMES strippable leading framing clauses ("since we're
|
|
149
|
+
// refactoring, which modules import x?" -> "which modules import x?") —
|
|
150
|
+
// the clause carries no query content, it's conversational scaffolding
|
|
151
|
+
// around a real question, same species as the greeting/thanks preambles.
|
|
152
|
+
// CONDITIONAL frames "if <clause>, is it <qualifier>?" compiles to the
|
|
153
|
+
// EXISTING compositional boolean-qualifier shape the grammar already
|
|
154
|
+
// answers ("<kind> <relation-gerund> <object> and <qualifier>" — proven by
|
|
155
|
+
// test/ask-compositional.test.mjs's "classes inheriting from Base and
|
|
156
|
+
// tested"), and the counterfactual "if X were deleted, what would break"
|
|
157
|
+
// compiles to the existing transitive-modifier reverse-dependency closure
|
|
158
|
+
// ("which modules transitively import X" — proven by
|
|
159
|
+
// test/ask.test.mjs's transitive-modifier suite). Both frame families are
|
|
160
|
+
// wired INSIDE normalizeQuery (not as a separate call site) because this
|
|
161
|
+
// agent's scope is normalize.mjs only — ask.mjs/interpret/pipeline.mjs
|
|
162
|
+
// call normalizeQuery already, so embedding here reaches every strategy
|
|
163
|
+
// for free, with no new call site required. A conditional shape NOT
|
|
164
|
+
// covered by these two closed patterns is deliberately left unmatched —
|
|
165
|
+
// the honest-miss discipline PLAN_ADVANCED_GRAMMAR §2a states explicitly
|
|
166
|
+
// ("refuse any conditional whose consequent isn't a computable
|
|
167
|
+
// traversal"): only rewrite to a shape independently verified correct,
|
|
168
|
+
// never a plausible-looking guess. ----
|
|
169
|
+
|
|
170
|
+
/** Strippable leading framing clause: "since/although/though/while/because/
|
|
171
|
+
* whereas/given that/now that <clause>, <Q>" -> "<Q>". Delimiter- (comma-)
|
|
172
|
+
* anchored and non-empty-remainder-required, same discipline as
|
|
173
|
+
* GREETING_PREAMBLE_RE — a bare "since when do you know that" (no comma
|
|
174
|
+
* splitting a framing clause from a real question) is NOT a subordination
|
|
175
|
+
* wrapper and is left alone; "since" as an ordinary temporal content word
|
|
176
|
+
* ("modules changed since last week", no leading comma-delimited clause)
|
|
177
|
+
* never matches either. */
|
|
178
|
+
const SUBORDINATION_FRAMES_RE =
|
|
179
|
+
/^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
|
|
180
|
+
|
|
181
|
+
/** Apply the subordination-frame strip to a small fixpoint (a doubly-wrapped
|
|
182
|
+
* "well, since X, although Y, <Q>" peels fully — rare, but the same
|
|
183
|
+
* discipline applyPreambleFrames already uses). Pure; unmatched text passes
|
|
184
|
+
* through byte-unchanged. */
|
|
185
|
+
export function applySubordinationFrames(text) {
|
|
186
|
+
let q = String(text || "");
|
|
187
|
+
for (let pass = 0; pass < 3; pass++) {
|
|
188
|
+
const m = q.match(SUBORDINATION_FRAMES_RE);
|
|
189
|
+
if (!m) break;
|
|
190
|
+
q = m[1].trim();
|
|
191
|
+
}
|
|
192
|
+
return q;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** relation-verb (bare 3rd-person singular, the RELATIONS table's own primary
|
|
196
|
+
* form) -> gerund, the shape the compositional grammar's proven
|
|
197
|
+
* "<kind> <gerund> <object> and <qualifier>" pattern needs. A small, closed,
|
|
198
|
+
* hand-curated table (not a generic morphological rule) — the same
|
|
199
|
+
* "no guessing" discipline as every other closed vocabulary in this file. */
|
|
200
|
+
const CONDITIONAL_VERB_GERUND = Object.freeze({
|
|
201
|
+
imports: "importing", calls: "calling", touches: "touching", tests: "testing",
|
|
202
|
+
exports: "exporting", contains: "containing", defines: "defining", uses: "using",
|
|
203
|
+
"inherits from": "inheriting from",
|
|
204
|
+
});
|
|
205
|
+
/** entity kind noun (singular) -> plural, the LISTABLE_KINDS the compositional
|
|
206
|
+
* grammar's subject slot takes. Regular English pluralization covers every
|
|
207
|
+
* entry (no irregulars in this closed set), so a flat table beats a
|
|
208
|
+
* morphological rule for the same "no guessing" reason as the verb table. */
|
|
209
|
+
const CONDITIONAL_KIND_PLURAL = Object.freeze({
|
|
210
|
+
module: "modules", class: "classes", function: "functions", method: "methods",
|
|
211
|
+
attribute: "attributes", variable: "variables", commit: "commits", file: "files",
|
|
212
|
+
});
|
|
213
|
+
const CONDITIONAL_QUALIFIER_SRC =
|
|
214
|
+
"public|private|protected|static|abstract|constant|re-?exported|exported|tested|covered|untested|uncovered";
|
|
215
|
+
/** "if a/the <kind> <relation-verb> <object>, is/are it/that/they/this
|
|
216
|
+
* <qualifier>?" -> "<kind plural> <relation-gerund> <object> and <qualifier>".
|
|
217
|
+
* Closed to the two small tables above (both ends validated), so an
|
|
218
|
+
* unrecognized kind/verb/qualifier simply doesn't match — falls through to
|
|
219
|
+
* the ordinary grammar, which honestly misses on the untransformed "if …"
|
|
220
|
+
* text rather than risk a wrong composition. */
|
|
221
|
+
const CONDITIONAL_QUALIFIER_RE = new RegExp(
|
|
222
|
+
"^if\\s+(?:a|an|the)?\\s*(" + Object.keys(CONDITIONAL_KIND_PLURAL).join("|") + ")\\s+"
|
|
223
|
+
+ "(" + Object.keys(CONDITIONAL_VERB_GERUND).join("|") + ")\\s+"
|
|
224
|
+
+ "(.+?),\\s*(?:is|are)\\s+(?:it|that|they|this)\\s+"
|
|
225
|
+
+ "(" + CONDITIONAL_QUALIFIER_SRC + ")\\??$",
|
|
226
|
+
"i",
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
/** Counterfactual deletion: "if <X> were/was deleted/removed(,)? what
|
|
230
|
+
* would/might break/fail/be affected?" -> "which modules transitively import
|
|
231
|
+
* <X>" — the EXISTING reverse-dependency closure (impactClosure via the
|
|
232
|
+
* transitive modifier, ask.mjs/codegraph.mjs), proven correct by
|
|
233
|
+
* test/ask.test.mjs's transitive-modifier suite. Exported so chat.mjs can
|
|
234
|
+
* independently recognize the SAME raw query shape and prepend a
|
|
235
|
+
* hypothetical marker to the rendered answer (a hypothetical consequent must
|
|
236
|
+
* never be presented as an unqualified fact) — normalize.mjs only rewrites
|
|
237
|
+
* the QUESTION text, it never touches the answer. */
|
|
238
|
+
export const COUNTERFACTUAL_RE =
|
|
239
|
+
/^if\s+(.+?)\s+(?:were|was)\s+(?:deleted|removed),?\s*what\s+(?:would|might|could)\s+(?:break|fail|be\s+affected)\??$/i;
|
|
240
|
+
|
|
241
|
+
/** Apply the two closed CONDITIONAL frames, first-match-wins (qualifier
|
|
242
|
+
* composition tried first — it is the more specific shape). Pure; unmatched
|
|
243
|
+
* text passes through byte-unchanged. */
|
|
244
|
+
export function applyConditionalFrames(text) {
|
|
245
|
+
const q = String(text || "");
|
|
246
|
+
const qual = q.match(CONDITIONAL_QUALIFIER_RE);
|
|
247
|
+
if (qual) {
|
|
248
|
+
const kind = CONDITIONAL_KIND_PLURAL[qual[1].toLowerCase()];
|
|
249
|
+
const gerund = CONDITIONAL_VERB_GERUND[qual[2].toLowerCase()];
|
|
250
|
+
return `${kind} ${gerund} ${qual[3].trim()} and ${qual[4].toLowerCase()}`;
|
|
251
|
+
}
|
|
252
|
+
const cf = q.match(COUNTERFACTUAL_RE);
|
|
253
|
+
if (cf) return `which modules transitively import ${cf[1].trim()}`;
|
|
254
|
+
return q;
|
|
255
|
+
}
|
|
256
|
+
|
|
47
257
|
/** Free-text -> normalized free-text: contractions expanded, g-dropped words
|
|
48
|
-
* restored,
|
|
258
|
+
* restored, closed preamble/subordination/conditional frames peeled, filler/
|
|
259
|
+
* politeness words stripped. Idempotent and pure — the same
|
|
49
260
|
* input always normalizes the same way, so both parsing strategies see
|
|
50
261
|
* identical text and their outputs are directly comparable. Deliberately
|
|
51
262
|
* does NOT force lowercase: object/subject terms (module names like
|
|
@@ -59,6 +270,19 @@ export function normalizeQuery(text) {
|
|
|
59
270
|
q = q.replace(MISSPELLING_RE, (m) => MISSPELLINGS[m.toLowerCase()]);
|
|
60
271
|
q = q.replace(WRONG_WORD_RE, (m) => WRONG_WORDS[m.toLowerCase()]);
|
|
61
272
|
q = q.replace(G_DROP, "$1ing");
|
|
273
|
+
// closed preamble frames (greeting lead-in, modal wrapper, show/give-me
|
|
274
|
+
// bridge) — AFTER the correction tables (a repaired "give me"/"show me" still
|
|
275
|
+
// feeds the bridge) but BEFORE the filler strip erases their anchor words.
|
|
276
|
+
q = applyPreambleFrames(q);
|
|
277
|
+
// subordination (strip a leading framing clause) THEN conditional (compile
|
|
278
|
+
// "if …" to an existing working shape) — subordination first so a stacked
|
|
279
|
+
// "since we're refactoring, if a module imports X, is it tested" peels its
|
|
280
|
+
// outer wrapper before the conditional frame ever sees it. Both run BEFORE
|
|
281
|
+
// the filler strip for the same reason the preamble frames do: their
|
|
282
|
+
// anchors ("since", "if", "is it") would otherwise be eaten as bare words,
|
|
283
|
+
// leaving punctuation/clause debris that poisons the parse.
|
|
284
|
+
q = applySubordinationFrames(q);
|
|
285
|
+
q = applyConditionalFrames(q);
|
|
62
286
|
if (FILLER_WORDS.length) {
|
|
63
287
|
const fillerRe = new RegExp(
|
|
64
288
|
"\\b(" + [...FILLER_WORDS].sort((a, b) => b.length - a.length).map(escapeRegex).join("|") + ")\\b",
|
|
@@ -173,8 +397,25 @@ export const PHRASING_FRAMES = Object.freeze([
|
|
|
173
397
|
// separate authorship edge; "touched" IS the authorship signal (the churn commits
|
|
174
398
|
// carry the author), so these are true synonyms of "who touched X", not a new
|
|
175
399
|
// capability. Anaphora rides through untouched ("who wrote it" → "who touched it").
|
|
176
|
-
|
|
177
|
-
|
|
400
|
+
// SHA GUARD (0.8.2 feel wave): a COMMIT object is NOT a synonym — "who is the
|
|
401
|
+
// author of abc1234" rewritten to "who touched abc1234" dumps the commit's
|
|
402
|
+
// touch-SET instead of naming its author. The negative lookahead refuses the
|
|
403
|
+
// rewrite when the object is a bare (optionally "commit "-prefixed) 7-40 char
|
|
404
|
+
// hex sha, leaving the un-rewritten form for the author lane to consume;
|
|
405
|
+
// file/symbol objects (anything non-sha, e.g. "deadbeef.mjs") keep the rewrite.
|
|
406
|
+
{ re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
|
|
407
|
+
{ re: /^who\s+is\s+the\s+authors?\s+of\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
|
|
408
|
+
|
|
409
|
+
// HAS-TESTS → the coverage question the RELATIONS table answers. "does X have
|
|
410
|
+
// tests" parses "have" as a defines-verb (VERB_TO_KIND), producing the garbled
|
|
411
|
+
// "No — no defines edge found from X to <whatever resolves>" receipt; "is X
|
|
412
|
+
// tested" traverses tests edges from the WRONG side (subject = X). Both mean
|
|
413
|
+
// the coverage question "what tests X" — rewrite onto it. Closed to a
|
|
414
|
+
// tests/coverage object ("does X have methods/members" stays the members
|
|
415
|
+
// family) and refuses any "not" in the subject span, so the set-complement
|
|
416
|
+
// negations ("is X not tested") keep their own handler downstream.
|
|
417
|
+
{ re: /^(?:does|do)\s+(?!.*\bnot\b)(.+?)\s+have\s+(?:any\s+)?(?:tests?|test\s+coverage|coverage)\??$/i, to: (m) => `what tests ${m[1]}` },
|
|
418
|
+
{ re: /^(?:is|are)\s+(?!.*\bnot\b)(.+?)\s+tested\??$/i, to: (m) => `what tests ${m[1]}` },
|
|
178
419
|
|
|
179
420
|
// NEEDS-TESTS → the untested-module survey. "what needs tests" / "what needs
|
|
180
421
|
// testing" is the plainest way to ask which modules are uncovered, and it hit the
|
package/src/memory/core.mjs
CHANGED
|
@@ -78,7 +78,7 @@ const MEMORY_VOCABULARY = [
|
|
|
78
78
|
{ prop: DERIVED_FROM_PROP, predicate: "derivedFrom", note: "umbrella: a Fact derived from a Source (or another Fact). ext ref prov:wasDerivedFrom (UNVERIFIED-pending-web-check)" },
|
|
79
79
|
{ prop: STATED_BY_PROP, predicate: "statedBy", note: "subPropertyOf derivedFrom: a Source directly asserts this Fact (one edge per independent source — replaces the factProvenance union)" },
|
|
80
80
|
{ prop: CANONICALISED_FROM_PROP, predicate: "canonicalisedFrom", note: "subPropertyOf derivedFrom: a canonical Fact cleaned from a raw Block/Source, never replacing it" },
|
|
81
|
-
{ prop: "mgx:sourceType", note: "a Source's kind: operator | provider | corpus | web | entailed (the trust-prior key)" },
|
|
81
|
+
{ prop: "mgx:sourceType", note: "a Source's kind: operator | teach | provider | corpus | web | entailed (the trust-prior key)" },
|
|
82
82
|
{ prop: "mgx:sourceUrl", note: "a web Source's URL" },
|
|
83
83
|
{ prop: "mgx:sourceRule", note: "an entailed Source's rule id" },
|
|
84
84
|
{ prop: TRUST_SCORE_PROP, note: "materialised trust cache in [0,1] — pure function of a fact's Sources + createdAt (memory/trust.mjs); invalidated when a statedBy edge is added" },
|
|
@@ -171,6 +171,7 @@ function setAttr(ind, prop, key, value) {
|
|
|
171
171
|
function sourceIdFor(desc) {
|
|
172
172
|
switch (desc?.kind) {
|
|
173
173
|
case "operator": return { id: OPERATOR_SOURCE_ID, type: "operator" };
|
|
174
|
+
case "teach": return { id: "src:teach-chat", type: "teach" };
|
|
174
175
|
case "provider": return { id: `src:provider:${desc.name}`, type: "provider" };
|
|
175
176
|
case "corpus": return { id: `src:corpus:${desc.name}`, type: "corpus" };
|
|
176
177
|
case "web": return { id: `src:learned:web:${fnv1aHex(String(desc.url || ""))}`, type: "web", url: String(desc.url || "") };
|
|
@@ -209,6 +210,7 @@ function upsertSource(payload, desc, createdAtCandidate) {
|
|
|
209
210
|
* through. The tag formats are exactly what the writers produce:
|
|
210
211
|
* corpus:conceptnet /r/IsA → { kind:"corpus", name:"conceptnet" }
|
|
211
212
|
* ace:chat:<session>@<ts> → { kind:"operator", createdAt:<ts> }
|
|
213
|
+
* teach:chat:<session>@<ts> → { kind:"teach", createdAt:<ts> }
|
|
212
214
|
* web:<url> | url:<url> → { kind:"web", url:<url> }
|
|
213
215
|
* entailed:<rule> → { kind:"entailed", rule:<rule> }
|
|
214
216
|
* chat:/session: refs map to the operator; an unknown tag → null (no Source).
|
|
@@ -222,6 +224,11 @@ export function provenanceTagToSource(tag) {
|
|
|
222
224
|
const at = head.indexOf("@");
|
|
223
225
|
return { kind: "operator", createdAt: at >= 0 ? head.slice(at + 1) : "" };
|
|
224
226
|
}
|
|
227
|
+
if (head.startsWith("teach:")) {
|
|
228
|
+
// the chat teach lane's natural frames — chat.mjs's teachProvenanceTag
|
|
229
|
+
const at = head.indexOf("@");
|
|
230
|
+
return { kind: "teach", createdAt: at >= 0 ? head.slice(at + 1) : "" };
|
|
231
|
+
}
|
|
225
232
|
if (head.startsWith("web:")) return { kind: "web", url: head.slice("web:".length) };
|
|
226
233
|
if (head.startsWith("url:")) return { kind: "web", url: head.slice("url:".length) };
|
|
227
234
|
if (head.startsWith("entailed:")) return { kind: "entailed", rule: head.slice("entailed:".length) };
|
package/src/memory/fold.mjs
CHANGED
|
Binary file
|
package/src/memory/trust.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// Trust is a COMPUTED attribute of a Fact — never hand-set — a pure function of
|
|
5
5
|
// its Source edges, those Sources' types, and its mgx:createdAt. Three inputs
|
|
6
6
|
// combine:
|
|
7
|
-
// - a Source-TYPE PRIOR (operator > provider > corpus > web > entailed);
|
|
7
|
+
// - a Source-TYPE PRIOR (operator > teach > provider > corpus > web > entailed);
|
|
8
8
|
// - CORROBORATION over the fact's distinct Sources by noisy-OR
|
|
9
9
|
// (1 − Π(1 − wᵢ), capped at 1) — two independent web sources (0.4) reach
|
|
10
10
|
// 0.64, a lone operator fact is already 1.0;
|
|
@@ -26,11 +26,15 @@
|
|
|
26
26
|
export const TRUST_SCORE_PROP = "mgx:trustScore";
|
|
27
27
|
export const TRUST_INPUTS_PROP = "mgx:trustInputs";
|
|
28
28
|
|
|
29
|
-
/** Source-type priors — the ordering operator > provider-graph >
|
|
30
|
-
* > web > unverified-entailment.
|
|
31
|
-
*
|
|
29
|
+
/** Source-type priors — the ordering operator > teach > provider-graph >
|
|
30
|
+
* curated-corpus > web > unverified-entailment. `teach` is the chat teach
|
|
31
|
+
* lane's natural-frame writes ("remember that …", "<Name> owns <X>") — still
|
|
32
|
+
* operator speech, but through a looser recognizer than the ACE-parsed
|
|
33
|
+
* operator assert, so it sits just below the operator prior. The entailed
|
|
34
|
+
* value is a FLOOR before premise adjustment (see the entailed hook below). */
|
|
32
35
|
export const SOURCE_PRIOR = Object.freeze({
|
|
33
36
|
operator: 1.0,
|
|
37
|
+
teach: 0.95,
|
|
34
38
|
provider: 0.9,
|
|
35
39
|
corpus: 0.7,
|
|
36
40
|
web: 0.4,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/router/call-validator.mjs — pure registry validators shared by the
|
|
2
|
+
// product router (resolver / guardrail / goal-reasoner) + the bench grader
|
|
3
|
+
// (agentbench/grade.mjs re-exports these). Depends ONLY on registry.mjs — no
|
|
4
|
+
// bench code — so the product←bench dependency stays inverted: the bench
|
|
5
|
+
// imports the product, never the other way round. No I/O, no Date.now, no LLM.
|
|
6
|
+
|
|
7
|
+
import { isCapability, argKeysOf, requiredArgsOf, preconditionsOf, PRECOND } from "./registry.mjs";
|
|
8
|
+
|
|
9
|
+
/** Every way a single produced call can be a HALLUCINATION — the one thing a
|
|
10
|
+
* deterministic router must never do. Returns [] for a clean call, else a list
|
|
11
|
+
* of { reason, detail }:
|
|
12
|
+
* - "undeclared" — name is not in the case's declared toolset
|
|
13
|
+
* - "unknown-tool"— name is not a registry capability at all
|
|
14
|
+
* - "unknown-arg" — an input key the capability does not accept
|
|
15
|
+
* - "missing-arg" — a required arg absent (and no any-present precond covers it)
|
|
16
|
+
* Any nonempty result = AUTOMATIC FAIL for the case. */
|
|
17
|
+
export function hallucinationsIn(call, declaredTools) {
|
|
18
|
+
const problems = [];
|
|
19
|
+
const name = call?.name;
|
|
20
|
+
if (typeof name !== "string" || !name) return [{ reason: "unknown-tool", detail: "no tool name" }];
|
|
21
|
+
if (!isCapability(name)) return [{ reason: "unknown-tool", detail: name }];
|
|
22
|
+
if (!declaredTools.includes(name)) problems.push({ reason: "undeclared", detail: name });
|
|
23
|
+
const input = call.input && typeof call.input === "object" ? call.input : {};
|
|
24
|
+
const accepted = argKeysOf(name);
|
|
25
|
+
for (const key of Object.keys(input)) {
|
|
26
|
+
if (!accepted.has(key)) problems.push({ reason: "unknown-arg", detail: `${name}.${key}` });
|
|
27
|
+
}
|
|
28
|
+
// required-arg presence, honoring an any-present disjunction (search: query|kind)
|
|
29
|
+
const anyGroups = preconditionsOf(name)
|
|
30
|
+
.filter((p) => p.pred === PRECOND.anyPresent)
|
|
31
|
+
.map((p) => p.params);
|
|
32
|
+
const present = (k) => input[k] !== undefined && input[k] !== null && String(input[k]).trim() !== "";
|
|
33
|
+
for (const req of requiredArgsOf(name)) {
|
|
34
|
+
if (!present(req)) problems.push({ reason: "missing-arg", detail: `${name}.${req}` });
|
|
35
|
+
}
|
|
36
|
+
for (const group of anyGroups) {
|
|
37
|
+
if (!group.some(present)) problems.push({ reason: "missing-arg", detail: `${name} needs one of ${group.join("|")}` });
|
|
38
|
+
}
|
|
39
|
+
return problems;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** True iff a produced call is dispatchable-shaped (no hallucination). */
|
|
43
|
+
export function isCallWellFormed(call, declaredTools) {
|
|
44
|
+
return hallucinationsIn(call, declaredTools).length === 0;
|
|
45
|
+
}
|