@polycode-projects/the-mechanical-code-talker 1.11.0 → 1.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -20
- package/ROADMAP.md +9 -12
- package/bin/tmct.mjs +82 -98
- package/data/games/river.txt +33 -0
- package/package.json +1 -1
- package/src/ask.mjs +119 -6
- package/src/chat.mjs +962 -51
- package/src/codegraph.mjs +24 -11
- package/src/domain.mjs +85 -6
- package/src/interpret/normalize.mjs +46 -0
- package/src/interpret/strategies/keywords.mjs +13 -9
- package/src/ledger-viz.mjs +31 -9
- package/src/memory/core.mjs +20 -1
- package/src/memory/shacl.mjs +4 -1
- package/src/memory-ask-browser-entry.mjs +5 -5
- package/src/memory-ask-browser.bundle.js +526 -47
- package/src/plan-viz.mjs +5 -6
- package/src/router/drive.mjs +122 -13
- package/src/router/resolver.mjs +13 -0
- package/src/router/taught.mjs +19 -8
- package/src/syllogise.mjs +154 -38
- package/src/viz-theme.mjs +19 -3
- package/src/ask-browser-entry.mjs +0 -19
- package/src/ask-browser.bundle.js +0 -5142
- package/src/viz.mjs +0 -959
package/src/codegraph.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lookupByProseTokens, proseLayerHits } from "./prose.mjs";
|
|
1
|
+
import { lookupByProseTokens, proseLayerHits, splitIdentifierWords } from "./prose.mjs";
|
|
2
2
|
import { cosine } from "./embed.mjs";
|
|
3
3
|
import { CREATED_AT_PROP, UPDATED_AT_PROP, provenanceTagToSource } from "./memory/core.mjs";
|
|
4
4
|
|
|
@@ -204,6 +204,19 @@ function relLabel(g) {
|
|
|
204
204
|
return g.prop ? `${g.predicate} [${g.prop}]` : g.predicate;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
/** A class enum in a rendered heading: a multi-word enum reads as words
|
|
208
|
+
* ("GlobalVariable" -> "Global Variable"); a single-word enum stays verbatim,
|
|
209
|
+
* keeping the long-standing Module/Function/Entity headings byte-identical.
|
|
210
|
+
* Title-cased (unlike ask.mjs's lowercase classDisplayName) because these
|
|
211
|
+
* sites use the enum as a heading label, not mid-sentence prose — and ask.mjs
|
|
212
|
+
* already imports this module, so reusing its formatter here would be a cycle. */
|
|
213
|
+
function classHeading(cls) {
|
|
214
|
+
const c = cls || "Entity";
|
|
215
|
+
const words = splitIdentifierWords(c);
|
|
216
|
+
if (words.length < 2) return c;
|
|
217
|
+
return words.map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
218
|
+
}
|
|
219
|
+
|
|
207
220
|
// Show the first `n` items, then a "+K more" tail with the true count.
|
|
208
221
|
function capJoin(items, n, sep = ", ") {
|
|
209
222
|
if (items.length <= n) return items.join(sep);
|
|
@@ -216,7 +229,7 @@ const PROV_CAP = 8;
|
|
|
216
229
|
/** Compact plain-text description of one individual — for an agent consumer. */
|
|
217
230
|
export function renderDescribe(graph, ind, { candidates = [] } = {}) {
|
|
218
231
|
const lines = [];
|
|
219
|
-
lines.push(`${ind.label} — ${ind.class
|
|
232
|
+
lines.push(`${ind.label} — ${classHeading(ind.class)} (id: ${ind.id})`);
|
|
220
233
|
|
|
221
234
|
const refs = (ind.derived_from || []).filter(isProvRef);
|
|
222
235
|
if (refs.length) lines.push(`attestation: touched by ${refs.length} commit(s)`);
|
|
@@ -244,7 +257,7 @@ export function renderDescribe(graph, ind, { candidates = [] } = {}) {
|
|
|
244
257
|
}
|
|
245
258
|
|
|
246
259
|
if (candidates.length) {
|
|
247
|
-
lines.push(`other matches: ${candidates.map((c) => `${c.label} (${c.class})`).join(", ")}`);
|
|
260
|
+
lines.push(`other matches: ${candidates.map((c) => `${c.label} (${classHeading(c.class)})`).join(", ")}`);
|
|
248
261
|
}
|
|
249
262
|
if (graph.truncated.length) {
|
|
250
263
|
lines.push(truncationNote(graph));
|
|
@@ -286,7 +299,7 @@ export function renderCompare(graph, indA, indB) {
|
|
|
286
299
|
const klass = indA.class || "Entity";
|
|
287
300
|
if ((indB.class || "Entity") !== klass) return null;
|
|
288
301
|
|
|
289
|
-
const lines = [`Comparing ${indA.label} and ${indB.label} (both ${klass}):`];
|
|
302
|
+
const lines = [`Comparing ${indA.label} and ${indB.label} (both ${classHeading(klass)}):`];
|
|
290
303
|
const a = edgesFor(graph, indA.id);
|
|
291
304
|
const b = edgesFor(graph, indB.id);
|
|
292
305
|
const outByPred = pairByPredicate(a.out, b.out);
|
|
@@ -1382,7 +1395,7 @@ const CALL_CAP = 30;
|
|
|
1382
1395
|
/** A class's methods + attributes (with sites/decorators) in one slice — replaces
|
|
1383
1396
|
* reading the class body. Uses the `contains` (seon:containsCodeEntity) relation. */
|
|
1384
1397
|
export function renderMembers(graph, ind) {
|
|
1385
|
-
const lines = [`${ind.label} — ${ind.class
|
|
1398
|
+
const lines = [`${ind.label} — ${classHeading(ind.class)} (id: ${ind.id})`];
|
|
1386
1399
|
const contains = edgesOfKind(graph, "contains").filter((e) => e.subject === ind.id);
|
|
1387
1400
|
if (!contains.length) {
|
|
1388
1401
|
lines.push("members: none recorded (empty class, or members not in the extracted graph). Use tmct_describe for its edges.");
|
|
@@ -1411,7 +1424,7 @@ const attrVal = (ind, key) => (ind?.attributes || []).find((a) => a.key === key)
|
|
|
1411
1424
|
* (kept OUT of tmct_context's lean bundle; this is the targeted tool for them). */
|
|
1412
1425
|
export function renderSignature(graph, ind) {
|
|
1413
1426
|
const site = siteOf(ind);
|
|
1414
|
-
const lines = [`${ind.label} — ${ind.class
|
|
1427
|
+
const lines = [`${ind.label} — ${classHeading(ind.class)}${spanTag(site)}`];
|
|
1415
1428
|
const params = attrVal(ind, "params");
|
|
1416
1429
|
const returns = attrVal(ind, "returns");
|
|
1417
1430
|
if (params || returns || (ind.class || "") === "Method" || (ind.class || "") === "Function") {
|
|
@@ -1451,7 +1464,7 @@ export function renderSubclasses(graph, ind) {
|
|
|
1451
1464
|
if (!childrenOf.has(e.object)) childrenOf.set(e.object, []);
|
|
1452
1465
|
childrenOf.get(e.object).push({ id: e.subject, label: e.subjectLabel || e.subject });
|
|
1453
1466
|
}
|
|
1454
|
-
const lines = [`${ind.label} — ${ind.class
|
|
1467
|
+
const lines = [`${ind.label} — ${classHeading(ind.class)} (id: ${ind.id})`];
|
|
1455
1468
|
lines.push(bases.length ? `extends: ${capJoin(bases, SUBCLASS_CAP)}` : "extends: (no internal/recorded base classes)");
|
|
1456
1469
|
const visited = new Set([ind.id]);
|
|
1457
1470
|
const levels = [];
|
|
@@ -1625,10 +1638,10 @@ export function callHint(graph, ind) {
|
|
|
1625
1638
|
export function renderCalls(graph, ind) {
|
|
1626
1639
|
const calls = edgesOfKind(graph, "callsSymbol").filter((e) => e.subject === ind.id);
|
|
1627
1640
|
if (!calls.length) {
|
|
1628
|
-
return `${ind.label} — ${ind.class
|
|
1641
|
+
return `${ind.label} — ${classHeading(ind.class)}: no in-repo calls recorded (calls only stdlib/external, or fine-grained call edges are not in the extracted graph).`;
|
|
1629
1642
|
}
|
|
1630
1643
|
const items = calls.map((e) => calleeRef(graph, e));
|
|
1631
|
-
return `${ind.label} — ${ind.class
|
|
1644
|
+
return `${ind.label} — ${classHeading(ind.class)} calls ${calls.length} in-repo symbol(s):\n ${capJoin(items, CALL_CAP, "\n ")}`;
|
|
1632
1645
|
}
|
|
1633
1646
|
|
|
1634
1647
|
// ---- commit history with author/date/subject (Commit attributes) ----------------
|
|
@@ -1664,11 +1677,11 @@ export function renderFileHistory(graph, ind) {
|
|
|
1664
1677
|
function renderSymbolHistory(graph, ind) {
|
|
1665
1678
|
const commits = edgesOfKind(graph, "touchesSymbol").filter((e) => e.object === ind.id);
|
|
1666
1679
|
if (!commits.length) {
|
|
1667
|
-
return `${ind.label} — ${ind.class
|
|
1680
|
+
return `${ind.label} — ${classHeading(ind.class)}: no symbol-level commit history recorded (outside the git-log window, or fine-grained history is not in the extracted graph).`;
|
|
1668
1681
|
}
|
|
1669
1682
|
const shown = commits.slice(0, HISTORY_CAP).map((e) => ` ${commitLine(graph, e.subject, e.subjectLabel)}`);
|
|
1670
1683
|
const tail = commits.length > HISTORY_CAP ? `\n …+${commits.length - HISTORY_CAP} more` : "";
|
|
1671
|
-
return `${ind.label} — ${ind.class
|
|
1684
|
+
return `${ind.label} — ${classHeading(ind.class)}: touched by ${commits.length} commit(s):\n${shown.join("\n")}${tail}`;
|
|
1672
1685
|
}
|
|
1673
1686
|
|
|
1674
1687
|
/** Method history — commits touching a specific method symbol (`touchesSymbol`). */
|
package/src/domain.mjs
CHANGED
|
@@ -46,7 +46,7 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
46
46
|
for (const rule of ruleRows || []) {
|
|
47
47
|
if (!String(rule.kind || "").startsWith("action-")) continue;
|
|
48
48
|
const name = normTerm(rule.name);
|
|
49
|
-
if (!byName.has(name)) byName.set(name, { name, signatures: [], preconds: [], effects: [] });
|
|
49
|
+
if (!byName.has(name)) byName.set(name, { name, signatures: [], preconds: [], effects: [], constraints: [] });
|
|
50
50
|
const family = byName.get(name);
|
|
51
51
|
const slots = rule.slots || {};
|
|
52
52
|
if (rule.kind === "action-signature") {
|
|
@@ -67,6 +67,12 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
67
67
|
subjectRole: normTerm(slots.subjectRole),
|
|
68
68
|
objectRole: normTerm(slots.objectRole),
|
|
69
69
|
});
|
|
70
|
+
} else if (rule.kind === "action-constraint") {
|
|
71
|
+
family.constraints.push({
|
|
72
|
+
left: normTerm(slots.left),
|
|
73
|
+
right: normTerm(slots.right),
|
|
74
|
+
guard: normTerm(slots.guard),
|
|
75
|
+
});
|
|
70
76
|
}
|
|
71
77
|
}
|
|
72
78
|
const actions = [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -75,6 +81,7 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
75
81
|
a.subjectClass.localeCompare(b.subjectClass) || a.targetClass.localeCompare(b.targetClass));
|
|
76
82
|
action.preconds.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
77
83
|
action.effects.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
84
|
+
action.constraints.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
// Class membership from typing edges. A member is a subject with a typing
|
|
@@ -92,6 +99,29 @@ export function compileDomain(factRows, ruleRows) {
|
|
|
92
99
|
for (let i = members.length - 1; i > 0; i -= 1) if (members[i] === members[i - 1]) members.splice(i, 1);
|
|
93
100
|
}
|
|
94
101
|
|
|
102
|
+
// A class-bound word (an effect role or constraint term that is neither
|
|
103
|
+
// "subject" nor "target") is substituted by its class's sole member at
|
|
104
|
+
// grounding time. With 0 or 2+ members that substitution would be a silent
|
|
105
|
+
// guess, so an ill-bound family fails loudly here instead.
|
|
106
|
+
const requireSoleMember = (word, where) => {
|
|
107
|
+
const count = (classMembers[word] || []).length;
|
|
108
|
+
if (count !== 1) {
|
|
109
|
+
throw new Error(`${where} names "${word}", which must be a class with exactly one member (it has ${count})`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
for (const action of actions) {
|
|
113
|
+
for (const effect of action.effects) {
|
|
114
|
+
for (const role of [effect.subjectRole, effect.objectRole]) {
|
|
115
|
+
if (role !== "subject" && role !== "target") requireSoleMember(role, `an effect role of "${action.name}"`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const constraint of action.constraints) {
|
|
119
|
+
for (const word of [constraint.left, constraint.right, constraint.guard]) {
|
|
120
|
+
requireSoleMember(word, `a constraint term of "${action.name}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
95
125
|
const dynamicPredicates = new Set();
|
|
96
126
|
for (const action of actions) for (const effect of action.effects) dynamicPredicates.add(effect.predicate);
|
|
97
127
|
|
|
@@ -171,13 +201,24 @@ function precondHolds(precond, subject, target, state, domain) {
|
|
|
171
201
|
return false;
|
|
172
202
|
}
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
|
|
204
|
+
/** Ground an effect/constraint role word: "subject"/"target" bind the
|
|
205
|
+
* grounding pair; any other word is class-bound and binds the class's sole
|
|
206
|
+
* member — its companion semantics (compileDomain guarantees exactly one). */
|
|
207
|
+
const roleBinding = (role, subject, target, domain) => {
|
|
208
|
+
if (role === "subject") return subject;
|
|
209
|
+
if (role === "target") return target;
|
|
210
|
+
return (domain.classMembers[role] || [])[0];
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const positionIn = (rows, term, predicate) =>
|
|
214
|
+
rows.find((r) => r.subject === term && r.predicate === predicate)?.object;
|
|
215
|
+
|
|
216
|
+
function applyEffects(effects, subject, target, state, domain) {
|
|
176
217
|
let rows = state;
|
|
177
218
|
let changed = false;
|
|
178
219
|
for (const effect of effects) {
|
|
179
|
-
const effSubject =
|
|
180
|
-
const effObject =
|
|
220
|
+
const effSubject = roleBinding(effect.subjectRole, subject, target, domain);
|
|
221
|
+
const effObject = roleBinding(effect.objectRole, subject, target, domain);
|
|
181
222
|
const already = rows.some((r) =>
|
|
182
223
|
r.subject === effSubject && r.predicate === effect.predicate && r.object === effObject);
|
|
183
224
|
if (already) continue;
|
|
@@ -189,6 +230,42 @@ function applyEffects(effects, subject, target, state) {
|
|
|
189
230
|
return [...rows].sort(rowSort);
|
|
190
231
|
}
|
|
191
232
|
|
|
233
|
+
/** True when `state` still permits every companion (class-bound effect
|
|
234
|
+
* subject) to move WITH the grounded subject. Co-location is a derived
|
|
235
|
+
* precondition, not a taught one: the taught effect says the companion ends
|
|
236
|
+
* up at the target, and applying that from a state where the companion
|
|
237
|
+
* stands elsewhere would teleport it instead of carrying it. Trivially true
|
|
238
|
+
* when the subject is its own companion. */
|
|
239
|
+
function companionsCoLocated(action, subject, target, state, domain) {
|
|
240
|
+
for (const effect of action.effects) {
|
|
241
|
+
if (effect.subjectRole === "subject" || effect.subjectRole === "target") continue;
|
|
242
|
+
const companion = roleBinding(effect.subjectRole, subject, target, domain);
|
|
243
|
+
if (companion === subject) continue;
|
|
244
|
+
const subjectAt = positionIn(state, subject, effect.predicate);
|
|
245
|
+
if (!subjectAt || positionIn(state, companion, effect.predicate) !== subjectAt) return false;
|
|
246
|
+
}
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** True when a successor state breaks one of the action's constraints: the
|
|
251
|
+
* left and right members sharing a position under one of the action's
|
|
252
|
+
* effect predicates while the guard member stands elsewhere. */
|
|
253
|
+
function constraintViolated(action, nextState, domain) {
|
|
254
|
+
if (!action.constraints.length) return false;
|
|
255
|
+
const predicates = [...new Set(action.effects.map((e) => e.predicate))];
|
|
256
|
+
for (const constraint of action.constraints) {
|
|
257
|
+
const left = (domain.classMembers[constraint.left] || [])[0];
|
|
258
|
+
const right = (domain.classMembers[constraint.right] || [])[0];
|
|
259
|
+
const guard = (domain.classMembers[constraint.guard] || [])[0];
|
|
260
|
+
for (const predicate of predicates) {
|
|
261
|
+
const leftAt = positionIn(nextState, left, predicate);
|
|
262
|
+
if (!leftAt || positionIn(nextState, right, predicate) !== leftAt) continue;
|
|
263
|
+
if (positionIn(nextState, guard, predicate) !== leftAt) return true;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
192
269
|
/** Every legal grounded action from `state`, with its successor.
|
|
193
270
|
* Deterministic: actions, signatures, and members are walked sorted. */
|
|
194
271
|
export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
@@ -214,8 +291,10 @@ export function movesFromRules(state, domain, { budget = 5000 } = {}) {
|
|
|
214
291
|
if (!precondHolds(precond, subject, target, state, domain)) { ok = false; break; }
|
|
215
292
|
}
|
|
216
293
|
if (!ok) continue;
|
|
217
|
-
|
|
294
|
+
if (!companionsCoLocated(action, subject, target, state, domain)) continue;
|
|
295
|
+
const nextState = applyEffects(action.effects, subject, target, state, domain);
|
|
218
296
|
if (!nextState) continue;
|
|
297
|
+
if (constraintViolated(action, nextState, domain)) continue;
|
|
219
298
|
out.push({
|
|
220
299
|
action: {
|
|
221
300
|
name: action.name,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
CONTRACTIONS, MISSPELLINGS, WRONG_WORDS, G_DROP, FILLER_WORDS,
|
|
10
10
|
NEGATION_FRAMES, COMMIT_CONTENT_FRAMES, VERB_TO_KIND, ENTITY_TO_TYPE,
|
|
11
|
+
TRAILING_SCOPE_FILLER,
|
|
11
12
|
} from "../ask-vocab.mjs";
|
|
12
13
|
|
|
13
14
|
export function escapeRegex(s) {
|
|
@@ -146,6 +147,21 @@ const EXPLAIN_WRAPPER_RE = /^explain\s+(?:to\s+me\s+|please\s+)*(.+?)\??$/i;
|
|
|
146
147
|
/** "tell me <Q>" (bare, no "about") -> "<Q>"; "tell me about X" is a
|
|
147
148
|
* different, untouched territory (chat.mjs's vagueTouchTermOf). */
|
|
148
149
|
const TELL_ME_WRAPPER_RE = /^tell\s+me\s+(.+?)\??$/i;
|
|
150
|
+
/** "do you know <Q>" -> "<Q>", gated on an interrogative remainder — so
|
|
151
|
+
* "do you know anything about movies" (small-talk, no embedded question)
|
|
152
|
+
* passes through untouched. */
|
|
153
|
+
const KNOW_WRAPPER_RE = /^do\s+you\s+know\s+(.+?)\??$/i;
|
|
154
|
+
/** "i'd like to know <Q>" / "i want to know <Q>" -> "<Q>", same
|
|
155
|
+
* interrogative-remainder gate as KNOW_WRAPPER_RE. */
|
|
156
|
+
const WANT_KNOW_WRAPPER_RE = /^i(?:'d|\s+would)?\s+(?:like|want|need)\s+to\s+know\s+(.+?)\??$/i;
|
|
157
|
+
/** EMBEDDED-QUESTION DE-INVERSION: the wrappers above unwrap "could you
|
|
158
|
+
* tell me what a dog is" down to the embedded clause "what a dog is",
|
|
159
|
+
* which keeps declarative word order — nothing downstream parses it. Fold
|
|
160
|
+
* it back to the direct question the meta lane already owns. Deliberately
|
|
161
|
+
* closed to a short (≤3-word) subject so an ordinary relative clause
|
|
162
|
+
* ("what the parser does with X …") is never re-inverted. */
|
|
163
|
+
const EMBEDDED_WHATIS_RE = /^what\s+((?:an?\s+|the\s+)?[\w'-]+(?:\s+[\w'-]+){0,2})\s+(is|are)\??$/i;
|
|
164
|
+
const EMBEDDED_MEANS_RE = /^what\s+((?:an?\s+|the\s+)?[\w'-]+(?:\s+[\w'-]+){0,2})\s+means\??$/i;
|
|
149
165
|
/** show/give-me presentation bridge: a kind-listing remainder is left
|
|
150
166
|
* untouched, a relation/interrogative remainder unwraps to itself, anything
|
|
151
167
|
* else bridges to "describe <thing>". */
|
|
@@ -194,6 +210,14 @@ export function applyPreambleFrames(text) {
|
|
|
194
210
|
if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
|
|
195
211
|
m = q.match(TELL_ME_WRAPPER_RE);
|
|
196
212
|
if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
|
|
213
|
+
m = q.match(KNOW_WRAPPER_RE);
|
|
214
|
+
if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
|
|
215
|
+
m = q.match(WANT_KNOW_WRAPPER_RE);
|
|
216
|
+
if (m && INTERROGATIVE_LEAD_RE.test(m[1].trim())) q = m[1].trim();
|
|
217
|
+
m = q.match(EMBEDDED_WHATIS_RE);
|
|
218
|
+
if (m) q = `what ${m[2].toLowerCase()} ${m[1].trim()}`;
|
|
219
|
+
m = q.match(EMBEDDED_MEANS_RE);
|
|
220
|
+
if (m) q = `what does ${m[1].trim()} mean`;
|
|
197
221
|
m = q.match(SHOW_GIVE_ME_RE);
|
|
198
222
|
if (m) {
|
|
199
223
|
const rest = m[1].trim();
|
|
@@ -387,6 +411,28 @@ export const PHRASING_FRAMES = Object.freeze([
|
|
|
387
411
|
// a bare "is").
|
|
388
412
|
{ re: /^were\s+is\s+(?:the\s+)?(.+?)\s+(defined|declared|located|implemented)\??$/i, to: (m) => `where is ${m[1]} ${m[2]}` },
|
|
389
413
|
|
|
414
|
+
// DESCRIBE PARAPHRASES ("what is the purpose of X", "what does X do in
|
|
415
|
+
// this codebase") → the meta/whatis shape ("what is a <term>"), which
|
|
416
|
+
// already answers a unique code entity via metaFallbackEntityAnswer. The
|
|
417
|
+
// term slot refuses an a/an article or a pronoun lead so the vocabulary
|
|
418
|
+
// phrasings ("what is the purpose of a horse", "what does it do here")
|
|
419
|
+
// pass through untouched to their own memory-facts and context readers,
|
|
420
|
+
// which read the raw text and must keep their turn. A leading "the" is
|
|
421
|
+
// entity-term noise (mirrors resolveObject's own article strip). The
|
|
422
|
+
// sibling "what is X for" paraphrase is deliberately NOT a frame: chat's
|
|
423
|
+
// module-overview lane owns that phrasing and gates on an ask() miss, so
|
|
424
|
+
// it lives as ask()'s own miss-gated fallback (WHATIS_FOR_FALLBACK_RE)
|
|
425
|
+
// instead, adopted only when the meta reading actually answers.
|
|
426
|
+
{ re: /^what\s+is\s+the\s+purpose\s+of\s+(?:the\s+)?(?!(?:an?|it|this|that|these|those)\s)(.+?)\??$/i, to: (m) => `what is a ${m[1]}` },
|
|
427
|
+
// Scoped form only: bare "what does X do" stays unrewritten — the chat
|
|
428
|
+
// surface's module-grain overview lane owns it and only gets its turn when
|
|
429
|
+
// ask() misses, so claiming it here would swap that richer answer for the
|
|
430
|
+
// one-line meta fallback.
|
|
431
|
+
{
|
|
432
|
+
re: new RegExp(`^what\\s+does\\s+(?:the\\s+)?(?!(?:an?|it|this|that|these|those)\\s)(.+?)\\s+do\\s+(?:${TRAILING_SCOPE_FILLER.map(escapeRegex).join("|")})\\??$`, "i"),
|
|
433
|
+
to: (m) => `what is a ${m[1]}`,
|
|
434
|
+
},
|
|
435
|
+
|
|
390
436
|
// PREDICATIVE QUALIFIER ("which modules are untested") → the ATTRIBUTIVE form
|
|
391
437
|
// ("untested modules") the grammar already answers. The QUALIFIER must sit
|
|
392
438
|
// immediately after are/is, so "…are NOT tested" keeps its own set-complement handler.
|
|
@@ -92,13 +92,14 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
92
92
|
verbHit = findPhrase(lemmaWords, VERB_TO_KIND);
|
|
93
93
|
if (verbHit) canonWords = lemmaWords;
|
|
94
94
|
}
|
|
95
|
+
let fuzzyVerb = false;
|
|
95
96
|
if (!verbHit) {
|
|
96
97
|
// tier 3: bounded-edit-distance rewrite toward verb/modifier keywords only
|
|
97
98
|
// ("impotr" -> "import"); ≥4-char words only — below that the bound covers
|
|
98
99
|
// half of English (and "and" is 1 edit from the "land in" constituent).
|
|
99
100
|
const fuzzyWords = lcWords.map((w) => (w.length >= 4 && eligibleForCanon(w) ? fuzzyVocabWord(w) || w : w));
|
|
100
101
|
verbHit = findPhrase(fuzzyWords, VERB_TO_KIND);
|
|
101
|
-
if (verbHit) canonWords = fuzzyWords;
|
|
102
|
+
if (verbHit) { canonWords = fuzzyWords; fuzzyVerb = true; }
|
|
102
103
|
}
|
|
103
104
|
if (!verbHit && lcWords.includes("by")) {
|
|
104
105
|
// A participle with no active verb entry still marks a passive when a passive
|
|
@@ -109,6 +110,9 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
if (!verbHit) return null;
|
|
113
|
+
// A tier-3 verb is a REPAIR, not a reading — downstream consumers (the teach
|
|
114
|
+
// lane's canonical receipt) need to know the difference, so it rides the AST.
|
|
115
|
+
const stamp = (ast) => (fuzzyVerb ? { ...ast, fuzzyVerb: true } : ast);
|
|
112
116
|
// POS rescue (Node-side only): a relation word used as a NOUN in a "the
|
|
113
117
|
// <imports> of <term>" frame would otherwise misparse; only fires inside this
|
|
114
118
|
// exact det+NOUN+"of" shape, since the same word tags NOUN in genuine verb use too.
|
|
@@ -119,7 +123,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
119
123
|
const tags = nlp.posTags(words);
|
|
120
124
|
if (tags[i] === "NOUN") {
|
|
121
125
|
const objText = words.slice(i + 2).filter((w, j) => !STOPWORDS.has(lcWords[i + 2 + j])).join(" ").trim();
|
|
122
|
-
if (objText) return { shape: "forward", entityType: null, modifier: "direct", kind: verbHit.kind, object: objText };
|
|
126
|
+
if (objText) return stamp({ shape: "forward", entityType: null, modifier: "direct", kind: verbHit.kind, object: objText });
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
}
|
|
@@ -154,7 +158,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
154
158
|
// "when" turns a touches decomposition temporal; other verbs fall through.
|
|
155
159
|
if (kind === "touches" && lcWords.includes("when")) {
|
|
156
160
|
const objText = beforeText || afterText;
|
|
157
|
-
if (objText) return { shape: "when", entityType: null, modifier: "direct", kind: "touches", object: objText };
|
|
161
|
+
if (objText) return stamp({ shape: "when", entityType: null, modifier: "direct", kind: "touches", object: objText });
|
|
158
162
|
}
|
|
159
163
|
|
|
160
164
|
// "who last touched X" would otherwise list every touching commit's author,
|
|
@@ -162,7 +166,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
162
166
|
// stopwords and wouldn't survive into beforeText/afterText.
|
|
163
167
|
if (kind === "touches" && lcWords.includes("who") && lcWords.includes("last")) {
|
|
164
168
|
const objText = beforeText || afterText;
|
|
165
|
-
if (objText) return { shape: "whoLast", entityType: null, modifier: "direct", kind: "touches", object: objText };
|
|
169
|
+
if (objText) return stamp({ shape: "whoLast", entityType: null, modifier: "direct", kind: "touches", object: objText });
|
|
166
170
|
}
|
|
167
171
|
|
|
168
172
|
// Reversible passive ("PATIENT is VERBed BY AGENT"): a passive auxiliary plus a
|
|
@@ -190,7 +194,7 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
190
194
|
nextAfterBy = lcWords[i]; break;
|
|
191
195
|
}
|
|
192
196
|
const agentNamed = nextAfterBy != null && !WH_WORDS.has(nextAfterBy) && !ENTITY_TO_TYPE[nextAfterBy];
|
|
193
|
-
return { shape: agentNamed ? "forward" : "reverse", entityType, modifier, kind, object };
|
|
197
|
+
return stamp({ shape: agentNamed ? "forward" : "reverse", entityType, modifier, kind, object });
|
|
194
198
|
}
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -201,19 +205,19 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
201
205
|
let subject = beforeText;
|
|
202
206
|
let object = afterText;
|
|
203
207
|
if (INHERITS_REVERSE_VERBS.includes(verbPhrase)) [subject, object] = [object, subject];
|
|
204
|
-
return { shape: "ask", entityType: null, modifier: "direct", kind, subject, object };
|
|
208
|
+
return stamp({ shape: "ask", entityType: null, modifier: "direct", kind, subject, object });
|
|
205
209
|
}
|
|
206
|
-
if (afterText) return { shape: "reverse", entityType, modifier, kind, object: afterText };
|
|
210
|
+
if (afterText) return stamp({ shape: "reverse", entityType, modifier, kind, object: afterText });
|
|
207
211
|
// "what is a kind of class": when the object is itself an entity-type noun, the
|
|
208
212
|
// entity match swallows the whole post-verb span as a grain qualifier, leaving
|
|
209
213
|
// afterText empty — re-read that span as the object instead.
|
|
210
214
|
if (kind === "inherits" && !beforeText && entityHit && entityHit.start === verbHit.end) {
|
|
211
215
|
const entityText = canonWords.slice(entityHit.start, entityHit.end).join(" ");
|
|
212
|
-
if (entityText) return { shape: "reverse", entityType: null, modifier, kind, object: entityText };
|
|
216
|
+
if (entityText) return stamp({ shape: "reverse", entityType: null, modifier, kind, object: entityText });
|
|
213
217
|
}
|
|
214
218
|
// forward keeps the spotted entityType (traverse()'s commit-as-subject grain
|
|
215
219
|
// selection); modifier stays hardcoded since no forward closure traversal exists.
|
|
216
|
-
if (beforeText) return { shape: "forward", entityType, modifier: "direct", kind, object: beforeText };
|
|
220
|
+
if (beforeText) return stamp({ shape: "forward", entityType, modifier: "direct", kind, object: beforeText });
|
|
217
221
|
return null;
|
|
218
222
|
}
|
|
219
223
|
|
package/src/ledger-viz.mjs
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
// ledger-viz.mjs — `tmct viz
|
|
2
|
-
//
|
|
1
|
+
// ledger-viz.mjs — `tmct viz`: the memory graph as a readable ledger of
|
|
2
|
+
// fact-sentences around one focus term, with the in-page chat dock
|
|
3
|
+
// (PLAN_VIZ_LEDGER.md).
|
|
3
4
|
//
|
|
4
|
-
//
|
|
5
|
+
// Three pure/impure-separated pieces:
|
|
5
6
|
// - computeLedgerData(repoDir, opts) — I/O (loadMemory) + derivation
|
|
6
7
|
// - computeLedgerDataFromPayload(payload) — the pure derivation half
|
|
7
8
|
// - renderLedgerHtml(data) — pure string builder, one
|
|
8
9
|
// self-contained document, no external requests.
|
|
10
|
+
// readMemoryAskBundle() is the one extra bit of I/O renderLedgerHtml itself
|
|
11
|
+
// doesn't do: it reads the checked-in chat-dock engine bundle.
|
|
9
12
|
|
|
10
13
|
import { loadMemory, readFactRows, findContradictions, normFactTerm } from "./memory/core.mjs";
|
|
11
|
-
import { escapeHtml, embedJson } from "./viz.mjs";
|
|
12
|
-
import {
|
|
14
|
+
import { THEME_TOKENS_CSS, SERIF_STACK, MONO_STACK, escapeHtml, embedJson } from "./viz-theme.mjs";
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
13
18
|
|
|
14
19
|
export const LEDGER_ROW_LIMIT_DEFAULT = 20000;
|
|
15
20
|
|
|
21
|
+
/** Read the checked-in browser memory-ask-engine bundle
|
|
22
|
+
* (`src/memory-ask-browser.bundle.js`) — the real memory-graph answer engine
|
|
23
|
+
* (chat.mjs's factAnswer/factReadBack) the chat dock runs on. Returns `""`,
|
|
24
|
+
* never throws, if the bundle hasn't been built — the page then renders with
|
|
25
|
+
* an honest "chat unavailable" note instead of a dock. */
|
|
26
|
+
export async function readMemoryAskBundle() {
|
|
27
|
+
try {
|
|
28
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
return await readFile(join(here, "memory-ask-browser.bundle.js"), "utf8");
|
|
30
|
+
} catch {
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
// Predicate rendering + family grouping. A closed table with a verbatim
|
|
17
36
|
// fallback: an unknown predicate still reads as itself, never breaks the page.
|
|
18
37
|
const PHRASES = new Map([
|
|
@@ -122,7 +141,7 @@ export function computeLedgerDataFromPayload(payload, { focus, term, rowLimit =
|
|
|
122
141
|
|
|
123
142
|
// Focus: the asked term when it resolves; otherwise the newest taught
|
|
124
143
|
// row's subject, then the highest-degree term. A miss never seeds a
|
|
125
|
-
// phantom term
|
|
144
|
+
// phantom term.
|
|
126
145
|
let focusTerm = null;
|
|
127
146
|
const asked = focus || term;
|
|
128
147
|
if (asked) {
|
|
@@ -177,8 +196,7 @@ export async function computeLedgerData(repoDir, opts = {}) {
|
|
|
177
196
|
return computeLedgerDataFromPayload(payload, opts);
|
|
178
197
|
}
|
|
179
198
|
|
|
180
|
-
/** The chat dock's answer-to-focus resolver (
|
|
181
|
-
* retargeted at the ledger term index). Pass 1: earliest term label (≥3
|
|
199
|
+
/** The chat dock's answer-to-focus resolver. Pass 1: earliest term label (≥3
|
|
182
200
|
* chars, space-boundary) appearing in the ANSWER text. Pass 2: strip the
|
|
183
201
|
* QUESTION's crust and try the remainder as one normalized term. Returns a
|
|
184
202
|
* term string or null. Self-contained on purpose: its source is injected
|
|
@@ -309,6 +327,7 @@ ${THEME_TOKENS_CSS}
|
|
|
309
327
|
.chatlog .u::before { content: "tmct> "; color: var(--taught); }
|
|
310
328
|
.chatlog .a { font-size: .9rem; line-height: 1.45; }
|
|
311
329
|
.chatlog .a.miss { color: var(--muted); }
|
|
330
|
+
.chatlog .a.goal { font-family: ${MONO_STACK}; font-size: .72rem; color: var(--muted); }
|
|
312
331
|
.chatask { display: flex; align-items: center; gap: .5rem; }
|
|
313
332
|
.chatlog:not(:empty) + .chatask { border-top: 1px solid var(--line); margin-top: .55rem; padding-top: .55rem; }
|
|
314
333
|
.chatask .prompt { color: var(--taught); font-size: .78rem; }
|
|
@@ -424,7 +443,7 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
424
443
|
}
|
|
425
444
|
function renderLedger() {
|
|
426
445
|
const box = el("ledger");
|
|
427
|
-
if (!focus) { box.innerHTML = '<div class="empty">Nothing in memory yet. Teach a fact in chat, then re-run tmct viz
|
|
446
|
+
if (!focus) { box.innerHTML = '<div class="empty">Nothing in memory yet. Teach a fact in chat, then re-run tmct viz.</div>'; return; }
|
|
428
447
|
const all = LEDGER.rows.filter((r) => touches(r, focus));
|
|
429
448
|
const mine = all.filter((r) => passes(r, null));
|
|
430
449
|
const srcs = new Set(all.map((r) => r.src.split(" | ")[0]));
|
|
@@ -585,6 +604,9 @@ ${hasMemChat ? `<script>\n${bundleStr}\n</script>` : ""}
|
|
|
585
604
|
}
|
|
586
605
|
if (fact && fact.text) {
|
|
587
606
|
addLine("a", esc(fact.text).replace(/\\n/g, "<br>"));
|
|
607
|
+
// Same formatting contract as chat's withGoalLine: capitalized,
|
|
608
|
+
// full-stop-terminated, rendered only when the engine deduced one.
|
|
609
|
+
if (fact.goal) addLine("a goal", "Goal (inferred): " + esc(fact.goal.charAt(0).toUpperCase() + fact.goal.slice(1)) + ".");
|
|
588
610
|
const hit = resolveAnsweredTerm(fact.text, q, LEDGER.terms, tmctMemoryAsk.normFactTerm);
|
|
589
611
|
if (hit) refocusWithLabel(hit, q);
|
|
590
612
|
} else {
|
package/src/memory/core.mjs
CHANGED
|
@@ -1173,9 +1173,11 @@ export const RULE_KIND_RECURSIVE = "recursive";
|
|
|
1173
1173
|
export const RULE_KIND_ACTION_SIGNATURE = "action-signature";
|
|
1174
1174
|
export const RULE_KIND_ACTION_PRECOND = "action-precond";
|
|
1175
1175
|
export const RULE_KIND_ACTION_EFFECT = "action-effect";
|
|
1176
|
+
export const RULE_KIND_ACTION_CONSTRAINT = "action-constraint";
|
|
1176
1177
|
export const RULE_KINDS = Object.freeze([
|
|
1177
1178
|
RULE_KIND_COMPOSE2, RULE_KIND_FILTER, RULE_KIND_RECURSIVE,
|
|
1178
1179
|
RULE_KIND_ACTION_SIGNATURE, RULE_KIND_ACTION_PRECOND, RULE_KIND_ACTION_EFFECT,
|
|
1180
|
+
RULE_KIND_ACTION_CONSTRAINT,
|
|
1179
1181
|
]);
|
|
1180
1182
|
|
|
1181
1183
|
export const RULE_NAME_PROP = "mgx:ruleName";
|
|
@@ -1203,6 +1205,12 @@ const RULE_SLOT_SPEC = {
|
|
|
1203
1205
|
["predicate", "mgx:ruleActionEffectPredicate"], ["subjectRole", "mgx:ruleActionEffectSubject"],
|
|
1204
1206
|
["objectRole", "mgx:ruleActionEffectObject"],
|
|
1205
1207
|
],
|
|
1208
|
+
// "the <left> may not be with the <right> without the <guard>" — each slot
|
|
1209
|
+
// names a class whose sole member src/domain.mjs resolves at compile time.
|
|
1210
|
+
[RULE_KIND_ACTION_CONSTRAINT]: [
|
|
1211
|
+
["left", "mgx:ruleActionConstraintLeft"], ["right", "mgx:ruleActionConstraintRight"],
|
|
1212
|
+
["guard", "mgx:ruleActionConstraintGuard"],
|
|
1213
|
+
],
|
|
1206
1214
|
};
|
|
1207
1215
|
|
|
1208
1216
|
// Content-addressed over (kind, name, ...slots in RULE_SLOT_SPEC order),
|
|
@@ -1568,13 +1576,24 @@ export async function removeFacts(dir, ids) {
|
|
|
1568
1576
|
* contradiction (below it the fact is too weak to contradict anything). */
|
|
1569
1577
|
export const CONTRADICTION_TRUST_FLOOR = 0.5;
|
|
1570
1578
|
|
|
1579
|
+
export const HAS_A_PREDICATE = "mgx:hasA";
|
|
1580
|
+
export const CAPABLE_OF_PREDICATE = "mgx:capableOf";
|
|
1581
|
+
|
|
1582
|
+
/** Predicates whose real-world semantics allow many objects at once ("a dog
|
|
1583
|
+
* has legs" AND "a dog has a tail"; "a bird can fly" AND "a bird can sing"),
|
|
1584
|
+
* so a second object is a second fact, never a disagreement. A closed list:
|
|
1585
|
+
* every predicate outside it keeps the full contradiction contract. */
|
|
1586
|
+
export const MULTI_VALUED_PREDICATES = new Set([HAS_A_PREDICATE, CAPABLE_OF_PREDICATE]);
|
|
1587
|
+
|
|
1571
1588
|
/** Facts that CONTRADICT: same (subject, predicate), different object, each
|
|
1572
1589
|
* above the trust floor. Returns groups (trust-desc) so callers surface both,
|
|
1573
|
-
* never silently pick one. Same (s,p,o) is corroboration, not contradiction
|
|
1590
|
+
* never silently pick one. Same (s,p,o) is corroboration, not contradiction,
|
|
1591
|
+
* and a MULTI_VALUED_PREDICATES predicate never contradicts on object count. */
|
|
1574
1592
|
export function findContradictions(memory, { floor = CONTRADICTION_TRUST_FLOOR } = {}) {
|
|
1575
1593
|
const rows = readFactRows(memory).filter((r) => r.trust >= floor);
|
|
1576
1594
|
const byKey = new Map();
|
|
1577
1595
|
for (const r of rows) {
|
|
1596
|
+
if (MULTI_VALUED_PREDICATES.has(r.predicate)) continue;
|
|
1578
1597
|
const key = `${r.subject} ${r.predicate}`;
|
|
1579
1598
|
if (!byKey.has(key)) byKey.set(key, []);
|
|
1580
1599
|
byKey.get(key).push(r);
|
package/src/memory/shacl.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
const MEMORY_CLASSES = new Set(["Utterance", "Fact", "Session", "Source", "Rule"]);
|
|
9
9
|
const RULE_KINDS = new Set([
|
|
10
10
|
"compose2", "filter", "recursive",
|
|
11
|
-
"action-signature", "action-precond", "action-effect",
|
|
11
|
+
"action-signature", "action-precond", "action-effect", "action-constraint",
|
|
12
12
|
]);
|
|
13
13
|
|
|
14
14
|
// Mirrors core.mjs's own (unexported) RULE_SLOT_SPEC exactly — the single
|
|
@@ -26,6 +26,9 @@ const RULE_SLOT_PROPS = {
|
|
|
26
26
|
"action-effect": [
|
|
27
27
|
"mgx:ruleActionEffectPredicate", "mgx:ruleActionEffectSubject", "mgx:ruleActionEffectObject",
|
|
28
28
|
],
|
|
29
|
+
"action-constraint": [
|
|
30
|
+
"mgx:ruleActionConstraintLeft", "mgx:ruleActionConstraintRight", "mgx:ruleActionConstraintGuard",
|
|
31
|
+
],
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
function attrValue(ind, prop) {
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
// memory-ask-browser-entry.mjs — the esbuild entry for
|
|
2
|
-
//
|
|
1
|
+
// memory-ask-browser-entry.mjs — the esbuild entry for the ledger page's
|
|
2
|
+
// chat dock (`tmct viz`, src/ledger-viz.mjs).
|
|
3
3
|
//
|
|
4
4
|
// Re-exports `factAnswer` (src/chat.mjs) and `createInMemoryStore`
|
|
5
|
-
// (src/memory/core.mjs), which lets the
|
|
5
|
+
// (src/memory/core.mjs), which lets the dock hand `factAnswer` the page's
|
|
6
6
|
// already-embedded payload with zero fs I/O. Called with `envelope: null,
|
|
7
7
|
// miss: true` to arm factAnswer's bare-question regex fallbacks directly,
|
|
8
|
-
// bypassing the structural-graph parse pipeline this
|
|
8
|
+
// bypassing the structural-graph parse pipeline this dock has no use for.
|
|
9
9
|
import { factAnswer, factReadBack } from "./chat.mjs";
|
|
10
10
|
import { createInMemoryStore, normFactTerm } from "./memory/core.mjs";
|
|
11
11
|
|
|
12
|
-
// normFactTerm is re-exported too, for
|
|
12
|
+
// normFactTerm is re-exported too, for the page's client-side term normalization.
|
|
13
13
|
// factReadBack carries the taught-relation chases (grandfather-style questions)
|
|
14
14
|
// that factAnswer's own lanes don't reach.
|
|
15
15
|
globalThis.tmctMemoryAsk = { factAnswer, factReadBack, createInMemoryStore, normFactTerm };
|