@polycode-projects/the-mechanical-code-talker 2.11.6 → 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.
@@ -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
- const nearestEntity = (idx, step) => {
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 (pos[i] === "NOUN" || pos[i] === "PROPN") return foldEntity(values[i], lexicon);
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 t = tripleAt(i, "rdfs:subClassOf");
146
- if (t) return [t];
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) {
@@ -40,7 +40,13 @@ export function defaultConfig() {
40
40
  return {
41
41
  graphFile: join(".tmct", "graph.json"),
42
42
  corpus: { tier: "tier1" },
43
- seed: { enabled: true },
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 &middot; 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>