@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +37 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@polycode-projects/the-mechanical-code-talker",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The Mechanical Code Talker (tmct) — a tolerant, offline, $0 chat surface that guides you toward precision queries about a software repository. ELIZA/PARRY-style but domain-obsessed with code. No model calls; indexes a repo on request (tmct index) or reads any producer's graph.",
|
|
@@ -172,6 +172,26 @@ export function createInMemoryStore() {
|
|
|
172
172
|
return { backend: BACKEND_MEMORY, payload: emptyMemory() };
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/** Assign `seedPayload` onto `memoryDir`'s own payload — spread over the
|
|
176
|
+
* store's own (possibly empty) payload rather than replacing it outright,
|
|
177
|
+
* so a partial seed (individuals and objectProperties only) still carries
|
|
178
|
+
* the classes/prefixes scaffolding the write path recounts, and a later
|
|
179
|
+
* teach turn works regardless of what the seed carries. A no-op when
|
|
180
|
+
* `seedPayload` is null/undefined — a browser session with nothing to seed
|
|
181
|
+
* keeps its own fresh empty payload untouched. */
|
|
182
|
+
export function applySeedPayload(memoryDir, seedPayload) {
|
|
183
|
+
if (seedPayload) memoryDir.payload = { ...memoryDir.payload, ...seedPayload };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A structurally independent copy of a memory payload — `structuredClone`
|
|
187
|
+
* where available, falling back to a JSON round-trip for an environment
|
|
188
|
+
* without it. Returns null for a null/undefined `payload`, so a caller can
|
|
189
|
+
* seed a fresh session with "no seed yet" rather than an empty object. */
|
|
190
|
+
export function cloneMemoryPayload(payload) {
|
|
191
|
+
if (!payload) return null;
|
|
192
|
+
try { return structuredClone(payload); } catch { return JSON.parse(JSON.stringify(payload)); }
|
|
193
|
+
}
|
|
194
|
+
|
|
175
195
|
// ---- Backend C — SQLite: a live node:sqlite connection, per-row
|
|
176
196
|
// INSERT/REPLACE/DELETE diffed against what's already stored (write cost
|
|
177
197
|
// proportional to what changed, not total store size). Reads are cached on
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -189,6 +189,77 @@ export const RELATIONS = {
|
|
|
189
189
|
},
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
// ---- world relations: the predicates a WORLD graph stores about its own
|
|
193
|
+
// individuals, as opposed to RELATIONS above, which is a code vocabulary —
|
|
194
|
+
// a module imports a module, a commit touches a file, and none of its verbs
|
|
195
|
+
// name where a thing is or how it feels. A game board, a burrow and a manor
|
|
196
|
+
// all record those, in the same `mgx:` predicates, so the words for them are
|
|
197
|
+
// curated once here.
|
|
198
|
+
//
|
|
199
|
+
// Each entry is keyed by its stored predicate and carries the LISTING NOUNS a
|
|
200
|
+
// question reaches it by ("the locations of…", "the moods of…") plus `reads`,
|
|
201
|
+
// the third-person phrase one subject's answer is written in. No `verbs` list
|
|
202
|
+
// and no VERB_TO_KIND entry: these answer a listing over a named set, not the
|
|
203
|
+
// "does X <verb> Y" shapes RELATIONS' verbs compile to, and folding them in
|
|
204
|
+
// would put "is in" into the code grammar's own verb table. ----
|
|
205
|
+
|
|
206
|
+
export const WORLD_RELATIONS = Object.freeze({
|
|
207
|
+
placement: Object.freeze({
|
|
208
|
+
predicate: "mgx:currently-in",
|
|
209
|
+
comment: "individual -> place: where the subject is right now.",
|
|
210
|
+
nouns: Object.freeze(["location", "locations", "position", "positions", "place", "places", "whereabouts", "room", "rooms"]),
|
|
211
|
+
reads: "is in",
|
|
212
|
+
}),
|
|
213
|
+
mood: Object.freeze({
|
|
214
|
+
predicate: "mgx:feels",
|
|
215
|
+
comment: "individual -> mood word: how the subject feels right now.",
|
|
216
|
+
nouns: Object.freeze(["mood", "moods", "feeling", "feelings", "emotion", "emotions"]),
|
|
217
|
+
reads: "feels",
|
|
218
|
+
}),
|
|
219
|
+
mass: Object.freeze({
|
|
220
|
+
predicate: "mgx:mass",
|
|
221
|
+
comment: "individual -> number: the subject's current mass.",
|
|
222
|
+
nouns: Object.freeze(["mass", "masses", "weight", "weights"]),
|
|
223
|
+
reads: "has mass",
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/** listing noun -> WORLD_RELATIONS key ("locations" -> "placement"). */
|
|
228
|
+
export const WORLD_NOUN_TO_RELATION = Object.freeze(Object.fromEntries(
|
|
229
|
+
Object.entries(WORLD_RELATIONS).flatMap(([token, { nouns }]) => nouns.map((n) => [n, token])),
|
|
230
|
+
));
|
|
231
|
+
|
|
232
|
+
/** Every stored world predicate, for a caller projecting fact rows into a
|
|
233
|
+
* graph `ask` can traverse. */
|
|
234
|
+
export const WORLD_PREDICATES = Object.freeze(
|
|
235
|
+
Object.values(WORLD_RELATIONS).map((r) => r.predicate),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// The regular English 3rd-person-singular suffix rule (the same regular
|
|
239
|
+
// -s/-es/-ies shape src/domain/inflect.mjs's own `pluralOf` applies to a
|
|
240
|
+
// noun) — not imported from there, since inflect.mjs sits downstream of
|
|
241
|
+
// interpret/normalize.mjs, which imports THIS module: importing it back
|
|
242
|
+
// here would be a real circular dependency, not just a style choice.
|
|
243
|
+
function thirdPersonSingular(w) {
|
|
244
|
+
if (/(?:[sxz]|ch|sh)$/i.test(w)) return `${w}es`;
|
|
245
|
+
if (/[^aeiou]y$/i.test(w)) return `${w.slice(0, -1)}ies`;
|
|
246
|
+
return `${w}s`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** The third-person-singular sentence phrase for a stored relation `kind`
|
|
250
|
+
* ("inherits" -> "inherits from", "cochange" -> "co-changes with"), derived
|
|
251
|
+
* from RELATIONS' own `bare` infinitive rather than a second hand-curated
|
|
252
|
+
* verb table. A symbol-grain kind ("callsSymbol", "touchesSymbol") reads its
|
|
253
|
+
* coarse sibling's phrase; an unrecognized kind falls back to a plain
|
|
254
|
+
* camelCase split. */
|
|
255
|
+
export function phraseForRelation(kind) {
|
|
256
|
+
const key = String(kind || "");
|
|
257
|
+
const entry = RELATIONS[key] || (key.endsWith("Symbol") && RELATIONS[key.slice(0, -6)]) || null;
|
|
258
|
+
if (!entry) return key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
259
|
+
const [head, ...rest] = entry.bare.split(" ");
|
|
260
|
+
return [thirdPersonSingular(head), ...rest].join(" ");
|
|
261
|
+
}
|
|
262
|
+
|
|
192
263
|
/** The closed set of reverse `inherits` verb phrasings a strategy checks to
|
|
193
264
|
* decide whether to swap subject/object. The three "the"-definite forms are
|
|
194
265
|
* named here but not reachable through VERB_TO_KIND (see above). */
|
package/src/domain/ask.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
PASSIVE_PARTICIPLE_TO_KIND, GENERIC_AGENT_WORDS, REDUCED_RELATIVE_CLAUSES,
|
|
27
27
|
AGGREGATE_TRIGGERS, LIST_TRIGGERS, SUPERLATIVE_EXTREMES, EDGE_NOUN_TO_METRIC, METRIC_IMPLIES_ENTITY, ANAPHORA_TRIGGERS,
|
|
28
28
|
MEMBERSHIP_KINDS, CASCADE_NOISE, CASCADE_SYNONYMS, HELP_TRIGGERS,
|
|
29
|
+
WORLD_RELATIONS, WORLD_NOUN_TO_RELATION, WORLD_PREDICATES,
|
|
29
30
|
stripTrailingScopeFiller,
|
|
30
31
|
} from "./ask-vocab.mjs";
|
|
31
32
|
import { expandContractions, normalizeQuery, applyNegationFrames, applyPhrasingFrames, matchNegationSet, STOPWORDS, splitWords, wordsOf, escapeRegex } from "./interpret/normalize.mjs";
|
|
@@ -2135,6 +2136,7 @@ function evalComposite(graph, ast, opts = {}) {
|
|
|
2135
2136
|
return { compositeKind: "count", count: evalSet(graph, ast.base, opts).length, entityType: ast.entityType, matches: [] };
|
|
2136
2137
|
}
|
|
2137
2138
|
if (ast.node === "list") return { compositeKind: "list", matches: evalSet(graph, ast.base, opts), entityType: ast.entityType, scoped: ast.scoped };
|
|
2139
|
+
if (ast.node === "worldRelation") return evalWorldRelation(graph, ast);
|
|
2138
2140
|
if (ast.node === "superlative") return evalSuperlative(graph, ast);
|
|
2139
2141
|
if (ast.node === "temporal") return evalTemporal(graph, ast, opts);
|
|
2140
2142
|
if (ast.node === "recentCommits") return evalRecentCommits(graph);
|
|
@@ -2292,6 +2294,20 @@ function renderComposite(parsed, result, graph) {
|
|
|
2292
2294
|
: "";
|
|
2293
2295
|
return { content: `${compositeList(result.matches)}${hint}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
2294
2296
|
}
|
|
2297
|
+
// One sentence per subject, in id order, in the world's own words — the
|
|
2298
|
+
// subject's id, the relation's `reads` phrase, and the stored object
|
|
2299
|
+
// verbatim. Nothing is derived: an empty set is the honest miss, never a
|
|
2300
|
+
// sentence about a subject the world has no row for.
|
|
2301
|
+
if (result.compositeKind === "worldRelation") {
|
|
2302
|
+
const asked = listJoin(result.askedClasses);
|
|
2303
|
+
const noun = WORLD_RELATIONS[result.relation].nouns[1];
|
|
2304
|
+
if (!result.pairs.length) {
|
|
2305
|
+
return { content: `no ${noun} on record for ${asked} in this graph.`, miss: true, ambiguous: false, matches: [] };
|
|
2306
|
+
}
|
|
2307
|
+
const reads = WORLD_RELATIONS[result.relation].reads;
|
|
2308
|
+
const sentences = result.pairs.map((p) => `${p.subject.label || p.subject.id} ${reads} ${p.object}`);
|
|
2309
|
+
return { content: `${sentences.join("; ")}.`, miss: false, ambiguous: false, matches: result.matches };
|
|
2310
|
+
}
|
|
2295
2311
|
// A non-empty inherited result is disclosed out loud ("X has no own <kind>
|
|
2296
2312
|
// — inherited from <ancestor>: …"), never silently presented as the owner's own.
|
|
2297
2313
|
if (result.compositeKind === "membership") {
|
|
@@ -4403,6 +4419,148 @@ function dynamicClassQuery(graph, query) {
|
|
|
4403
4419
|
return listM ? { node: "list", entityType, base, scoped: false } : { node: "count", entityType, base };
|
|
4404
4420
|
}
|
|
4405
4421
|
|
|
4422
|
+
// ---- world-relation listing: "list the locations of flies and spiders", the
|
|
4423
|
+
// plural, multi-class form of a question a world graph can already answer. A
|
|
4424
|
+
// listing over ONE world predicate (ask-vocab.mjs's WORLD_RELATIONS) filtered
|
|
4425
|
+
// to one or more subject CLASSES, resolved dynamically against whatever
|
|
4426
|
+
// classes actually have an individual in this graph — so a world's own
|
|
4427
|
+
// taxonomy needs no entry in the closed code-graph noun table, exactly as the
|
|
4428
|
+
// count/list fallback above resolves its class noun. Fires only once the
|
|
4429
|
+
// normal cascade has already produced an honest miss, and misses honestly
|
|
4430
|
+
// itself when any asked class has no individual here. ----
|
|
4431
|
+
|
|
4432
|
+
// A class list: "flies and spiders", "spiders, flies and eggs". Each part must
|
|
4433
|
+
// be a single bare noun — a phrase with anything else in it is a restrictor
|
|
4434
|
+
// this shape can't honour, so it declines rather than dropping it.
|
|
4435
|
+
function parseWorldClassList(graph, text) {
|
|
4436
|
+
const parts = String(text || "").split(/\s*,\s*|\s+and\s+/i).map((s) => s.trim()).filter(Boolean);
|
|
4437
|
+
if (!parts.length) return null;
|
|
4438
|
+
const asked = [];
|
|
4439
|
+
const classes = [];
|
|
4440
|
+
for (const part of parts) {
|
|
4441
|
+
const word = part.replace(/^(?:all\s+)?(?:the\s+)?/i, "").trim();
|
|
4442
|
+
if (!/^[a-z][a-z'-]*$/i.test(word)) return null;
|
|
4443
|
+
const cls = resolveDynamicClass(graph, word);
|
|
4444
|
+
if (!cls) return null;
|
|
4445
|
+
asked.push(word.toLowerCase());
|
|
4446
|
+
classes.push(cls);
|
|
4447
|
+
}
|
|
4448
|
+
return { asked: [...new Set(asked)], classes: [...new Set(classes)] };
|
|
4449
|
+
}
|
|
4450
|
+
|
|
4451
|
+
const WORLD_LISTING_RE = new RegExp(
|
|
4452
|
+
`^(?:${LIST_TRIGGERS_SORTED.map(escapeRegex).join("|")})\\s+(?:all\\s+)?(?:the\\s+)?([a-z][a-z'-]*)\\s+(?:of|for)\\s+(.+?)[?.!\\s]*$`,
|
|
4453
|
+
"i",
|
|
4454
|
+
);
|
|
4455
|
+
// "where are the flies and spiders" — the same listing said the natural way.
|
|
4456
|
+
// Singular too ("where is the spider"): the class list resolves either way, and
|
|
4457
|
+
// with only the code-graph `where` lane to fall back on the answer would be
|
|
4458
|
+
// "no recorded code location" for a subject whose cell this graph plainly
|
|
4459
|
+
// holds. A phrase that isn't a bare class noun ("where is auth.mjs defined")
|
|
4460
|
+
// fails parseWorldClassList and keeps the definition-site reading.
|
|
4461
|
+
const WORLD_WHERE_RE = /^where(?:'s|\s+is|\s+are)\s+(?:all\s+)?(?:the\s+)?(.+?)[?.!\s]*$/i;
|
|
4462
|
+
|
|
4463
|
+
/** Compile "<list trigger> the <world-relation noun> of <class> and <class>"
|
|
4464
|
+
* (or "where are the <class> and <class>") into a world-relation listing AST,
|
|
4465
|
+
* or null when this isn't that shape. */
|
|
4466
|
+
function worldRelationQuery(graph, query) {
|
|
4467
|
+
const q = String(query || "").trim();
|
|
4468
|
+
const listM = q.match(WORLD_LISTING_RE);
|
|
4469
|
+
const relation = listM ? WORLD_NOUN_TO_RELATION[listM[1].toLowerCase()] : "placement";
|
|
4470
|
+
if (!relation) return null;
|
|
4471
|
+
const subjectText = listM ? listM[2] : q.match(WORLD_WHERE_RE)?.[1];
|
|
4472
|
+
if (!subjectText) return null;
|
|
4473
|
+
const resolved = parseWorldClassList(graph, subjectText);
|
|
4474
|
+
if (!resolved) return null;
|
|
4475
|
+
return {
|
|
4476
|
+
node: "worldRelation",
|
|
4477
|
+
relation,
|
|
4478
|
+
predicate: WORLD_RELATIONS[relation].predicate,
|
|
4479
|
+
classes: resolved.classes,
|
|
4480
|
+
askedClasses: resolved.asked,
|
|
4481
|
+
};
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
const normalizePredicate = (p) => String(p || "").toLowerCase();
|
|
4485
|
+
|
|
4486
|
+
function evalWorldRelation(graph, ast) {
|
|
4487
|
+
const wanted = new Set(ast.classes);
|
|
4488
|
+
const latest = new Map();
|
|
4489
|
+
for (const group of graph?.relations || []) {
|
|
4490
|
+
if (normalizePredicate(group.prop) !== ast.predicate && normalizePredicate(group.predicate) !== ast.predicate) continue;
|
|
4491
|
+
// A world appends a fresh row per turn rather than rewriting one, so the
|
|
4492
|
+
// LAST edge for a subject is its current value — the same last-wins fold
|
|
4493
|
+
// every world's own state reader applies to its rows.
|
|
4494
|
+
for (const edge of group.edges || []) {
|
|
4495
|
+
const subject = graph.byId?.get(edge.subject);
|
|
4496
|
+
if (subject && wanted.has(subject.class)) latest.set(edge.subject, { subject, object: edge.object });
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
const pairs = [...latest.values()].sort((a, b) => String(a.subject.id).localeCompare(String(b.subject.id)));
|
|
4500
|
+
return {
|
|
4501
|
+
compositeKind: "worldRelation",
|
|
4502
|
+
relation: ast.relation,
|
|
4503
|
+
askedClasses: ast.askedClasses,
|
|
4504
|
+
pairs,
|
|
4505
|
+
matches: pairs.map((p) => p.subject),
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// A world writes each turn's facts against a stamped subject ("spider-1@turn3",
|
|
4510
|
+
// "player@step7") and folds them back onto the base id, newest stamp winning —
|
|
4511
|
+
// spider-fly.mjs's foldSpiderFlyState and domain.mjs's foldWorldState both do
|
|
4512
|
+
// exactly this. An unstamped row is the world's own starting value, stamp 0.
|
|
4513
|
+
const WORLD_SNAPSHOT_RE = /^(.+)@(?:turn|step)(\d+)$/;
|
|
4514
|
+
|
|
4515
|
+
function splitWorldSnapshot(term) {
|
|
4516
|
+
const m = WORLD_SNAPSHOT_RE.exec(String(term ?? ""));
|
|
4517
|
+
return m ? { base: m[1], stamp: Number(m[2]) } : { base: String(term ?? ""), stamp: 0 };
|
|
4518
|
+
}
|
|
4519
|
+
|
|
4520
|
+
/** Project plain world fact rows (`{subject, predicate, object}`, the shape
|
|
4521
|
+
* memory/core.mjs's readFactRows returns) into the `{individuals,
|
|
4522
|
+
* objectProperties}` payload parseEntities consumes, so a page holding a live
|
|
4523
|
+
* world can hand `ask` a graph of it instead of an empty one. Only the
|
|
4524
|
+
* WORLD_PREDICATES rows are carried: a board's own geometry (every cell and
|
|
4525
|
+
* every exit) is thousands of rows no natural-language listing asks for.
|
|
4526
|
+
* Turn-stamped rows are folded onto their base subject, so the graph carries
|
|
4527
|
+
* each subject's CURRENT value and never a stale one from an earlier turn.
|
|
4528
|
+
*
|
|
4529
|
+
* A subject's class comes from its own `rdf:type` row when the world states
|
|
4530
|
+
* one; `classOf(id)` is the fallback for a world that names its individuals
|
|
4531
|
+
* by convention instead (spider-fly's "spider-1" is a spider because of its
|
|
4532
|
+
* id). Returning null from `classOf` drops the subject — that is the seam a
|
|
4533
|
+
* caller filters through, so "which subjects still count as live" stays a
|
|
4534
|
+
* rule of the world module rather than of this projection. Pure. */
|
|
4535
|
+
export function worldRelationGraphPayload(rows, { classOf = () => null } = {}) {
|
|
4536
|
+
const declaredClass = new Map();
|
|
4537
|
+
for (const row of rows || []) {
|
|
4538
|
+
if (row?.predicate === "rdf:type" && row.subject && row.object) {
|
|
4539
|
+
declaredClass.set(splitWorldSnapshot(row.subject).base, String(row.object));
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
const wanted = new Set(WORLD_PREDICATES);
|
|
4543
|
+
const individuals = new Map();
|
|
4544
|
+
const newest = new Map();
|
|
4545
|
+
for (const row of rows || []) {
|
|
4546
|
+
if (!row?.subject || !wanted.has(row.predicate)) continue;
|
|
4547
|
+
const { base, stamp } = splitWorldSnapshot(row.subject);
|
|
4548
|
+
const cls = declaredClass.get(base) || classOf(base);
|
|
4549
|
+
if (!cls) continue;
|
|
4550
|
+
const key = `${row.predicate}${base}`;
|
|
4551
|
+
const prior = newest.get(key);
|
|
4552
|
+
if (prior && prior.stamp > stamp) continue;
|
|
4553
|
+
individuals.set(base, { id: base, label: base, class: cls });
|
|
4554
|
+
newest.set(key, { stamp, predicate: row.predicate, subject: base, object: splitWorldSnapshot(row.object).base });
|
|
4555
|
+
}
|
|
4556
|
+
const groups = new Map();
|
|
4557
|
+
for (const edge of newest.values()) {
|
|
4558
|
+
if (!groups.has(edge.predicate)) groups.set(edge.predicate, { prop: edge.predicate, predicate: edge.predicate, examples: [] });
|
|
4559
|
+
groups.get(edge.predicate).examples.push({ subject: edge.subject, object: edge.object });
|
|
4560
|
+
}
|
|
4561
|
+
return { individuals: [...individuals.values()], objectProperties: [...groups.values()] };
|
|
4562
|
+
}
|
|
4563
|
+
|
|
4406
4564
|
// Matches T5's own bare-object capture (grammar.mjs meta-whatis), reused below to
|
|
4407
4565
|
// extract the term for the article-insertion fallback rather than duplicating it.
|
|
4408
4566
|
const BARE_META_WHATIS_RE = /^what\s+(?:is|are)\s+(?:an?\s+)?(.+?)[?.!\s]*$/i;
|
|
@@ -4447,6 +4605,16 @@ export function ask(graph, query, { contextId = null, nlp = undefined, prev = nu
|
|
|
4447
4605
|
if (!dynRendered.miss) { parsed = dyn; result = dynResult; rendered = dynRendered; relaxed = null; }
|
|
4448
4606
|
}
|
|
4449
4607
|
}
|
|
4608
|
+
// World-relation listing ("list the locations of flies and spiders"), on the
|
|
4609
|
+
// same terms: only after an honest miss, and only when it answers for real.
|
|
4610
|
+
if (rendered.miss && !rendered.ambiguous) {
|
|
4611
|
+
const world = worldRelationQuery(graph, query);
|
|
4612
|
+
if (world) {
|
|
4613
|
+
const worldResult = traverse(graph, world, { contextId, prev });
|
|
4614
|
+
const worldRendered = render(world, worldResult, graph);
|
|
4615
|
+
if (!worldRendered.miss) { parsed = world; result = worldResult; rendered = worldRendered; relaxed = null; }
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4450
4618
|
// Bare "what is X" (no article) meta fallback, narrow to ask() and never
|
|
4451
4619
|
// touching the outer `parsed` even on a miss — chat.mjs's own gates key off
|
|
4452
4620
|
// `!envelope.parsed`, so populating it here would steal turns from lanes
|
|
@@ -143,6 +143,17 @@ export function mudMassDrainPerTurn(mudConfig, characterId) {
|
|
|
143
143
|
return Number.isFinite(drain) ? drain : 0;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/** The mass denominator a spider-and-fly class's HUD bar and sprite
|
|
147
|
+
* expression both scale against — "how full" reads the same on either. `cls`
|
|
148
|
+
* is "spider" or "fly"; any other class (an egg, or a class this world adds
|
|
149
|
+
* later) has no denominator and gets null, never a guessed number. `config`
|
|
150
|
+
* is the page's own `{ maxSpiderMass, maxFlyMass }` pair. Pure. */
|
|
151
|
+
export function massScaleFor(cls, config) {
|
|
152
|
+
if (cls === "spider") return config?.maxSpiderMass ?? null;
|
|
153
|
+
if (cls === "fly") return config?.maxFlyMass ?? null;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
146
157
|
/**
|
|
147
158
|
* Fold a normalized tmct.toml's `games`/`planning` tables (the raw sparse
|
|
148
159
|
* pass-through src/adapters/toml-config.mjs's normalizeConfig produces —
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// mud-facts.mjs — the one `mgx:is-predator` reader shared across the
|
|
2
|
+
// client/server boundary: mud-turn.mjs (the server-side turn engine, deciding
|
|
3
|
+
// which rooms to route an animal away from) and mud-viz.mjs (the client-facing
|
|
4
|
+
// view, deciding which room to draw a pounce against) each asked "which
|
|
5
|
+
// subject does the world mark dangerous" their own way, over the exact same
|
|
6
|
+
// filter. One reader here, so the two never drift apart on what counts as a
|
|
7
|
+
// predator.
|
|
8
|
+
|
|
9
|
+
/** Every subject the world marks `mgx:is-predator` true, in fact-row order.
|
|
10
|
+
* Pure. */
|
|
11
|
+
export function predatorSubjects(rows) {
|
|
12
|
+
return (rows || [])
|
|
13
|
+
.filter((r) => r.predicate === "mgx:is-predator" && r.object === "true")
|
|
14
|
+
.map((r) => r.subject);
|
|
15
|
+
}
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
import { resolveOne, backwardChainWorld, resolveMemoryTerm } from "./resolver.mjs";
|
|
32
32
|
import { plan, isMultiStep, decompose, MAX_STEPS } from "./planner.mjs";
|
|
33
33
|
import { goalReason } from "./goal-reasoner.mjs";
|
|
34
|
-
import { capabilities } from "./registry.mjs";
|
|
34
|
+
import { capabilities, preconditionsOf, PRECOND } from "./registry.mjs";
|
|
35
35
|
import { registerTaughtActions } from "./taught.mjs";
|
|
36
36
|
import { intersect, fallbackIfEmpty, guardIfEmpty, memberIndividuals, membersReaching, resultSetOf } from "./results.mjs";
|
|
37
37
|
import { resolveObject } from "../ask.mjs";
|
|
@@ -288,23 +288,49 @@ export async function runCapabilityPlan(request, tools, ctx) {
|
|
|
288
288
|
* same store, re-reading it per request via ctx.readTaughtStore. The new
|
|
289
289
|
* registrations' unregister disposers ride the ctx as `ctx.disposers`; the
|
|
290
290
|
* caller runs them when the ctx is done. The same `memoryDir` also opens
|
|
291
|
-
* `ctx.resolveMemoryTerm` — resolveOne's binding oracle for a
|
|
292
|
-
* (tmct_related's `term`), re-reading the store's fact rows per request
|
|
291
|
+
* `ctx.resolveMemoryTerm` — resolveOne's binding oracle for a KINDS.MemoryTerm
|
|
292
|
+
* slot (tmct_related's `term`), re-reading the store's fact rows per request
|
|
293
293
|
* through resolveMemoryTerm (resolver.mjs), the memory-graph sibling of
|
|
294
|
-
* `resolve` above.
|
|
294
|
+
* `resolve` above.
|
|
295
|
+
*
|
|
296
|
+
* MEMORY-ONLY MODE: pass a `memoryDir` with no `graph` and no `source`, and
|
|
297
|
+
* the ctx builds with no code graph at all — the shape of a page that lives on
|
|
298
|
+
* the memory graph alone. `resolve` then misses every term (a code-graph slot
|
|
299
|
+
* refuses honestly at binding), dispatch refuses any capability whose
|
|
300
|
+
* preconditions need a loaded code graph, and the memory lanes — MemoryTerm
|
|
301
|
+
* binding, the taught world-goal simulation — carry the whole ctx. `config`
|
|
302
|
+
* (if any) is still handed to dispatchTool so a memory-store tool can derive
|
|
303
|
+
* its backend. A call with no graph, no source and no memoryDir is a wiring
|
|
304
|
+
* error and throws. */
|
|
295
305
|
export async function buildCapabilityPlanCtx({
|
|
296
|
-
config, source, tel = null, graph = null, memoryDir = null,
|
|
306
|
+
config, source = null, tel = null, graph = null, memoryDir = null,
|
|
297
307
|
dispatchTool, isToolError = () => false, selectTool = null,
|
|
298
308
|
loadMemory = null, readFactRows = null, readRuleRows = null,
|
|
299
309
|
} = {}) {
|
|
300
|
-
const g = graph || parseEntities(await source.fetchEntities(config));
|
|
301
|
-
|
|
310
|
+
const g = graph || (source ? parseEntities(await source.fetchEntities(config)) : null);
|
|
311
|
+
if (!g && !(memoryDir && loadMemory)) {
|
|
312
|
+
throw new Error("buildCapabilityPlanCtx: pass a graph, a source to load one, or a memoryDir (memory-only mode)");
|
|
313
|
+
}
|
|
314
|
+
const resolve = g ? (term) => resolveObject(g, term) : () => ({ match: null, ambiguous: false, candidates: [] });
|
|
315
|
+
const needsCodeGraph = (name) => preconditionsOf(name).some((p) => p.pred === PRECOND.graphLoaded);
|
|
302
316
|
const dispatch = async (name, input) => {
|
|
317
|
+
if (!g && needsCodeGraph(name)) {
|
|
318
|
+
return { ok: false, error: `${name} queries the code graph, and this memory-only context has none loaded` };
|
|
319
|
+
}
|
|
303
320
|
try {
|
|
304
|
-
|
|
321
|
+
// `source ?? undefined` so a memory-only caller's explicit null never
|
|
322
|
+
// suppresses dispatchTool's own default source for a tool that loads.
|
|
323
|
+
// `memoryBackend` hands a memory-reading tool the SAME open store this
|
|
324
|
+
// ctx binds its MemoryTerm slots against, so binding and dispatch can
|
|
325
|
+
// never answer from two different stores — and so a store that was never
|
|
326
|
+
// derived from a config (a browser page's in-memory one) is reachable at
|
|
327
|
+
// all.
|
|
328
|
+
const text = await dispatchTool(name, input, {
|
|
329
|
+
config, source: source ?? undefined, tel, memoryBackend: memoryDir ?? undefined,
|
|
330
|
+
});
|
|
305
331
|
const primary = input && (input.symbol ?? input.module ?? input.class ?? input.query);
|
|
306
332
|
const resolved = primary ? resolve(String(primary)).match : null;
|
|
307
|
-
const result = resultSetOf(g, name, input, resolved);
|
|
333
|
+
const result = g ? resultSetOf(g, name, input, resolved) : [];
|
|
308
334
|
return { ok: true, text, resolved, result };
|
|
309
335
|
} catch (e) {
|
|
310
336
|
if (isToolError(e)) return { ok: false, error: e.message };
|
|
@@ -26,16 +26,25 @@ export const VOCAB = Object.freeze({
|
|
|
26
26
|
|
|
27
27
|
// Parameter entity-KINDS — the seon/mgx classes a slot ranges over. `Query` and
|
|
28
28
|
// `Kind`/`Package` are free-text / enum slots (no graph resolution); the rest
|
|
29
|
-
// name a graph entity the resolver must prove RESOLVES before the call fires
|
|
29
|
+
// name a graph entity the resolver must prove RESOLVES before the call fires —
|
|
30
|
+
// code-graph kinds through resolveObject (ask.mjs), MemoryTerm through
|
|
31
|
+
// resolveMemoryTerm (resolver.mjs), the conversational-memory sibling oracle.
|
|
30
32
|
export const KINDS = Object.freeze({
|
|
31
33
|
Symbol: "seon:CodeEntity", // any code symbol: function/method/class/module/attribute
|
|
32
34
|
Module: "mgx:Module", // SEON has no JS-module class (its nearest are Namespace/main:File); owned
|
|
33
35
|
Class: "seon:ClassType", // SEON's real class for a class definition
|
|
36
|
+
MemoryTerm: "mgx:MemoryTerm", // owned: a conversational-memory term (a minted skos:Concept or a world-fact subject/object) — binds in the memory graph, never the code graph
|
|
34
37
|
Query: "cap:FreeText", // lexical search string — no resolution precondition
|
|
35
38
|
Kind: "cap:KindFilter", // enum: function|class|method|… (search filter)
|
|
36
39
|
Package: "cap:PackageName", // optional architecture-scope filter
|
|
37
40
|
});
|
|
38
41
|
|
|
42
|
+
// The kinds that bind in the conversational-memory graph (resolveMemoryTerm)
|
|
43
|
+
// rather than the code graph (resolveObject). A consumer that walks KINDS for
|
|
44
|
+
// code-graph-resolvable classes (e.g. the synthbench enumerator's focus
|
|
45
|
+
// classes) must exclude these — a memory term is never a code-graph focus.
|
|
46
|
+
export const MEMORY_KINDS = Object.freeze([KINDS.MemoryTerm]);
|
|
47
|
+
|
|
39
48
|
// Precondition PREDICATE tags (the small closed vocabulary a precondition uses).
|
|
40
49
|
export const PRECOND = Object.freeze({
|
|
41
50
|
graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
|
|
@@ -87,7 +96,8 @@ function capability({ name, label, question, params = [], preconditions = [], ad
|
|
|
87
96
|
// callees/tests/history/… take `symbol`; impact/exports take `module`; members/
|
|
88
97
|
// subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
|
|
89
98
|
// architecture takes an optional `package`; untested takes nothing; related
|
|
90
|
-
// takes `term` (a memory-graph concept term)
|
|
99
|
+
// takes `term` (a memory-graph concept term); sprite takes `class` (a memory-graph
|
|
100
|
+
// term) plus optional `expression`/`size`.
|
|
91
101
|
|
|
92
102
|
const CAPABILITIES = Object.freeze([
|
|
93
103
|
capability({
|
|
@@ -187,10 +197,20 @@ const CAPABILITIES = Object.freeze([
|
|
|
187
197
|
}),
|
|
188
198
|
capability({
|
|
189
199
|
name: "tmct_related", label: "related", question: "a term's synonyms and related concepts (the SKOS view over the conversational-memory graph)",
|
|
190
|
-
params: [param("term", KINDS.
|
|
191
|
-
preconditions: [memoryFacts()],
|
|
200
|
+
params: [param("term", KINDS.MemoryTerm, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
|
|
201
|
+
preconditions: [memoryFacts(), resolves("term", KINDS.MemoryTerm)],
|
|
192
202
|
add: [knows("related", "term")],
|
|
193
203
|
}),
|
|
204
|
+
capability({
|
|
205
|
+
name: "tmct_sprite", label: "sprite", question: "the sprite for a class, with the expression and size asked for",
|
|
206
|
+
params: [
|
|
207
|
+
param("class", KINDS.MemoryTerm, { arg: "class", note: "a world class, matched against the memory graph's own fact rows rather than resolved in the code graph" }),
|
|
208
|
+
param("expression", KINDS.Kind, { required: false, note: "one of sprite-expressions.mjs's palette words, carried as an mgx:feels fact" }),
|
|
209
|
+
param("size", KINDS.Kind, { required: false, note: "a taught mgx:hasProperty size word, resolved to a render scale — not a template tier" }),
|
|
210
|
+
],
|
|
211
|
+
preconditions: [memoryFacts(), resolves("class", KINDS.MemoryTerm)],
|
|
212
|
+
add: [knows("sprite", "class")],
|
|
213
|
+
}),
|
|
194
214
|
]);
|
|
195
215
|
|
|
196
216
|
// The live capability set: the built-in frozen array is the seed; registration
|