@polycode-projects/the-mechanical-code-talker 2.6.0 → 2.7.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.
Files changed (63) hide show
  1. package/corpus/LICENSES.json +14 -0
  2. package/corpus/child/LICENSE-NOTICE +43 -0
  3. package/corpus/child/README.md +79 -0
  4. package/corpus/child/index.json.gz +0 -0
  5. package/corpus/child/manifest.json +313 -0
  6. package/corpus/child/shards/child-00.jsonl.gz +0 -0
  7. package/corpus/child/shards/child-01.jsonl.gz +0 -0
  8. package/corpus/child/shards/child-02.jsonl.gz +0 -0
  9. package/corpus/child/shards/child-03.jsonl.gz +0 -0
  10. package/corpus/child/shards/child-04.jsonl.gz +0 -0
  11. package/corpus/child/shards/child-05.jsonl.gz +0 -0
  12. package/corpus/child/shards/child-06.jsonl.gz +0 -0
  13. package/corpus/child/shards/child-07.jsonl.gz +0 -0
  14. package/corpus/child/shards/child-08.jsonl.gz +0 -0
  15. package/corpus/child/shards/child-09.jsonl.gz +0 -0
  16. package/corpus/child/shards/child-0a.jsonl.gz +0 -0
  17. package/corpus/child/shards/child-0b.jsonl.gz +0 -0
  18. package/corpus/child/shards/child-0c.jsonl.gz +0 -0
  19. package/corpus/child/shards/child-0d.jsonl.gz +0 -0
  20. package/corpus/child/shards/child-0e.jsonl.gz +0 -0
  21. package/corpus/child/shards/child-0f.jsonl.gz +0 -0
  22. package/corpus/child/shards/child-10.jsonl.gz +0 -0
  23. package/corpus/child/shards/child-11.jsonl.gz +0 -0
  24. package/corpus/child/shards/child-12.jsonl.gz +0 -0
  25. package/corpus/child/shards/child-13.jsonl.gz +0 -0
  26. package/corpus/child/shards/child-14.jsonl.gz +0 -0
  27. package/corpus/child/shards/child-15.jsonl.gz +0 -0
  28. package/corpus/child/shards/child-16.jsonl.gz +0 -0
  29. package/corpus/child/shards/child-17.jsonl.gz +0 -0
  30. package/corpus/child/shards/child-18.jsonl.gz +0 -0
  31. package/corpus/child/shards/child-19.jsonl.gz +0 -0
  32. package/corpus/child/shards/child-1a.jsonl.gz +0 -0
  33. package/corpus/child/shards/child-1b.jsonl.gz +0 -0
  34. package/corpus/child/shards/child-1c.jsonl.gz +0 -0
  35. package/corpus/child/shards/child-1d.jsonl.gz +0 -0
  36. package/corpus/child/shards/child-1e.jsonl.gz +0 -0
  37. package/corpus/child/shards/child-1f.jsonl.gz +0 -0
  38. package/corpus/conceptnet/child-seed.mjs +169 -0
  39. package/corpus/conceptnet/filter-dump.mjs +69 -48
  40. package/corpus/worlds/README.md +26 -0
  41. package/corpus/worlds/index.json.gz +0 -0
  42. package/corpus/worlds/manifest.json +33 -0
  43. package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
  44. package/corpus/worlds/src/ashcombe-hall.jsonl +64 -0
  45. package/data/templates/responses.jsonl +1 -1
  46. package/package.json +4 -1
  47. package/src/adapters/corpus/child-pack.mjs +115 -0
  48. package/src/adapters/corpus/conceptnet-map.toml +7 -0
  49. package/src/adapters/corpus/worlds-pack.mjs +122 -0
  50. package/src/domain/ask.mjs +66 -10
  51. package/src/domain/child-pack.mjs +79 -0
  52. package/src/domain/codegraph.mjs +1 -1
  53. package/src/domain/grammar/ace.mjs +77 -0
  54. package/src/domain/grammar/lexicon-core.json +6 -0
  55. package/src/domain/interpret/normalize.mjs +1 -1
  56. package/src/domain/memory/trust.mjs +8 -0
  57. package/src/domain/router/registry.mjs +17 -4
  58. package/src/domain/router/resolver.mjs +4 -2
  59. package/src/domain/worlds-pack.mjs +71 -0
  60. package/src/services/adventure.mjs +669 -0
  61. package/src/services/chat.mjs +652 -43
  62. package/src/services/sessions.mjs +10 -2
  63. package/src/surfaces/web/memory-ask-browser.bundle.js +607 -57
