@polycode-projects/the-mechanical-code-talker 2.6.1 → 2.7.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 +46 -1
- package/corpus/LICENSES.json +14 -0
- package/corpus/child/LICENSE-NOTICE +43 -0
- package/corpus/child/README.md +79 -0
- package/corpus/child/index.json.gz +0 -0
- package/corpus/child/manifest.json +313 -0
- package/corpus/child/shards/child-00.jsonl.gz +0 -0
- package/corpus/child/shards/child-01.jsonl.gz +0 -0
- package/corpus/child/shards/child-02.jsonl.gz +0 -0
- package/corpus/child/shards/child-03.jsonl.gz +0 -0
- package/corpus/child/shards/child-04.jsonl.gz +0 -0
- package/corpus/child/shards/child-05.jsonl.gz +0 -0
- package/corpus/child/shards/child-06.jsonl.gz +0 -0
- package/corpus/child/shards/child-07.jsonl.gz +0 -0
- package/corpus/child/shards/child-08.jsonl.gz +0 -0
- package/corpus/child/shards/child-09.jsonl.gz +0 -0
- package/corpus/child/shards/child-0a.jsonl.gz +0 -0
- package/corpus/child/shards/child-0b.jsonl.gz +0 -0
- package/corpus/child/shards/child-0c.jsonl.gz +0 -0
- package/corpus/child/shards/child-0d.jsonl.gz +0 -0
- package/corpus/child/shards/child-0e.jsonl.gz +0 -0
- package/corpus/child/shards/child-0f.jsonl.gz +0 -0
- package/corpus/child/shards/child-10.jsonl.gz +0 -0
- package/corpus/child/shards/child-11.jsonl.gz +0 -0
- package/corpus/child/shards/child-12.jsonl.gz +0 -0
- package/corpus/child/shards/child-13.jsonl.gz +0 -0
- package/corpus/child/shards/child-14.jsonl.gz +0 -0
- package/corpus/child/shards/child-15.jsonl.gz +0 -0
- package/corpus/child/shards/child-16.jsonl.gz +0 -0
- package/corpus/child/shards/child-17.jsonl.gz +0 -0
- package/corpus/child/shards/child-18.jsonl.gz +0 -0
- package/corpus/child/shards/child-19.jsonl.gz +0 -0
- package/corpus/child/shards/child-1a.jsonl.gz +0 -0
- package/corpus/child/shards/child-1b.jsonl.gz +0 -0
- package/corpus/child/shards/child-1c.jsonl.gz +0 -0
- package/corpus/child/shards/child-1d.jsonl.gz +0 -0
- package/corpus/child/shards/child-1e.jsonl.gz +0 -0
- package/corpus/child/shards/child-1f.jsonl.gz +0 -0
- package/corpus/conceptnet/child-seed.mjs +169 -0
- package/corpus/conceptnet/filter-dump.mjs +69 -48
- package/corpus/worlds/README.md +26 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +33 -0
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +64 -0
- package/package.json +4 -1
- package/src/adapters/corpus/child-pack.mjs +115 -0
- package/src/adapters/corpus/conceptnet-map.toml +7 -0
- package/src/adapters/corpus/worlds-pack.mjs +122 -0
- package/src/domain/ask-vocab.mjs +10 -2
- package/src/domain/ask.mjs +6 -0
- package/src/domain/child-pack.mjs +79 -0
- package/src/domain/grammar/ace.mjs +77 -0
- package/src/domain/grammar/lexicon-core.json +6 -0
- package/src/domain/interpret/normalize.mjs +32 -8
- package/src/domain/memory/trust.mjs +17 -0
- package/src/domain/worlds-pack.mjs +71 -0
- package/src/services/adventure.mjs +718 -0
- package/src/services/chat.mjs +137 -31
- package/src/surfaces/web/memory-ask-browser.bundle.js +555 -46
- package/src/tools/definitions.mjs +2 -2
|
@@ -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
|
+
}
|
package/src/domain/ask-vocab.mjs
CHANGED
|
@@ -466,12 +466,20 @@ export const PLACEHOLDER_NOUNS = Object.freeze([
|
|
|
466
466
|
|
|
467
467
|
/** Boolean connectives over same-subject clauses -> a set operation on result ids.
|
|
468
468
|
* "and" = intersection, "or" = union, "but not"/"and not"/"without"/"except" =
|
|
469
|
-
* difference.
|
|
470
|
-
*
|
|
469
|
+
* difference. The do-support negations ("but do not import Y", the expanded
|
|
470
|
+
* form every "don't" reaches after the contraction pass) are difference too —
|
|
471
|
+
* the auxiliary is part of the connective, never of the branch. A bare "but"
|
|
472
|
+
* is contrastive coordination, which still intersects ("inheriting from X but
|
|
473
|
+
* untested" = both at once). Multi-word keys are matched longest-first by
|
|
474
|
+
* ask.mjs so "but do not" wins over "but not" wins over a bare "but".
|
|
475
|
+
* Left-associative in ask.mjs's fold. */
|
|
471
476
|
export const BOOLEAN_CONNECTIVES = Object.freeze({
|
|
477
|
+
"but do not": "difference", "but does not": "difference",
|
|
478
|
+
"and do not": "difference", "and does not": "difference",
|
|
472
479
|
"but not": "difference", "and not": "difference", "except": "difference",
|
|
473
480
|
"without": "difference",
|
|
474
481
|
"and": "intersection", "plus": "intersection",
|
|
482
|
+
"but": "intersection",
|
|
475
483
|
"or": "union",
|
|
476
484
|
});
|
|
477
485
|
|
package/src/domain/ask.mjs
CHANGED
|
@@ -437,6 +437,12 @@ function parseNested(w, lc, nlp, depth) {
|
|
|
437
437
|
if (!noun) continue; // marker not preceded by a noun
|
|
438
438
|
const head = w.slice(0, r - 1); // outer clause words, minus the placeholder noun
|
|
439
439
|
if (!head.length) continue; // noun is the leading subject → subject-relative, not this shape
|
|
440
|
+
// A head made ENTIRELY of qualifier adjectives ("tested modules importing
|
|
441
|
+
// X") is an adjective stack over the subject, not an outer clause —
|
|
442
|
+
// "tested" doubles as a relation verb, so parseSimpleClause would read it
|
|
443
|
+
// as reverse(tests) over the inner set and answer the test module itself.
|
|
444
|
+
// parseRelationalOrQualified owns the adjective reading.
|
|
445
|
+
if (head.every((x) => QUALIFIERS[x.toLowerCase()])) continue;
|
|
440
446
|
const outer = parseSimpleClause([...head, NEST_SENTINEL].join(" "), nlp);
|
|
441
447
|
if (!outer || (outer.shape !== "reverse" && outer.shape !== "forward")) continue;
|
|
442
448
|
if (outer.modifier && outer.modifier !== "direct") continue; // no transitive-over-set closure primitive
|
|
@@ -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:";
|
|
@@ -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": {},
|
|
@@ -8,7 +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, TRAILING_TEMPORAL_ADVERBS,
|
|
11
|
+
TRAILING_SCOPE_FILLER, TRAILING_TEMPORAL_ADVERBS, stripTrailingDiscourseTag,
|
|
12
12
|
} from "../ask-vocab.mjs";
|
|
13
13
|
|
|
14
14
|
export function escapeRegex(s) {
|
|
@@ -251,18 +251,29 @@ export function applyPreambleFrames(text) {
|
|
|
251
251
|
return q;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
/** Strippable leading framing clause: "since/although/
|
|
255
|
-
* whereas/given that/now that <clause>, <Q>" -> "<Q>".
|
|
256
|
-
* non-empty-remainder-required, same discipline as
|
|
254
|
+
/** Strippable leading framing clause: "since/[even] though/although/while/
|
|
255
|
+
* because/[even] if/whereas/given that/now that <clause>, <Q>" -> "<Q>".
|
|
256
|
+
* Comma-anchored and non-empty-remainder-required, same discipline as
|
|
257
|
+
* GREETING_PREAMBLE_RE. */
|
|
257
258
|
const SUBORDINATION_FRAMES_RE =
|
|
258
|
-
/^(?:since|although|though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
|
|
259
|
+
/^(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+?,\s*(.+)$/i;
|
|
260
|
+
|
|
261
|
+
/** The same framing clause TRAILING the question ("<Q>, because the sprint
|
|
262
|
+
* just kicked off") — comma-anchored on the same subordinator set, so the
|
|
263
|
+
* clause never reads as part of the object term. A bare "while"/"since"
|
|
264
|
+
* mid-question without the comma is untouched (those can be content). */
|
|
265
|
+
const TRAILING_SUBORDINATION_RE =
|
|
266
|
+
/^(.+?),\s*(?:since|although|(?:even\s+)?though|while|because|whereas|given\s+that|now\s+that)\s+.+$/i;
|
|
259
267
|
|
|
260
268
|
export function applySubordinationFrames(text) {
|
|
261
269
|
let q = String(text || "");
|
|
262
270
|
for (let pass = 0; pass < 3; pass++) {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
q = m[1].trim();
|
|
271
|
+
const before = q;
|
|
272
|
+
let m = q.match(SUBORDINATION_FRAMES_RE);
|
|
273
|
+
if (m) q = m[1].trim();
|
|
274
|
+
m = q.match(TRAILING_SUBORDINATION_RE);
|
|
275
|
+
if (m) q = m[1].trim();
|
|
276
|
+
if (q === before) break;
|
|
266
277
|
}
|
|
267
278
|
return q;
|
|
268
279
|
}
|
|
@@ -390,6 +401,14 @@ export function normalizeQuery(text) {
|
|
|
390
401
|
q = applySubordinationFrames(q);
|
|
391
402
|
q = applyConditionalFrames(q);
|
|
392
403
|
q = stripFillerWords(q);
|
|
404
|
+
// A trailing bare discourse tag ("which modules import a.mjs THEN") is
|
|
405
|
+
// conversational glue, not part of the object term — the same curated call
|
|
406
|
+
// ask-vocab's stripTrailingDiscourseTag already makes for the meta-whatis
|
|
407
|
+
// object. Stripped here, in the shared pre-pass and AFTER the filler strip
|
|
408
|
+
// (the noise wrappers that leave the tag behind sit at the other end), so
|
|
409
|
+
// both parse strategies see one string and the residue guard never has to
|
|
410
|
+
// refuse over a word that carried no content.
|
|
411
|
+
q = stripTrailingDiscourseTag(q);
|
|
393
412
|
// emphatic trailing punctuation (item 10): a run of terminal "?" collapses to
|
|
394
413
|
// one — the anchored templates consume exactly one optional trailing "?", so
|
|
395
414
|
// "…walk.mjs??" otherwise leaks a stray "?" into the captured object term (the
|
|
@@ -485,6 +504,11 @@ const PHRASING_FRAMES = Object.freeze([
|
|
|
485
504
|
{ re: /^what\s+does\s+(.+?)\s+changes?\s+together\s+with\??$/i, to: (m) => `what co-changes with ${m[1]}` },
|
|
486
505
|
{ re: /^what\s+changes?\s+together\s+with\s+(.+?)\??$/i, to: (m) => `what co-changes with ${m[1]}` },
|
|
487
506
|
|
|
507
|
+
// COMMIT-COUNT PASSIVE ("how many commits are recorded for X") → the touch
|
|
508
|
+
// phrasing the count restrictor already compiles. "recorded" is no relation
|
|
509
|
+
// verb, so the passive otherwise dies in the restrictor parse.
|
|
510
|
+
{ re: /^how\s+many\s+commits\s+(?:are|were)\s+(?:recorded|logged)\s+(?:for|against)\s+(?:the\s+)?(.+?)\??$/i, to: (m) => `how many commits touched ${m[1]}` },
|
|
511
|
+
|
|
488
512
|
// AUTHORSHIP → "who touched X" (tmct's touch edge IS the authorship signal).
|
|
489
513
|
// A commit sha object is excluded — that dumps the commit's touch-set, not its author.
|
|
490
514
|
{ re: /^who\s+(?:wrote|authored)\s+(?:the\s+)?(?!(?:commit\s+)?[0-9a-f]{7,40}\??$)(.+?)\??$/i, to: (m) => `who touched ${m[1]}` },
|
|
@@ -30,6 +30,16 @@ 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)
|
|
38
|
+
* world:<name>[:turnN] -> { kind:"corpus", name:<name> }
|
|
39
|
+
* (a loaded world's facts are first-party authored shipped content — the
|
|
40
|
+
* same tier the hand-written tier2 corpus already scores at; the :turnN
|
|
41
|
+
* segment a snapshot write carries records when, not who, and is not
|
|
42
|
+
* part of the Source identity)
|
|
33
43
|
* ace:chat:<session>@<ts> -> { kind:"operator", createdAt:<ts>, sessionId:<session> }
|
|
34
44
|
* teach:chat:<session>@<ts> -> { kind:"teach", createdAt:<ts>, sessionId:<session> }
|
|
35
45
|
* web:<url> | url:<url> -> { kind:"web", url:<url> }
|
|
@@ -54,6 +64,13 @@ export function provenanceTagToSource(tag) {
|
|
|
54
64
|
const head = t.split(/\s+/)[0]; // drop trailing " /r/IsA" etc.
|
|
55
65
|
if (head.startsWith("corpus-weak:")) return { kind: "corpusWeak", name: head.slice("corpus-weak:".length) || "unknown" };
|
|
56
66
|
if (head.startsWith("corpus:")) return { kind: "corpus", name: head.slice("corpus:".length) || "unknown" };
|
|
67
|
+
// child:<pack>:<term> — the lazy child triples pack, scored at the corpus tier
|
|
68
|
+
// under the pack's shared Source; the per-term tail is dropped from the id.
|
|
69
|
+
if (head.startsWith("child:")) return { kind: "corpus", name: head.slice("child:".length).split(":")[0] || "unknown" };
|
|
70
|
+
// world:<name>[:turnN] — a loaded world's facts and snapshots, first-party
|
|
71
|
+
// authored shipped content scored at the corpus tier; the per-turn tail is
|
|
72
|
+
// dropped from the id so every write of one world corroborates one Source.
|
|
73
|
+
if (head.startsWith("world:")) return { kind: "corpus", name: head.slice("world:".length).split(":")[0] || "unknown" };
|
|
57
74
|
if (head.startsWith("ace:")) return { kind: "operator", ...parseChatTagRest(head.slice("ace:".length)) };
|
|
58
75
|
if (head.startsWith("teach:")) {
|
|
59
76
|
// the chat teach lane's natural frames — chat.mjs's teachProvenanceTag
|
|
@@ -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
|
+
}
|