@polycode-projects/the-mechanical-code-talker 2.11.5 → 2.11.9
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/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +250 -53
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +140 -138
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
|
@@ -106,6 +106,18 @@ const OPTIMISTIC_SKIP = new Set([
|
|
|
106
106
|
"our", "my", "your", "some", "any", "one", "kind", "sort", "type", "of",
|
|
107
107
|
]);
|
|
108
108
|
const OPTIMISTIC_ENTITY_HOPS = 4;
|
|
109
|
+
// Crossing any of these while scanning for a copula's entities voids the isa
|
|
110
|
+
// read — the noun on the far side belongs to a different clause or to a
|
|
111
|
+
// prepositional complement, not to "X is a Y".
|
|
112
|
+
const COPULA_FRAME_BLOCKERS = new Set(["VERB", "AUX", "ADP", "SCONJ", "CCONJ"]);
|
|
113
|
+
// Of-chain handling on a copula object: classifier heads read through to the
|
|
114
|
+
// real class; partitive containers state composition and yield no isa.
|
|
115
|
+
const COPULA_OF_READ_THROUGH = new Set(["type", "kind", "sort", "form", "class", "variety", "species", "breed", "genus"]);
|
|
116
|
+
// Naming periphrases stay copular: "can be termed as a name", "is known as",
|
|
117
|
+
// "is defined as" — the participle + "as" carries the same class claim the
|
|
118
|
+
// bare copula does, unlike any other verb after "is".
|
|
119
|
+
const COPULA_NAMING_PARTICIPLES = new Set(["termed", "known", "defined", "described", "referred", "called", "classified"]);
|
|
120
|
+
const COPULA_PARTITIVE_HEADS = new Set(["body", "mass", "group", "collection", "set", "series", "number", "amount", "piece", "part", "lot", "pair", "bunch", "pile"]);
|
|
109
121
|
|
|
110
122
|
/** Fold an entity surface to its stored key: a lexicon noun's lemma, else the
|
|
111
123
|
* word's own normFactTerm (the optimistic tier mints unlisted content nouns
|
|
@@ -128,22 +140,77 @@ function optimisticTriplesPos(sentence, lexicon, nlp) {
|
|
|
128
140
|
values = doc.tokens().out(nlp.its.value);
|
|
129
141
|
pos = doc.tokens().out(nlp.its.pos);
|
|
130
142
|
} catch { return []; }
|
|
131
|
-
|
|
143
|
+
// A found noun is read as its whole contiguous NOUN/PROPN run, head-lemma
|
|
144
|
+
// folded — "a string instrument" is the class "string instrument", never
|
|
145
|
+
// its modifier "string"; a single-word run keeps the plain lemma fold.
|
|
146
|
+
const isNounish = (i) => pos[i] === "NOUN" || pos[i] === "PROPN";
|
|
147
|
+
const entityRunAt = (i) => {
|
|
148
|
+
let lo = i;
|
|
149
|
+
let hi = i;
|
|
150
|
+
while (lo - 1 >= 0 && isNounish(lo - 1)) lo -= 1;
|
|
151
|
+
while (hi + 1 < values.length && isNounish(hi + 1)) hi += 1;
|
|
152
|
+
if (lo === hi) return foldEntity(values[i], lexicon);
|
|
153
|
+
const head = lookupNoun(lexicon, String(values[hi]).toLowerCase());
|
|
154
|
+
return normFactTerm([...values.slice(lo, hi), head ? head.lemma : values[hi]].join(" "));
|
|
155
|
+
};
|
|
156
|
+
const nearestEntity = (idx, step, blocked = null) => {
|
|
132
157
|
for (let i = idx + step; i >= 0 && i < values.length; i += step) {
|
|
133
158
|
if (pos[i] === "PUNCT") break;
|
|
134
|
-
if (
|
|
159
|
+
if (blocked && blocked.has(pos[i])) break;
|
|
160
|
+
if (isNounish(i)) return entityRunAt(i);
|
|
135
161
|
}
|
|
136
162
|
return null;
|
|
137
163
|
};
|
|
138
|
-
const tripleAt = (i, predicate) => {
|
|
139
|
-
const subject = nearestEntity(i, -1);
|
|
140
|
-
const object = nearestEntity(i, +1);
|
|
164
|
+
const tripleAt = (i, predicate, blocked = null) => {
|
|
165
|
+
const subject = nearestEntity(i, -1, blocked);
|
|
166
|
+
const object = nearestEntity(i, +1, blocked);
|
|
141
167
|
return subject && object && subject !== object ? { subject, predicate, object } : null;
|
|
142
168
|
};
|
|
169
|
+
// An isa needs a CLEAN copula frame: only determiners/adjectives/adverbs/
|
|
170
|
+
// numerals may sit between each entity and the copula. Crossing a verb or
|
|
171
|
+
// auxiliary means the noun belongs to another clause ("one reason life can
|
|
172
|
+
// exist here IS that earth …" is not "life is-a earth"); crossing a
|
|
173
|
+
// preposition or subordinator means locative/complement predication ("water
|
|
174
|
+
// is IN the oceans", "land is grouped INTO continents") — none of them
|
|
175
|
+
// class membership.
|
|
176
|
+
// An of-chain on the object reads through a classifier head to the real
|
|
177
|
+
// class ("a type of mammal" → mammal); a partitive container head states
|
|
178
|
+
// composition, never a class ("a large body of ice" — no isa at all).
|
|
179
|
+
const copulaObjectAt = (i) => {
|
|
180
|
+
for (let j = i + 1; j < values.length; j += 1) {
|
|
181
|
+
// A naming periphrasis ("… termed as …", "… known as …") keeps the
|
|
182
|
+
// frame copular: skip the participle and its "as" and read on.
|
|
183
|
+
if ((pos[j] === "VERB" || pos[j] === "AUX") && COPULA_NAMING_PARTICIPLES.has(values[j]?.toLowerCase())
|
|
184
|
+
&& values[j + 1]?.toLowerCase() === "as") { j += 1; continue; }
|
|
185
|
+
if (pos[j] === "PUNCT" || COPULA_FRAME_BLOCKERS.has(pos[j])) {
|
|
186
|
+
if (values[j]?.toLowerCase() !== "of") return null;
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (!isNounish(j)) continue;
|
|
190
|
+
let hi = j;
|
|
191
|
+
while (hi + 1 < values.length && isNounish(hi + 1)) hi += 1;
|
|
192
|
+
const headWord = String(values[hi]).toLowerCase();
|
|
193
|
+
const nextIsOf = values[hi + 1]?.toLowerCase() === "of";
|
|
194
|
+
if (!nextIsOf) return entityRunAt(j);
|
|
195
|
+
if (COPULA_OF_READ_THROUGH.has(headWord)) { i = hi + 1; j = hi + 1; continue; }
|
|
196
|
+
if (COPULA_PARTITIVE_HEADS.has(headWord)) return null;
|
|
197
|
+
return entityRunAt(j);
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
// The copula's own modal chain ("can be", "may be") is part of one verb
|
|
202
|
+
// complex — the subject scan starts left of it, while a free-standing VERB
|
|
203
|
+
// on the way still voids the frame.
|
|
204
|
+
const copulaSubjectAt = (i) => {
|
|
205
|
+
let k = i - 1;
|
|
206
|
+
while (k >= 0 && pos[k] === "AUX") k -= 1;
|
|
207
|
+
return nearestEntity(k + 1, -1, COPULA_FRAME_BLOCKERS);
|
|
208
|
+
};
|
|
143
209
|
for (let i = 1; i < values.length - 1; i += 1) {
|
|
144
210
|
if (pos[i] === "AUX" && OPTIMISTIC_COPULAS.has(values[i].toLowerCase())) {
|
|
145
|
-
const
|
|
146
|
-
|
|
211
|
+
const subject = copulaSubjectAt(i);
|
|
212
|
+
const object = copulaObjectAt(i);
|
|
213
|
+
if (subject && object && subject !== object) return [{ subject, predicate: "rdfs:subClassOf", object }];
|
|
147
214
|
}
|
|
148
215
|
}
|
|
149
216
|
for (let i = 1; i < values.length - 1; i += 1) {
|
package/src/services/init.mjs
CHANGED
|
@@ -40,7 +40,13 @@ export function defaultConfig() {
|
|
|
40
40
|
return {
|
|
41
41
|
graphFile: join(".tmct", "graph.json"),
|
|
42
42
|
corpus: { tier: "tier1" },
|
|
43
|
-
|
|
43
|
+
// captureUnknownContext defaults ON: every shipped tier-1/tier-2 curated
|
|
44
|
+
// bundle maps cleanly (no ace="none" relation appears in any of them), so
|
|
45
|
+
// this is a no-op against the default persona — it only starts capturing
|
|
46
|
+
// once an operator also activates a raw bundle like conceptnet/seon that
|
|
47
|
+
// has genuinely dropped rows, and there the capture is bounded by
|
|
48
|
+
// unknownContextLimit.
|
|
49
|
+
seed: { enabled: true, captureUnknownContext: true },
|
|
44
50
|
};
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -114,6 +120,14 @@ enabled = ${seed.enabled ? "true" : "false"}
|
|
|
114
120
|
# By default the WHOLE committed slice seeds (no cap — the operator's "seed all").
|
|
115
121
|
# To cap it, uncomment and set a number (definitional band first):
|
|
116
122
|
${seed.limit != null ? `limit = ${Number(seed.limit)}` : "# limit = 500"}
|
|
123
|
+
# Also capture a term that only ever appears in a relation the axiom graph
|
|
124
|
+
# drops (e.g. DerivedFrom/HasContext) — tagged with the passage it was found
|
|
125
|
+
# in, instead of vanishing. A no-op against every shipped bundle (none of
|
|
126
|
+
# them carry a dropped relation); it starts capturing once a bundle that does
|
|
127
|
+
# (conceptnet, seon, or a host-supplied one) is also active.
|
|
128
|
+
capture_unknown_context = ${seed.captureUnknownContext ? "true" : "false"}
|
|
129
|
+
# How many distinct terms one capture_unknown_context pass captures, at most:
|
|
130
|
+
${seed.unknownContextLimit != null ? `unknown_context_limit = ${Number(seed.unknownContextLimit)}` : "# unknown_context_limit = 500"}
|
|
117
131
|
`;
|
|
118
132
|
// [memory] backend — only emitted when a caller actually supplies it.
|
|
119
133
|
let out = base;
|
|
@@ -292,7 +306,10 @@ export async function initRepo(dir, { force = false, seed, env = process.env, pe
|
|
|
292
306
|
if (config.seed?.limit != null && entries.has("conceptnet")) {
|
|
293
307
|
entries.set("conceptnet", { ...entries.get("conceptnet"), limit: Number(config.seed.limit) });
|
|
294
308
|
}
|
|
295
|
-
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(memoryDir, entries
|
|
309
|
+
const { appended, skipped, total, perBundle } = await seedActiveCorpusEntries(memoryDir, entries, {
|
|
310
|
+
captureUnknownContext: config.seed?.captureUnknownContext,
|
|
311
|
+
unknownContextLimit: config.seed?.unknownContextLimit,
|
|
312
|
+
});
|
|
296
313
|
// If every active bundle failed, re-throw the first error so the outer catch
|
|
297
314
|
// reports it, rather than claiming success with zero facts written.
|
|
298
315
|
const bundleNames = Object.keys(perBundle);
|
|
@@ -363,6 +380,8 @@ async function readWrittenConfig(tomlPath, base) {
|
|
|
363
380
|
cfg.seed = { ...cfg.seed };
|
|
364
381
|
if (raw.seed.enabled !== undefined) cfg.seed.enabled = Boolean(raw.seed.enabled);
|
|
365
382
|
if (raw.seed.limit !== undefined) cfg.seed.limit = Number(raw.seed.limit);
|
|
383
|
+
if (raw.seed.capture_unknown_context !== undefined) cfg.seed.captureUnknownContext = Boolean(raw.seed.capture_unknown_context);
|
|
384
|
+
if (raw.seed.unknown_context_limit !== undefined) cfg.seed.unknownContextLimit = Number(raw.seed.unknown_context_limit);
|
|
366
385
|
}
|
|
367
386
|
// Sparse pass-through — src/services/extensions.mjs validates; this layer just carries the
|
|
368
387
|
// raw tables through unmodified.
|
|
@@ -588,8 +588,7 @@ ${THEME_TOKENS_CSS}
|
|
|
588
588
|
body { margin: 0; background: var(--bg); color: var(--ink); font-family: ${SERIF_STACK}; font-size: 16px; line-height: 1.5; }
|
|
589
589
|
.mono { font-family: ${MONO_STACK}; }
|
|
590
590
|
main { max-width: 1080px; margin: 0 auto; padding: 1.4rem 1.2rem 3rem; }
|
|
591
|
-
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; }
|
|
592
|
-
h1 { font-size: 1.4rem; margin: .3rem 0 .9rem; text-wrap: balance; }
|
|
591
|
+
.eyebrow { font-family: ${MONO_STACK}; font-size: .7rem; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); display: flex; flex-wrap: wrap; gap: .4em 1.2em; margin-bottom: .9rem; }
|
|
593
592
|
button { font: inherit; color: inherit; background: none; border: none; padding: 0; cursor: pointer; }
|
|
594
593
|
button:focus-visible, input:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; border-radius: 4px; }
|
|
595
594
|
.topbar { display: flex; flex-wrap: wrap; align-items: center; gap: .8rem; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: .5rem 0; margin-bottom: 1.1rem; }
|
|
@@ -715,7 +714,6 @@ ${THEME_TOKENS_CSS}
|
|
|
715
714
|
<body>
|
|
716
715
|
<main>
|
|
717
716
|
<div class="eyebrow"><span>tmct · memory ledger</span><span id="counts"></span></div>
|
|
718
|
-
<h1>A graph you can read</h1>
|
|
719
717
|
${dashboardHtml(stats)}
|
|
720
718
|
<div class="topbar">
|
|
721
719
|
<nav class="crumbs" id="crumbs" aria-label="Focus trail"></nav>
|