@@ -0,0 +1,122 @@
1
+ // corpus/worlds-pack.mjs — lazy, failure-tolerated loader for the shipped
2
+ // worlds pack (corpus/worlds/): a gzipped world index consulted first, then
3
+ // exactly one gzipped JSONL shard per world. Nothing here ever throws at a
4
+ // caller — an absent, truncated or corrupt pack reads as null, and a null is
5
+ // the ordinary honest decline ("no worlds pack here").
6
+ //
7
+ // The provider seam mirrors reference-pack.mjs's exactly:
8
+ // registerWorldsPackProvider swaps the whole lookup behind one async
9
+ // `{ list(), load(worldName) }` contract. It exists because a browser surface
10
+ // cannot read this filesystem layout — a web provider can fetch worlds
11
+ // instead, and chat code never knows which one it is talking to. No provider
12
+ // registered = the fs loader below.
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { gunzipSync } from "node:zlib";
16
+ import { fileURLToPath } from "node:url";
17
+ import { dirname, join } from "node:path";
18
+ import { isWorldsIndexEntry, isWorldRow, isWorldFactRow, isWorldRuleRow, isWorldMetaRow } from "../../domain/worlds-pack.mjs";
19
+
20
+ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
21
+
22
+ /** The pack directory: TMCT_WORLDS_PACK_DIR when set, else the package's own
23
+ * corpus/worlds/. */
24
+ export function worldsPackDir(env = process.env) {
25
+ return env?.TMCT_WORLDS_PACK_DIR || join(PKG_ROOT, "corpus", "worlds");
26
+ }
27
+
28
+ const indexCacheByDir = new Map(); // dir -> { worldName: {s} } | null
29
+ const worldCacheByKey = new Map(); // `${dir}\0${world}` -> payload | null
30
+
31
+ /** Drop every cached index/world — for tests that mutate a pack dir. */
32
+ export function clearWorldsPackCache() {
33
+ indexCacheByDir.clear();
34
+ worldCacheByKey.clear();
35
+ }
36
+
37
+ function readGunzipped(file) {
38
+ try {
39
+ return gunzipSync(readFileSync(file));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /** The pack's world index, lazily read and cached per dir; null (cached)
46
+ * when the pack is absent or unreadable. Never throws. */
47
+ export function loadWorldsIndex(dir) {
48
+ if (indexCacheByDir.has(dir)) return indexCacheByDir.get(dir);
49
+ let index = null;
50
+ const body = readGunzipped(join(dir, "index.json.gz"));
51
+ if (body) {
52
+ try {
53
+ const parsed = JSON.parse(body.toString("utf8"));
54
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) index = parsed;
55
+ } catch { /* tolerated: a corrupt index is an absent pack */ }
56
+ }
57
+ indexCacheByDir.set(dir, index);
58
+ return index;
59
+ }
60
+
61
+ /** One world by name: index hit -> the one shard the index names (cached) ->
62
+ * { facts, rules, meta }. Null on an index miss or an unreadable shard, so
63
+ * an unknown world never costs a shard read. Never throws. */
64
+ export function loadWorld(dir, worldName) {
65
+ const index = loadWorldsIndex(dir);
66
+ if (!index) return null;
67
+ const entry = index[String(worldName ?? "")];
68
+ if (!isWorldsIndexEntry(entry)) return null;
69
+ const key = `${dir}\0${worldName}`;
70
+ if (worldCacheByKey.has(key)) return worldCacheByKey.get(key);
71
+ let payload = null;
72
+ const body = readGunzipped(join(dir, "shards", `${entry.s}.jsonl.gz`));
73
+ if (body) {
74
+ const facts = [];
75
+ const rules = [];
76
+ let meta = null;
77
+ for (const line of body.toString("utf8").split("\n")) {
78
+ if (!line.trim()) continue;
79
+ try {
80
+ const row = JSON.parse(line);
81
+ if (!isWorldRow(row) || row.world !== worldName) continue;
82
+ if (isWorldFactRow(row)) facts.push(row);
83
+ else if (isWorldRuleRow(row)) rules.push(row);
84
+ else if (isWorldMetaRow(row) && !meta) meta = row;
85
+ } catch { /* tolerated: a bad line loses one row, not the world */ }
86
+ }
87
+ if (facts.length || rules.length || meta) payload = { name: worldName, facts, rules, meta };
88
+ }
89
+ worldCacheByKey.set(key, payload);
90
+ return payload;
91
+ }
92
+
93
+ const fsProviderFor = (dirOf) => ({
94
+ list: async () => {
95
+ const index = loadWorldsIndex(dirOf());
96
+ return index ? Object.keys(index).sort() : null;
97
+ },
98
+ load: async (worldName) => loadWorld(dirOf(), worldName),
99
+ });
100
+
101
+ const fsProvider = fsProviderFor(() => worldsPackDir());
102
+
103
+ let registeredProvider = null;
104
+
105
+ /** Swap the pack lookup: provider = { list: async () => string[]|null,
106
+ * load: async (worldName) => payload|null }. Pass null to restore the
107
+ * default fs loader. */
108
+ export function registerWorldsPackProvider(provider) {
109
+ registeredProvider = provider
110
+ && typeof provider.load === "function" && typeof provider.list === "function"
111
+ ? provider : null;
112
+ }
113
+
114
+ /** The active provider — the registered one, else the lazy fs loader. An
115
+ * explicit `env` bag (a chat turn's own env, which may carry
116
+ * TMCT_WORLDS_PACK_DIR) makes the fs loader resolve the pack dir from that
117
+ * bag instead of process.env; with no argument the behavior is unchanged. */
118
+ export function getWorldsPackProvider(env) {
119
+ if (registeredProvider) return registeredProvider;
120
+ if (env === undefined) return fsProvider;
121
+ return fsProviderFor(() => worldsPackDir(env));
122
+ }
@@ -225,7 +225,7 @@ function parseComposite(text, nlp) {
225
225
  || parseQualifierCheck(w, lc)
226
226
  || parseUniversal(w, lc, nlp)
227
227
  || parseNegation(text, nlp, 0)
228
- || parseNegatedAsk(w, lc)
228
+ || parseNegatedAsk(w, lc, nlp)
229
229
  || parseForwardNegation(w, lc, nlp)
230
230
  || parseTemporal(w, lc, nlp, 0)
231
231
  || parseCommitFilter(w, lc)
@@ -718,11 +718,18 @@ function parseQualifierCheck(w, lc) {
718
718
  * the same sentence at positive polarity ("not" is no stopword) and the merge
719
719
  * would call the two readings an ambiguity. Declining to a composite production
720
720
  * is what keeps the sentence out of that merge. */
721
- function parseNegatedAsk(w, lc) {
722
- if (lc[0] !== "do" && lc[0] !== "does" && lc[0] !== "did") return null;
721
+ function parseNegatedAsk(w, lc, nlp) {
722
+ // The copular leads carry the PASSIVE twin ("is X not imported by Y")
723
+ // without them the strategies read the sentence at positive polarity (the
724
+ // "not" drops as noise) and the bare "Yes" answers the un-negated question.
725
+ // parseQualifierCheck ran first, so "is X not deprecated" keeps its lane.
726
+ // The positive re-parse runs the SAME simple-clause pair the composer's
727
+ // fragments use — the passive lives in the keyword strategy, which the
728
+ // anchored grammar alone never reaches.
729
+ if (!["do", "does", "did", "is", "are", "was", "were"].includes(lc[0])) return null;
723
730
  const notIdx = lc.indexOf("not", 1);
724
731
  if (notIdx < 0) return null;
725
- const positive = parseAnchored(w.filter((_, i) => i !== notIdx).join(" "));
732
+ const positive = parseSimpleClause(w.filter((_, i) => i !== notIdx).join(" "), nlp);
726
733
  if (!positive || positive.shape !== "ask") return null;
727
734
  return { ...positive, negated: true };
728
735
  }
@@ -1784,7 +1791,15 @@ function evalTemporal(graph, ast, opts) {
1784
1791
  }
1785
1792
 
1786
1793
  function evalSuperlative(graph, ast) {
1787
- const pool = graph.individuals.filter((i) => i.class === ast.entityType);
1794
+ let pool = graph.individuals.filter((i) => i.class === ast.entityType);
1795
+ // A tests-metric ranking over Modules surveys COVERAGE, and a test module
1796
+ // is never a coverage target — the same exclusion renderUntested (the
1797
+ // /untested surface) applies, so the two surveys can't disagree about the
1798
+ // same set ("what most needs a test" used to name b.test.mjs).
1799
+ if (ast.metric?.kind === "tests" && ast.entityType === "Module") {
1800
+ const testSubjects = new Set(edgesOfKind(graph, "tests").map((e) => e.subject));
1801
+ pool = pool.filter((i) => !testSubjects.has(i.id) && !isTestPath(String(i.label).toLowerCase()));
1802
+ }
1788
1803
  const scored = pool.map((ind) => ({ ind, score: degreeMetric(graph, ind, ast.metric) }))
1789
1804
  .sort((a, z) => (ast.extreme === "most" ? z.score - a.score : a.score - z.score));
1790
1805
  if (!scored.length) return { compositeKind: "superlative", entityType: ast.entityType, matches: [] };
@@ -2608,7 +2623,10 @@ export function resolveObject(graph, term, opts = {}) {
2608
2623
  }
2609
2624
  return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
2610
2625
  }
2611
- return resolveObjectCore(graph, term, opts);
2626
+ // The pinned-class branch honors the same contract — a term carrying words
2627
+ // the index has no reading for declines instead of resolving past them
2628
+ // ("the old Task" pinned to Class must not silently swallow "old").
2629
+ return declineOnUnplacedWords(resolveObjectCore(graph, term, opts), term);
2612
2630
  }
2613
2631
 
2614
2632
  /** Resolve a term that may be a context pronoun ("this"/"it"/"that"/"here") —
@@ -2979,7 +2997,10 @@ export function traverse(graph, parsed, { contextId = null, prev = null, pinnedO
2979
2997
  if (entityType && entityType !== "Change") {
2980
2998
  const wantClasses = classesForKinds(graph, fwdKinds);
2981
2999
  const siblingClass = FINE_CLASS_SIBLING[entityType];
2982
- if (!wantClasses.has(entityType) && !(siblingClass && wantClasses.has(siblingClass))) {
3000
+ // An edge-less kind (wantClasses empty) is not a grain mismatch — there
3001
+ // is nothing to name after "only …", and the render leaked a literal
3002
+ // "undefined" there. Fall through to the plain empty-edges answer.
3003
+ if (wantClasses.size && !wantClasses.has(entityType) && !(siblingClass && wantClasses.has(siblingClass))) {
2983
3004
  return {
2984
3005
  matches: [], objMatch, candidates, ambiguous, matchedVia,
2985
3006
  forwardGrainMiss: true, wantClasses: [...wantClasses],
@@ -3451,13 +3472,48 @@ function renderCore(parsed, result, graph) {
3451
3472
  if (result.ambiguous) {
3452
3473
  // Name the actual candidates in the prose, not just the structured field
3453
3474
  // — "narrow the term" isn't actionable if the reader can't see the options.
3454
- const pool = [result.objMatch, ...(result.candidates || [])].filter(Boolean);
3475
+ let pool = [result.objMatch, ...(result.candidates || [])].filter(Boolean);
3476
+ let branches = result.branches;
3477
+ // A relation question's object slot is a code entity, so a Commit/Session
3478
+ // candidate (a prose-tier hit on a commit MESSAGE) is grain noise in the
3479
+ // did-you-mean — dropped, along with its branch, unless the question is
3480
+ // itself about touches/history where a commit genuinely fits.
3481
+ if (parsed.kind && parsed.kind !== "touches" && !pool.every((i) => i.class === "Commit")) {
3482
+ const grainOk = (i) => !["Commit", "Session", "Source", "Utterance"].includes(i?.class);
3483
+ const kept = pool.filter(grainOk);
3484
+ if (kept.length) {
3485
+ pool = kept;
3486
+ if (branches?.length) branches = branches.filter((b) => grainOk(b.candidate));
3487
+ }
3488
+ }
3489
+ // The nearest REAL neighbour joins the list: a misremembered symbol
3490
+ // ("saveTask") shares an identifier word with the real one ("saveStore"),
3491
+ // which no containment/prose tier ever surfaces. Ranked by shared
3492
+ // camelCase-split words (≥3 chars), edit distance breaking ties.
3493
+ if (graph && !/[/.]/.test(String(parsed.object || ""))) {
3494
+ const tLc = String(parsed.object || "").toLowerCase();
3495
+ const termWords = new Set(splitIdentifierWords(String(parsed.object || "")).filter((w) => w.length >= 3));
3496
+ const already = new Set(pool.map((i) => i.id));
3497
+ let nearest = null;
3498
+ let bestShared = 0;
3499
+ let bestD = Infinity;
3500
+ if (termWords.size) {
3501
+ for (const i of graph.individuals) {
3502
+ if (!["Function", "Method", "Class", "GlobalVariable", "Attribute"].includes(i.class) || already.has(i.id)) continue;
3503
+ const shared = splitIdentifierWords(String(i.label)).filter((w) => termWords.has(w)).length;
3504
+ if (!shared || shared < bestShared) continue;
3505
+ const d = editDistance(String(i.label).toLowerCase(), tLc, 8);
3506
+ if (shared > bestShared || d < bestD) { nearest = i; bestShared = shared; bestD = d; }
3507
+ }
3508
+ }
3509
+ if (nearest) pool = [...pool, nearest];
3510
+ }
3455
3511
  const noun = pool.length && pool.every((i) => i.class === "Commit") ? "commit" : "module";
3456
3512
  const shown = pool.slice(0, OVERFLOW_CAP).map((i) => i.label);
3457
3513
  const extra = pool.length > OVERFLOW_CAP ? `, …and ${pool.length - OVERFLOW_CAP} more` : "";
3458
3514
  const lead = `"${parsed.object}" matches more than one ${noun} ambiguously — did you mean ${listJoin(shown)}${extra}? Try one of those. If you're not sure, narrow it to one name.`;
3459
- const content = (result.branches && result.branches.length)
3460
- ? `${lead}\n${result.branches.map((b, i) => `${i + 1}) ${b.candidate.label}: ${b.rendered.content}`).join("\n")}`
3515
+ const content = (branches && branches.length)
3516
+ ? `${lead}\n${branches.map((b, i) => `${i + 1}) ${b.candidate.label}: ${b.rendered.content}`).join("\n")}`
3461
3517
  : lead;
3462
3518
  return {
3463
3519
  content, miss: false, ambiguous: true, candidates: pool.map((i) => i.label),
@@ -0,0 +1,79 @@
1
+ // child-pack.mjs — the pure half of the shipped CHILD triples pack
2
+ // (corpus/child/): the shard naming contract, the index/row shape validators
3
+ // every writer and reader share, and the provenance tag a fact learned from the
4
+ // pack carries. The pack itself is gzipped JSONL shards plus a gzipped term
5
+ // index; loading them is I/O and lives in src/adapters/corpus/child-pack.mjs.
6
+ //
7
+ // The pack is a lazy learn-on-miss reference the clean-miss cascade consults:
8
+ // on a miss for term T the loader returns T's ConceptNet triples, and the chat
9
+ // hook appends them to memory (see childProvenanceTag). It mirrors the reference
10
+ // pack's on-disk shape (index -> one shard per hit) but carries TRIPLES, not
11
+ // article prose — so its row is a list of {subject, predicate, object} facts,
12
+ // already mapped through conceptnet-map.toml into tmct's predicate vocabulary.
13
+
14
+ import { fnv1aHex, normFactTerm } from "./hash.mjs";
15
+
16
+ export const CHILD_PACK_NAME = "conceptnet";
17
+ export const CHILD_SHARD_COUNT = 32;
18
+
19
+ /** The shard a term's triples row lives in: FNV-1a first byte mod 32, as the
20
+ * file basename "child-00" … "child-1f". Part of the pack's on-disk contract —
21
+ * the build script shards with THIS function, so the reader never scans. The
22
+ * term is normFactTerm-folded first, so the key the index stores and the key a
23
+ * clean-miss lookup computes are the same spelling. */
24
+ export function shardNameFor(term) {
25
+ const byte = parseInt(fnv1aHex(normFactTerm(term)).slice(0, 2), 16);
26
+ return `child-${(byte % CHILD_SHARD_COUNT).toString(16).padStart(2, "0")}`;
27
+ }
28
+
29
+ const SHARD_NAME_RE = /^child-[0-1][0-9a-f]$/;
30
+
31
+ /** An index entry {s, t, n}: the shard holding the row, the row's canonical
32
+ * term key (a normFactTerm fixed point), and the fact count (a positive
33
+ * integer, so a zero-fact row can never be indexed). */
34
+ export function isChildIndexEntry(e) {
35
+ return !!e && typeof e === "object"
36
+ && typeof e.s === "string" && SHARD_NAME_RE.test(e.s)
37
+ && typeof e.t === "string" && e.t.length > 0
38
+ && Number.isInteger(e.n) && e.n > 0;
39
+ }
40
+
41
+ /** One triple in a row: {subject, predicate, object, weight?}. subject/object
42
+ * are human terms ("penguin", "bird"); predicate is a tmct vocabulary URI
43
+ * ("rdfs:subClassOf", "mgx:capableOf", "mgxneg:capableOf"). weight, when
44
+ * present, is the ConceptNet edge weight (a positive number). */
45
+ export function isChildFact(f) {
46
+ if (!f || typeof f !== "object") return false;
47
+ for (const field of ["subject", "predicate", "object"]) {
48
+ if (typeof f[field] !== "string" || !f[field]) return false;
49
+ }
50
+ if (f.weight !== undefined && !(typeof f.weight === "number" && Number.isFinite(f.weight) && f.weight > 0)) return false;
51
+ return true;
52
+ }
53
+
54
+ /** A shard row: {term, facts: [ChildFact, …]} — the term's edges, at least one,
55
+ * every one a valid ChildFact that actually touches the term (as subject or
56
+ * object, once normalised). */
57
+ export function isChildFactsRow(row) {
58
+ if (!row || typeof row !== "object") return false;
59
+ if (typeof row.term !== "string" || !row.term) return false;
60
+ if (!Array.isArray(row.facts) || row.facts.length === 0) return false;
61
+ for (const f of row.facts) {
62
+ if (!isChildFact(f)) return false;
63
+ if (normFactTerm(f.subject) !== row.term && normFactTerm(f.object) !== row.term) return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /** The provenance tag a fact stored from a child-pack lookup carries. The chat
69
+ * hook stamps every fact it appends from term T's row with THIS tag;
70
+ * memory/trust.mjs parses it back to a corpus-tier Source ({kind:"corpus",
71
+ * name:"conceptnet"}) — the child slice is curated ConceptNet, scored at the
72
+ * 0.7 corpus prior. The term segment records which miss pulled the fact in, so
73
+ * a fact's origin shard stays auditable on its factProvenance string even
74
+ * though the Source it corroborates is the shared ConceptNet corpus. */
75
+ export function childProvenanceTag(term) {
76
+ return `${CHILD_PROVENANCE_PREFIX}${CHILD_PACK_NAME}:${normFactTerm(term)}`;
77
+ }
78
+
79
+ export const CHILD_PROVENANCE_PREFIX = "child:";
@@ -429,7 +429,7 @@ export function renderImpact(graph, ind, { maxDepth = 8 } = {}) {
429
429
  lines.push(
430
430
  "warning: partial edge lists (" +
431
431
  truncatedStructural.map((t) => `${t.predicate}: ${t.shown}/${t.count}`).join(", ") +
432
- ") — this closure may be missing edges. Cross-check critical results with tmct_search.",
432
+ ") — this closure may be missing edges. Cross-check critical results with a lexical search (/find <term>).",
433
433
  );
434
434
  }
435
435
  return lines.join("\n");
@@ -457,6 +457,83 @@ export function parseAce(sentence, lexicon = loadLexicon()) {
457
457
  return parseRelation(lexicon, toks, lower);
458
458
  }
459
459
 
460
+ // ---- the imperative command pattern -----------------------------------------
461
+ // A subjectless action command ("go north", "take the key", "unlock the
462
+ // cabinet with the key"). Unlike the nine assertion/question patterns above,
463
+ // this one produces no OWL triple — an imperative has no truth value to
464
+ // assert, it has an ACTION NAME to resolve against the taught action
465
+ // families — so parseImperative returns a structured command instead
466
+ // (precedent: parseCardinality's own non-triple `n`). It is a separate
467
+ // export, never folded into parseAce: every triple pattern requires an
468
+ // explicit subject noun phrase, and parseAce's callers expect triples.
469
+ //
470
+ // The verb set is CLOSED (an unlisted verb is a hard null, never a guess),
471
+ // and object phrases resolve through the same lexicon-noun gate as every
472
+ // other pattern: a structural fit over an undeclared word rides out as
473
+ // `residue` so the caller can name it; a declared word in an unusable shape
474
+ // is a hard null.
475
+
476
+ const IMPERATIVE_VERBS = new Set(["go", "take", "drop", "open", "unlock", "close", "give", "look"]);
477
+ const IMPERATIVE_DIRECTIONS = new Set(["north", "south", "east", "west", "up", "down"]);
478
+
479
+ /** Resolve one imperative object phrase to its bare lexicon term. */
480
+ function imperativeNP(lexicon, tokens) {
481
+ const np = resolveNP(lexicon, tokens);
482
+ if (np.term == null) return { term: null, unknown: np.unknown };
483
+ return { term: local(lexicon, np.term), unknown: [] };
484
+ }
485
+
486
+ /**
487
+ * Parse one imperative command against the closed verb set. Returns
488
+ * `{ pattern: "imperative", verb, residue, object?, indirectObject?,
489
+ * instrument?, direction? }`, a residue-carrying miss for a structural fit
490
+ * over undeclared words (`residue` non-empty, no slots), or null when the
491
+ * sentence is not an imperative of this fragment at all.
492
+ */
493
+ export function parseImperative(sentence, lexicon = loadLexicon()) {
494
+ const toks = tokenize(sentence);
495
+ if (!toks.length) return null;
496
+ const verb = toks[0].toLowerCase();
497
+ if (!IMPERATIVE_VERBS.has(verb)) return null;
498
+ const rest = toks.slice(1);
499
+ const lower = rest.map((t) => t.toLowerCase());
500
+ const command = (fields) => ({ pattern: "imperative", verb, residue: [], ...fields });
501
+ const miss = (unknown) => (unknown.length ? { pattern: "imperative", verb, residue: unknown } : null);
502
+
503
+ if (verb === "look") {
504
+ if (!rest.length || (rest.length === 1 && lower[0] === "around")) return command({});
505
+ return null;
506
+ }
507
+ if (verb === "go") {
508
+ if (rest.length === 1 && IMPERATIVE_DIRECTIONS.has(lower[0])) return command({ direction: lower[0] });
509
+ return null;
510
+ }
511
+ if (verb === "give") {
512
+ const toIdx = lower.indexOf("to");
513
+ if (toIdx < 1 || toIdx === rest.length - 1) return null;
514
+ const object = imperativeNP(lexicon, rest.slice(0, toIdx));
515
+ const indirect = imperativeNP(lexicon, rest.slice(toIdx + 1));
516
+ if (object.term == null || indirect.term == null) return miss([...object.unknown, ...indirect.unknown]);
517
+ return command({ object: object.term, indirectObject: indirect.term });
518
+ }
519
+ if (verb === "unlock") {
520
+ const withIdx = lower.indexOf("with");
521
+ if (withIdx !== -1) {
522
+ if (withIdx < 1 || withIdx === rest.length - 1) return null;
523
+ const object = imperativeNP(lexicon, rest.slice(0, withIdx));
524
+ const instrument = imperativeNP(lexicon, rest.slice(withIdx + 1));
525
+ if (object.term == null || instrument.term == null) return miss([...object.unknown, ...instrument.unknown]);
526
+ return command({ object: object.term, instrument: instrument.term });
527
+ }
528
+ // fall through to the plain-object arm: "unlock the cabinet" is a valid
529
+ // command whose missing instrument is the CALLER's precondition to name.
530
+ }
531
+ if (!rest.length) return null;
532
+ const object = imperativeNP(lexicon, rest);
533
+ if (object.term == null) return miss(object.unknown);
534
+ return command({ object: object.term });
535
+ }
536
+
460
537
  /** Pattern 9 — "N can VERB" → mgx:capableOf. The modal is not a relation
461
538
  * verb: without this, parseRelation reads "can" through lookupVerb and
462
539
  * asserts a generic object property ("dog cans swim") that no capability
@@ -274,6 +274,9 @@
274
274
  "judge": {},
275
275
  "priest": {},
276
276
  "servant": {},
277
+ "butler": {},
278
+ "housekeeper": {},
279
+ "gardener": {},
277
280
  "employee": {},
278
281
  "boss": {},
279
282
  "husband": {},
@@ -328,6 +331,9 @@
328
331
  "dress": {},
329
332
  "clothing": {},
330
333
  "chair": {},
334
+ "desk": {},
335
+ "lamp": {},
336
+ "portrait": {},
331
337
  "bed": {},
332
338
  "door": {},
333
339
  "window": {},
@@ -132,7 +132,7 @@ const THANKS_PREAMBLE_RE = /^(?:thanks|thank\s+you|many\s+thanks|thx|ty|cheers)(
132
132
  const ACK_PREAMBLE_RE = /^(?:(?:ok(?:ay)?|aight|cool|alright|sure|right|fine|great|nice|got it|gotcha|sounds good|no worries|no problem)[\s,]+)+(.+)$/i;
133
133
  /** Self-orientation lead-in with a delimiter — "just poking around, <Q>",
134
134
  * "first time using this, <Q>". */
135
- const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here))\s*[,.—–-]\s*(.+)$/i;
135
+ const BROWSING_PREAMBLE_RE = /^(?:just\s+(?:poking\s+around|looking\s+around|browsing|exploring|checking\s+(?:this|it)\s+out)|first\s+time\s+(?:trying\s+this\s+out|using\s+this|here)|i'?m\s+new\s+(?:here|around\s+here|to\s+(?:this|all\s+this)(?:\s+(?:repo|codebase|project|app|tool|thing))?))\s*[,.—–-]\s*(.+)$/i;
136
136
  /** Repeated leading hedge adverb before a polite request verb ("maybe
137
137
  * possibly tell me <Q>"). No delimiter required, unlike ACK_PREAMBLE_RE. */
138
138
  const HEDGE_ADVERB_PREAMBLE_RE = /^(?:(?:maybe|possibly|perhaps)\s+)+(.+)$/i;
@@ -30,6 +30,11 @@ function parseChatTagRest(rest) {
30
30
  * kind set (the kinds SOURCE_PRIOR scores):
31
31
  * corpus:conceptnet /r/IsA -> { kind:"corpus", name:"conceptnet" }
32
32
  * corpus-weak:conceptnet /r/RelatedTo -> { kind:"corpusWeak", name:"conceptnet" }
33
+ * child:conceptnet:<term> -> { kind:"corpus", name:"conceptnet" }
34
+ * (the lazy child triples pack is curated ConceptNet — same corpus tier,
35
+ * same 0.7 prior, same shared Source as the bulk conceptnet import; the
36
+ * <term> segment records which miss pulled the fact in and is not part of
37
+ * the Source identity)
33
38
  * ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
34
39
  * teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
35
40
  * web:<url> | url:<url> -> { kind:"web", url:<url> }
@@ -54,6 +59,9 @@ export function provenanceTagToSource(tag) {
54
59
  const head = t.split(/\s+/)[0]; // drop trailing " /r/IsA" etc.
55
60
  if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
56
61
  if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
62
+ // child:<pack>:<term> — the lazy child triples pack, scored at the corpus tier
63
+ // under the pack's shared Source; the per-term tail is dropped from the id.
64
+ if (head.startsWith("child:")) return { kind: "corpus", name: head.slice("child:".length).split(":")[0] || "unknown" };
57
65
  if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
58
66
  if (head.startsWith("teach:")) {
59
67
  // the chat teach lane's natural frames — chat.mjs's teachProvenanceTag
@@ -41,6 +41,7 @@ export const PRECOND = Object.freeze({
41
41
  graphLoaded: "cap:graph-loaded", // a graph artifact is present + parseable
42
42
  resolves: "cap:resolves", // { param, as } — the slot binds to an entity of kind `as`
43
43
  anyPresent: "cap:any-present", // { params } — at least one of these slots is provided
44
+ memoryFacts: "cap:memory-facts", // the conversational-memory store holds relation facts for the term
44
45
  });
45
46
 
46
47
  // ---- capability builder (returns PLAIN FROZEN data) -------------------------
@@ -57,6 +58,11 @@ const resolves = (paramName, as) =>
57
58
  /** any-present(params) — search-style disjunction (query OR kind must be given). */
58
59
  const anyPresent = (params) =>
59
60
  Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.anyPresent, params: Object.freeze([...params]) });
61
+ /** memory-facts — the memory graph holds mgx:synonym / mgx:relatedTo / mgx:similarTo
62
+ * facts for the term. The memory-graph sibling of graph-loaded: the SKOS view
63
+ * answers from the conversational-memory store and misses honestly without it,
64
+ * with or without a code-map graph. */
65
+ const memoryFacts = () => Object.freeze({ type: VOCAB.Precondition, pred: PRECOND.memoryFacts });
60
66
 
61
67
  /** Add-effect: after the call the agent knows `topic` about `?of`. */
62
68
  const knows = (topic, ofParam = null) =>
@@ -80,7 +86,8 @@ function capability({ name, label, question, params = [], preconditions = [], ad
80
86
  // Arg keys verified against src/tools/server.mjs `dispatchTool`'s switch: describe/callers/
81
87
  // callees/tests/history/… take `symbol`; impact/exports take `module`; members/
82
88
  // subclasses take `class`; search takes `query` (+ optional kind/name/decorator);
83
- // architecture takes an optional `package`; untested takes nothing.
89
+ // architecture takes an optional `package`; untested takes nothing; related
90
+ // takes `term` (a memory-graph concept term).
84
91
 
85
92
  const CAPABILITIES = Object.freeze([
86
93
  capability({
@@ -107,9 +114,9 @@ const CAPABILITIES = Object.freeze([
107
114
  add: [knows("signature", "symbol")],
108
115
  }),
109
116
  capability({
110
- name: "tmct_impact", label: "impact", question: "what a change to this module reaches (impact closure)",
111
- params: [param("module", KINDS.Module)],
112
- preconditions: [graphLoaded(), resolves("module", KINDS.Module)],
117
+ name: "tmct_impact", label: "impact", question: "what a change to this module or symbol reaches (impact closure)",
118
+ params: [param("module", KINDS.Symbol, { note: "a Module, or any sited symbol — a fine-grained seed walks callsSymbol dependents and coarsens them to module grain" })],
119
+ preconditions: [graphLoaded(), resolves("module", KINDS.Symbol)],
113
120
  add: [knows("impact", "module")],
114
121
  }),
115
122
  capability({
@@ -178,6 +185,12 @@ const CAPABILITIES = Object.freeze([
178
185
  preconditions: [graphLoaded()],
179
186
  add: [knows("architecture", "package")],
180
187
  }),
188
+ capability({
189
+ 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.Query, { note: "a concept term, matched against memory relation facts rather than resolved in the code graph" })],
191
+ preconditions: [memoryFacts()],
192
+ add: [knows("related", "term")],
193
+ }),
181
194
  ]);
182
195
 
183
196
  // The live capability set: the built-in frozen array is the seed; registration
@@ -56,8 +56,10 @@ export const UNMAPPED_KINDS = Object.freeze({
56
56
 
57
57
  // ---- capabilities the NL surface cannot reach today (named, not accidental) ---
58
58
  // A declared capability with no NL/command/frame path is a routing gap and must be tagged
59
- // here with the reason. Every capability is currently reachable, so this is empty.
60
- export const NOT_NL_REACHABLE = Object.freeze({});
59
+ // here with the reason.
60
+ export const NOT_NL_REACHABLE = Object.freeze({
61
+ tmct_related: "the SKOS synonym/related surface is served by the chat lane's own recogniser over the memory graph; a router frame for it needs memory-term binding, which resolveObject (code-graph-only) does not prove yet",
62
+ });
61
63
 
62
64
  // ---- imperative intent FRAMES (fills what the relational grammar and command register
63
65
  // both miss). regex -> { topic, arg | noArg }. Ordered: first match wins.
@@ -0,0 +1,71 @@
1
+ // worlds-pack.mjs — the pure half of the shipped worlds pack: the row-shape
2
+ // validators every writer and reader share, the provenance tag a loaded
3
+ // world's facts carry, and the closed rule-kind set a world may instate. The
4
+ // pack itself is one gzipped JSONL shard per world plus a gzipped world
5
+ // index; loading them is I/O and lives in src/adapters/corpus/worlds-pack.mjs.
6
+ //
7
+ // A world row is one of three kinds:
8
+ // fact — an ordinary graph triple the loader appends into the session's
9
+ // memory store (rooms, exits, placements, NPC cast);
10
+ // rule — a pre-built action-Rule row (the same four action kinds the live
11
+ // teach frames store) the loader instates via appendRule;
12
+ // meta — the world's one announcement row (the opening line).
13
+
14
+ const WORLD_NAME_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
15
+
16
+ /** The action-rule kinds a world shard may carry — the same closed set
17
+ * src/adapters/memory/core.mjs stores for live-taught actions. */
18
+ export const WORLD_RULE_KINDS = Object.freeze([
19
+ "action-signature", "action-precond", "action-effect", "action-constraint",
20
+ ]);
21
+
22
+ const RULE_KIND_SET = new Set(WORLD_RULE_KINDS);
23
+
24
+ const isNonEmptyString = (v) => typeof v === "string" && v.trim() !== "";
25
+
26
+ /** A pack world name: lowercase, hyphen-joined ("ashcombe-hall"). */
27
+ export function isWorldName(name) {
28
+ return typeof name === "string" && WORLD_NAME_RE.test(name);
29
+ }
30
+
31
+ /** An index entry { s }: the shard (basename, no extension) holding the
32
+ * world's rows. */
33
+ export function isWorldsIndexEntry(e) {
34
+ return !!e && typeof e === "object" && isNonEmptyString(e.s);
35
+ }
36
+
37
+ /** A fact row: { world, kind:"fact", subject, predicate, object }. */
38
+ export function isWorldFactRow(row) {
39
+ return !!row && typeof row === "object" && row.kind === "fact"
40
+ && isWorldName(row.world)
41
+ && isNonEmptyString(row.subject) && isNonEmptyString(row.predicate) && isNonEmptyString(row.object);
42
+ }
43
+
44
+ /** A rule row: { world, kind:"rule", name, ruleKind, slots } — ruleKind one
45
+ * of WORLD_RULE_KINDS, slots a flat object of non-empty strings (the exact
46
+ * per-kind slot contract is appendRule's to enforce at instate time). */
47
+ export function isWorldRuleRow(row) {
48
+ if (!row || typeof row !== "object" || row.kind !== "rule") return false;
49
+ if (!isWorldName(row.world) || !isNonEmptyString(row.name)) return false;
50
+ if (!RULE_KIND_SET.has(row.ruleKind)) return false;
51
+ if (!row.slots || typeof row.slots !== "object" || Array.isArray(row.slots)) return false;
52
+ const values = Object.values(row.slots);
53
+ return values.length > 0 && values.every(isNonEmptyString);
54
+ }
55
+
56
+ /** A meta row: { world, kind:"meta", opening } — the world's opening line. */
57
+ export function isWorldMetaRow(row) {
58
+ return !!row && typeof row === "object" && row.kind === "meta"
59
+ && isWorldName(row.world) && isNonEmptyString(row.opening);
60
+ }
61
+
62
+ /** Any valid world row. */
63
+ export function isWorldRow(row) {
64
+ return isWorldFactRow(row) || isWorldRuleRow(row) || isWorldMetaRow(row);
65
+ }
66
+
67
+ /** The provenance tag every fact/rule loaded from a world carries —
68
+ * "world:<name>", so a loaded world is auditable apart from taught facts. */
69
+ export function worldProvenanceTag(worldName) {
70
+ return `world:${worldName}`;
71
+ }