@polycode-projects/the-mechanical-code-talker 1.9.2 → 1.10.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.
- package/README.md +441 -202
- package/bin/tmct.mjs +126 -1
- package/package.json +4 -2
- package/src/answer-variants.mjs +8 -36
- package/src/ask-browser-entry.mjs +5 -23
- package/src/ask-browser.bundle.js +1 -2
- package/src/ask-nlp.mjs +9 -23
- package/src/ask-vocab.mjs +139 -589
- package/src/ask.mjs +627 -1729
- package/src/chat.mjs +1684 -2872
- package/src/cli-args.mjs +14 -28
- package/src/codegraph.mjs +236 -644
- package/src/completions/complete.mjs +18 -62
- package/src/completions/graph-adapter.mjs +14 -60
- package/src/completions/group.mjs +12 -68
- package/src/completions/infer.mjs +38 -126
- package/src/completions/prune.mjs +17 -70
- package/src/completions/rank.mjs +16 -69
- package/src/completions/search.mjs +8 -31
- package/src/concept.mjs +32 -88
- package/src/conformance.mjs +11 -15
- package/src/corpus/conceptnet.mjs +31 -89
- package/src/corpus/templates.mjs +19 -45
- package/src/corpus/unknown-ingest.mjs +31 -92
- package/src/embed.mjs +10 -22
- package/src/extensions.mjs +50 -154
- package/src/finish.mjs +35 -91
- package/src/grammar/ace.mjs +16 -40
- package/src/grammar/assert.mjs +1 -1
- package/src/grammar/lexicon-core.json +1 -1
- package/src/grammar/lexicon.mjs +9 -27
- package/src/graph-merge.mjs +2 -3
- package/src/hash.mjs +6 -14
- package/src/index.mjs +6 -10
- package/src/init.mjs +38 -125
- package/src/interpret/fuzzy.mjs +10 -29
- package/src/interpret/merge.mjs +9 -27
- package/src/interpret/normalize.mjs +137 -585
- package/src/interpret/pipeline.mjs +23 -71
- package/src/interpret/strategies/ace.mjs +7 -31
- package/src/interpret/strategies/constructions.mjs +14 -41
- package/src/interpret/strategies/grammar.mjs +21 -60
- package/src/interpret/strategies/keywords.mjs +42 -131
- package/src/interpret/strategies/noise-strip.mjs +18 -89
- package/src/memory/bias.mjs +11 -54
- package/src/memory/blocks.mjs +18 -69
- package/src/memory/core.mjs +171 -591
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +7 -25
- package/src/memory/shacl.mjs +10 -39
- package/src/memory/trust.mjs +26 -127
- package/src/memory-ask-browser-entry.mjs +7 -30
- package/src/memory-ask-browser.bundle.js +1 -1
- package/src/paraphrase.mjs +20 -53
- package/src/planning.mjs +15 -157
- package/src/prose-nlp.mjs +4 -17
- package/src/prose.mjs +19 -67
- package/src/providers/bootstrap.mjs +1 -2
- package/src/providers/fixture.mjs +1 -2
- package/src/providers/graph-service.mjs +28 -59
- package/src/repository-interface.mjs +6 -8
- package/src/router/drive.mjs +183 -0
- package/src/router/goal-reasoner.mjs +66 -231
- package/src/router/guardrail.mjs +20 -58
- package/src/router/planner.mjs +15 -46
- package/src/router/registry.mjs +13 -43
- package/src/router/resolver.mjs +46 -131
- package/src/router/results.mjs +231 -0
- package/src/schema-docs.mjs +10 -27
- package/src/server-http.mjs +10 -19
- package/src/server.mjs +22 -28
- package/src/sessions.mjs +15 -30
- package/src/source-slice.mjs +5 -7
- package/src/source.mjs +10 -20
- package/src/syllogise.mjs +187 -575
- package/src/telemetry.mjs +3 -3
- package/src/toml-config.mjs +4 -4
- package/src/tui/app.mjs +9 -19
- package/src/viz.mjs +66 -123
- package/src/wink-model.mjs +10 -24
|
@@ -1,24 +1,14 @@
|
|
|
1
|
-
// corpus/conceptnet.mjs — the ConceptNet slice loader + memory seeder
|
|
2
|
-
// (ROADMAP Phase 2, "ConceptNet corpus slice").
|
|
1
|
+
// corpus/conceptnet.mjs — the ConceptNet slice loader + memory seeder.
|
|
3
2
|
//
|
|
4
3
|
// loadSlice(path?) stream corpus/conceptnet/slice.jsonl → assertions
|
|
5
4
|
// loadMap(path?) src/corpus/conceptnet-map.toml → Map(rel → row)
|
|
6
5
|
// toFacts(assertions,map) assertions → appendFact-shaped triples
|
|
7
|
-
// seedMemory(dir, opts) write them into <dir>/.tmct/memory via appendFacts
|
|
6
|
+
// seedMemory(dir, opts) write them into <dir>/.tmct/memory via appendFacts
|
|
8
7
|
//
|
|
9
|
-
// The slice is committed data (
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// ace = "none" are deliberate non-emissions. A slice relation MISSING from
|
|
14
|
-
// the table is a drift error — loud, never guessed around.
|
|
15
|
-
//
|
|
16
|
-
// Seeding goes through src/memory/core.mjs appendFacts() (memory is import-only
|
|
17
|
-
// here): fact ids are content-hashed from (s,p,o), so re-seeding is idempotent by
|
|
18
|
-
// construction. seedMemory pre-loads the store once and skips triples already
|
|
19
|
-
// present, then hands the survivors to appendFacts as ONE batched read-modify-
|
|
20
|
-
// write — so seeding the whole slice is O(N), not the O(N²) a per-fact appendFact
|
|
21
|
-
// loop would incur (the 6 k-fact slice: ~7 min → a couple of seconds).
|
|
8
|
+
// The slice is committed data (en→en only; CC-BY-SA 4.0 — see
|
|
9
|
+
// corpus/conceptnet/LICENSE-NOTICE). The mapping table decides which relations become
|
|
10
|
+
// memory facts and under which predicate; a slice relation missing from the table is a
|
|
11
|
+
// drift error, loud, never guessed around.
|
|
22
12
|
|
|
23
13
|
import { createReadStream } from "node:fs";
|
|
24
14
|
import { readFile } from "node:fs/promises";
|
|
@@ -32,29 +22,18 @@ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
|
32
22
|
export const SLICE_FILE = join(PKG_ROOT, "corpus", "conceptnet", "slice.jsonl");
|
|
33
23
|
export const MAP_FILE = join(PKG_ROOT, "src", "corpus", "conceptnet-map.toml");
|
|
34
24
|
|
|
35
|
-
// The tier-1 curated Software-Engineering ontology (SEON)
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// definition, sense} list the chat answer layer prefers for a lexicon term's
|
|
40
|
-
// "what is a <term>". tier-2 corpuses (aws/python/java) share the slice shape too.
|
|
25
|
+
// The tier-1 curated Software-Engineering ontology (SEON): concepts.jsonl shares
|
|
26
|
+
// ConceptNet's slice shape and loads through the same loadSlice/loadMap/toFacts path.
|
|
27
|
+
// definitions.jsonl is a separate {term, definition, sense} list for lexicon lookups.
|
|
28
|
+
// Tier-2 corpuses (aws/python/java) share the slice shape too.
|
|
41
29
|
export const SEON_CONCEPTS_FILE = join(PKG_ROOT, "corpus", "seon", "concepts.jsonl");
|
|
42
30
|
export const SEON_DEFINITIONS_FILE = join(PKG_ROOT, "corpus", "seon", "definitions.jsonl");
|
|
43
31
|
export const TIER2_DIR = join(PKG_ROOT, "corpus", "tier2");
|
|
44
32
|
export const TIER2_MANIFEST_FILE = join(TIER2_DIR, "manifest.json");
|
|
45
33
|
|
|
46
|
-
// corpus/wordnet/generate.mjs's output: the Open English WordNet ->
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// fetch-slice.mjs + slice.jsonl share one directory) rather than under a new
|
|
50
|
-
// "tier-3" name — corpus/README.md's tiering policy already uses "tier-3" for
|
|
51
|
-
// something else entirely (runtime-learned facts, NEVER committed), and this
|
|
52
|
-
// bundle is curated + committed, the same shape as a tier-2 corpus, just too
|
|
53
|
-
// large/mechanically-derived to hand-curate. "wordnet-xl"/"wordnet-full" are
|
|
54
|
-
// wired as BUILTIN_EXTENSIONS corpus entries in src/extensions.mjs, so
|
|
55
|
-
// `tmct import --corpus wordnet-xl` resolves directly there rather than
|
|
56
|
-
// through TIER2_MANIFEST_FILE's id lookup — see that module's own
|
|
57
|
-
// BUILTIN_EXTENSIONS comment.
|
|
34
|
+
// corpus/wordnet/generate.mjs's output: the Open English WordNet -> ConceptNet-shape
|
|
35
|
+
// conversion, same slice shape/loader path as tier-1/tier-2. "wordnet-xl"/"wordnet-full"
|
|
36
|
+
// are wired as BUILTIN_EXTENSIONS corpus entries in src/extensions.mjs.
|
|
58
37
|
export const WORDNET_DIR = join(PKG_ROOT, "corpus", "wordnet");
|
|
59
38
|
export const WORDNET_MANIFEST_FILE = join(WORDNET_DIR, "manifest.json");
|
|
60
39
|
|
|
@@ -114,17 +93,10 @@ export const termText = (uri) => {
|
|
|
114
93
|
return m ? m[1].replace(/_/g, " ") : null;
|
|
115
94
|
};
|
|
116
95
|
|
|
117
|
-
/** Map slice assertions → appendFact-shaped triples
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* Rows whose relation maps ace="none" are skipped — deliberate non-emission.
|
|
122
|
-
* A relation with NO row in the map throws: that is table drift, not data.
|
|
123
|
-
*
|
|
124
|
-
* `provenancePrefix` names the corpus half of the provenance string; it defaults
|
|
125
|
-
* to "corpus:conceptnet" so the ConceptNet seed stays BYTE-IDENTICAL to before.
|
|
126
|
-
* The seon / tier-2 corpuses reuse the same slice shape, tagged "corpus:seon" or
|
|
127
|
-
* "corpus:tier2:<id>" so a reader can tell a curated SE fact from ConceptNet noise. */
|
|
96
|
+
/** Map slice assertions → appendFact-shaped triples { subject, predicate, object,
|
|
97
|
+
* provenance }. Rows whose relation maps ace="none" are skipped; a relation with no row
|
|
98
|
+
* in the map throws (table drift, not data). `provenancePrefix` tags the corpus half of
|
|
99
|
+
* provenance, e.g. "corpus:seon" / "corpus:tier2:<id>" for non-ConceptNet callers. */
|
|
128
100
|
export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet") {
|
|
129
101
|
const facts = [];
|
|
130
102
|
for (const a of assertions) {
|
|
@@ -136,10 +108,8 @@ export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet")
|
|
|
136
108
|
const subject = termText(a.start);
|
|
137
109
|
const object = termText(a.end);
|
|
138
110
|
if (!subject || !object) continue; // non-en endpoint slipped in — filtered, not fatal
|
|
139
|
-
// mgx:relatedTo is
|
|
140
|
-
//
|
|
141
|
-
// (below plain corpus, above web) applies, computed from the Source's kind
|
|
142
|
-
// like every other tier, never hand-set on the Fact.
|
|
111
|
+
// mgx:relatedTo is low-precision (undirected, ambiguous) — routed through the
|
|
112
|
+
// corpus-weak: prefix so memory/trust.mjs's SOURCE_PRIOR.corpusWeak applies.
|
|
143
113
|
const prefix = row.predicate === "mgx:relatedTo"
|
|
144
114
|
? provenancePrefix.replace(/^corpus:/, "corpus-weak:")
|
|
145
115
|
: provenancePrefix;
|
|
@@ -153,39 +123,18 @@ export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet")
|
|
|
153
123
|
return facts;
|
|
154
124
|
}
|
|
155
125
|
|
|
156
|
-
/** Seed a repo's memory graph (<dir>/.tmct/memory/graph.json) from the
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* wants the DEFINITIONAL band ("a cache is a kind of buffer") ahead of the
|
|
163
|
-
* location trivia the slice happens to open with; without `prefer` the
|
|
164
|
-
* behavior is byte-identical to before.
|
|
165
|
-
*
|
|
166
|
-
* Idempotent twice over: appendFacts' content-hashed ids make a blind
|
|
167
|
-
* re-append an upsert, and we pre-read the store once to skip triples that
|
|
168
|
-
* are already there (so re-seeding costs one read, not N rewrites). The
|
|
169
|
-
* survivors are written in ONE batched appendFacts call, not a per-fact loop.
|
|
170
|
-
* Returns { appended, skipped, total }. `provenancePrefix` is threaded through to
|
|
171
|
-
* toFacts (default "corpus:conceptnet" → byte-identical seed) so a seon/tier-2
|
|
172
|
-
* corpus can tag its facts "corpus:seon" / "corpus:tier2:<id>".
|
|
126
|
+
/** Seed a repo's memory graph (<dir>/.tmct/memory/graph.json) from the committed slice.
|
|
127
|
+
* `limit` caps the facts written; `prefer` stable-partitions facts by predicate (so a
|
|
128
|
+
* capped seed favors the definitional band over whatever trivia the slice opens with).
|
|
129
|
+
* Idempotent: pre-reads the store to skip triples already there, then writes survivors
|
|
130
|
+
* in one batched appendFacts call. Returns { appended, skipped, total }.
|
|
131
|
+
* `provenancePrefix` tags facts (default "corpus:conceptnet").
|
|
173
132
|
*
|
|
174
|
-
* `captureUnknownContext` (default false
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* drops (an `ace = "none"` relation like RelatedTo/HasContext) still lands
|
|
180
|
-
* in memory, tagged with the passage it was found in, instead of vanishing.
|
|
181
|
-
* `unknownContextLimit` bounds how many distinct unknown terms one call
|
|
182
|
-
* captures (default 500 — see that module's own doc comment for why an
|
|
183
|
-
* unbounded sweep over a wide slice would not be "bounded, not padding").
|
|
184
|
-
* The result's `unknown` key is present only when the flag is set. Loaded
|
|
185
|
-
* dynamically (not a static import) to avoid a load-time import cycle with
|
|
186
|
-
* unknown-ingest.mjs, which itself statically imports `termText` from here —
|
|
187
|
-
* the same "avoid the cycle" discipline extensions.mjs's
|
|
188
|
-
* seedActiveCorpusEntries already uses for this very module. */
|
|
133
|
+
* `captureUnknownContext` (default false): also runs unknown-ingest.mjs's
|
|
134
|
+
* ingestUnknownFromAssertions so a term that only appears in an `ace="none"` row (never
|
|
135
|
+
* reified as a Fact) still lands in memory, tagged with the passage it was found in.
|
|
136
|
+
* `unknownContextLimit` bounds how many distinct terms one call captures (default 500).
|
|
137
|
+
* Loaded dynamically to avoid a load-time import cycle with unknown-ingest.mjs. */
|
|
189
138
|
export async function seedMemory(dir, {
|
|
190
139
|
limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix,
|
|
191
140
|
captureUnknownContext = false, unknownContextLimit,
|
|
@@ -194,15 +143,11 @@ export async function seedMemory(dir, {
|
|
|
194
143
|
let facts = toFacts(assertions, map, provenancePrefix);
|
|
195
144
|
if (Array.isArray(prefer) && prefer.length) {
|
|
196
145
|
const rank = new Map(prefer.map((p, i) => [p, i]));
|
|
197
|
-
// stable partition: Array.prototype.sort is stable in Node, so equal-rank
|
|
198
|
-
// facts keep their slice order — deterministic across runs by construction.
|
|
199
146
|
facts = facts.slice().sort((a, b) => (rank.get(a.predicate) ?? prefer.length) - (rank.get(b.predicate) ?? prefer.length));
|
|
200
147
|
}
|
|
201
148
|
if (limit !== undefined) facts = facts.slice(0, limit);
|
|
202
149
|
|
|
203
|
-
//
|
|
204
|
-
// memory's own normFactTerm so they match the normalized read-back exactly
|
|
205
|
-
// (appendFact converges /c/en/foo_bar, tmct:Foo and "Foo bar" to one term).
|
|
150
|
+
// Keyed with normFactTerm so it matches the store's own normalized read-back.
|
|
206
151
|
const factKey = (s, p, o) => `${normFactTerm(s)} ${p} ${normFactTerm(o)}`;
|
|
207
152
|
const existing = new Set();
|
|
208
153
|
const memory = await loadMemory(dir);
|
|
@@ -223,9 +168,6 @@ export async function seedMemory(dir, {
|
|
|
223
168
|
existing.add(key);
|
|
224
169
|
toWrite.push(fact);
|
|
225
170
|
}
|
|
226
|
-
// ONE read-modify-write for the whole seed (was one per fact — O(N²) I/O, ~7 min
|
|
227
|
-
// for the 6 k-fact slice). appendFacts also skips any malformed row rather than
|
|
228
|
-
// throwing, so its skipped count folds into the dedup skips here.
|
|
229
171
|
const res = await appendFacts(dir, toWrite);
|
|
230
172
|
|
|
231
173
|
let unknown;
|
package/src/corpus/templates.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// corpus/templates.mjs — the response-template library + SE phrase book loaders
|
|
2
|
-
//
|
|
1
|
+
// corpus/templates.mjs — the response-template library + SE phrase book loaders.
|
|
2
|
+
// Plain diffable data in, strict renderers out:
|
|
3
3
|
//
|
|
4
4
|
// data/templates/responses.jsonl {id, class, template, register} rows
|
|
5
5
|
// data/phrasebook/software-phrases.txt one phrase pattern per line
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// loadTemplates() validates the whole file (parse, required fields, unique
|
|
9
9
|
// ids) and caches; render(id, slots) is then synchronous and STRICT — an
|
|
10
10
|
// unknown id or a missing slot throws, it never emits a half-filled sentence.
|
|
11
|
-
// The response surface
|
|
12
|
-
//
|
|
11
|
+
// The response surface fills templates from grounded data only, so a thrown
|
|
12
|
+
// slot is a programming error, not a user-facing miss.
|
|
13
13
|
|
|
14
14
|
import { readFile } from "node:fs/promises";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,20 +19,14 @@ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
|
19
19
|
export const TEMPLATES_FILE = join(PKG_ROOT, "data", "templates", "responses.jsonl");
|
|
20
20
|
export const PHRASEBOOK_FILE = join(PKG_ROOT, "data", "phrasebook", "software-phrases.txt");
|
|
21
21
|
|
|
22
|
-
// Registers
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
// superlative + the provenance we already compute) as advanced prose. A
|
|
26
|
-
// technical template is FORMULAIC COMPETENCE: it renders via:"template", so the
|
|
27
|
-
// dual banding counts it in the PERFORMANCE band only, never the productive one.
|
|
22
|
+
// Registers: `terse|friendly` are the conversational bands; `technical` is the C1
|
|
23
|
+
// technical-paper band, rendering mechanical conclusions (count/comparison/superlative
|
|
24
|
+
// + provenance) as advanced prose.
|
|
28
25
|
const REGISTERS = new Set(["terse", "friendly", "technical"]);
|
|
29
26
|
const SLOT_RE = /\{([A-Za-z][A-Za-z0-9]*)\}/g;
|
|
30
27
|
|
|
31
|
-
// Slot-lint for the technical band:
|
|
32
|
-
//
|
|
33
|
-
// scopes, provenance) — no free-text slot can smuggle unattributable prose into
|
|
34
|
-
// the C1 register. Every technical row must also carry a {provenance} fill (the
|
|
35
|
-
// item-5 "+ provenance" contract: an advanced claim always shows its source).
|
|
28
|
+
// Slot-lint for the technical band: only mechanical values, no free-text slot — and
|
|
29
|
+
// every technical row must carry a {provenance} fill.
|
|
36
30
|
export const TECHNICAL_SLOTS = Object.freeze(new Set([
|
|
37
31
|
"subject", "count", "noun", "scope", "comparison", "metric", "unit",
|
|
38
32
|
"superlative", "provenance",
|
|
@@ -47,18 +41,12 @@ export function slotsOf(template) {
|
|
|
47
41
|
return out;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
|
-
// --- Segmentation IR
|
|
44
|
+
// --- Segmentation IR ---------------------------------------------------------
|
|
51
45
|
// A rendered answer is ALSO a list of typed spans: [{ type, text }, …] with
|
|
52
|
-
// type ∈ prose | entity | path | number | code | provenance | receipt
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// Slot kinds map a template hole to its protected span type. A slot fill is
|
|
58
|
-
// ALWAYS protected (never prose): it is grounded data, not our wording. The
|
|
59
|
-
// specific type is derived from the slot name; unknown slots fall back to the
|
|
60
|
-
// conservative `entity` (protect-when-unsure). Bytes never depend on the type,
|
|
61
|
-
// only on the fill, so the type is metadata layered over an exact split.
|
|
46
|
+
// type ∈ prose | entity | path | number | code | provenance | receipt, satisfying
|
|
47
|
+
// flatten(segments) === render(). Everything except `prose` is PROTECTED — a grammar
|
|
48
|
+
// rule may only transform prose spans, never touch a fact. A slot fill is always
|
|
49
|
+
// protected; unknown slot names fall back to the conservative `entity`.
|
|
62
50
|
const SLOT_KIND = {
|
|
63
51
|
count: "number",
|
|
64
52
|
when: "number",
|
|
@@ -152,22 +140,11 @@ export async function loadTemplates(path = TEMPLATES_FILE) {
|
|
|
152
140
|
return byId;
|
|
153
141
|
}
|
|
154
142
|
|
|
155
|
-
/** Load + merge several EXTENSION-PACK template files
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
* than inventing a same-id collision-precedence policy: a bare, unnamespaced
|
|
161
|
-
* id, or the SAME id appearing under two different paths, both throw loudly
|
|
162
|
-
* naming the offending path/id.
|
|
163
|
-
*
|
|
164
|
-
* loadTemplates(path) mutates the module's own render()-serving `cache` as a
|
|
165
|
-
* side effect; this function restores it to whatever it was before the merge
|
|
166
|
-
* ran, so calling loadTemplatesMerged() never clobbers the "current" default
|
|
167
|
-
* templates map for an unrelated caller (e.g. a concurrent render() call
|
|
168
|
-
* elsewhere in the same process). The returned map is NOT installed as the
|
|
169
|
-
* render() default — a caller that wants render() to serve the merged set
|
|
170
|
-
* passes it explicitly: render(id, slots, mergedMap). */
|
|
143
|
+
/** Load + merge several EXTENSION-PACK template files into one Map<id,row>. Every
|
|
144
|
+
* extension-pack template id must be namespaced "<packname>:<id>" (a bare or duplicate
|
|
145
|
+
* id throws, naming the offending path/id). Restores the module's `cache` to its
|
|
146
|
+
* pre-call state afterward — the returned map is not installed as the render() default;
|
|
147
|
+
* a caller passes it explicitly: render(id, slots, mergedMap). */
|
|
171
148
|
export async function loadTemplatesMerged(paths = []) {
|
|
172
149
|
const savedCache = cache;
|
|
173
150
|
const merged = new Map();
|
|
@@ -196,9 +173,6 @@ export async function loadTemplatesMerged(paths = []) {
|
|
|
196
173
|
* `templates` explicitly to bypass the module cache, e.g. in tests). */
|
|
197
174
|
export function render(id, slots = {}, templates = cache) {
|
|
198
175
|
if (!templates) throw new Error("render() before loadTemplates() — load the template library first");
|
|
199
|
-
// render() IS the flattened segmentation, by construction: the byte output is
|
|
200
|
-
// provably identical to the old `.replace(SLOT_RE, …)` (test/segments.test.mjs
|
|
201
|
-
// renders every responses.jsonl row both ways and asserts equality).
|
|
202
176
|
return flatten(renderSegments(id, slots, templates));
|
|
203
177
|
}
|
|
204
178
|
|
|
@@ -1,70 +1,29 @@
|
|
|
1
|
-
// corpus/unknown-ingest.mjs — context-preserving ingestion for unknown terms
|
|
2
|
-
// (PLAN_AGENTS.md §4 Phase 1, the "still not built at all" bullet).
|
|
1
|
+
// corpus/unknown-ingest.mjs — context-preserving ingestion for unknown terms.
|
|
3
2
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
3
|
+
// `toFacts()` (conceptnet.mjs) skips a row whose relation isn't axiom-worthy
|
|
4
|
+
// (`ace === "none"`, e.g. RelatedTo/Synonym/HasContext), which is right for the axiom
|
|
5
|
+
// graph but leaves a term that only ever appears in such a row with no anchor in memory
|
|
6
|
+
// at all. This module captures those terms as a separate, honestly-labelled kind of
|
|
7
|
+
// individual: a term is "unknown" if it's never been the subject/object of any reified
|
|
8
|
+
// Fact (in memory already, or in this same seeding batch). Each captured term gets a
|
|
9
|
+
// `mgx:contextPassage` fact (the passage it was found in — ConceptNet's own surfaceText,
|
|
10
|
+
// or the map's `surface` template) and `mgx:coOccursWith` links to its row's other
|
|
11
|
+
// endpoint plus any already-known terms recognizable in the same passage (bounded) —
|
|
12
|
+
// deliberately closed-vocabulary co-occurrence, not distributional/embedding inference.
|
|
14
13
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// otherwise vanish: a term is "unknown" here if it has never been the
|
|
18
|
-
// subject/object of any reified Fact — not in memory already, and not in the
|
|
19
|
-
// mapped facts this same seeding batch is about to write. For each dropped
|
|
20
|
-
// (ace="none") row that touches an unknown term, the term becomes a Fact
|
|
21
|
-
// tagged with the PASSAGE it was found in (ConceptNet's own `surfaceText`
|
|
22
|
-
// when present, else the map's own `surface` template filled with the row's
|
|
23
|
-
// two endpoints — both are committed, closed-vocabulary text; nothing here
|
|
24
|
-
// ever generates free text), via a dedicated `mgx:contextPassage` predicate.
|
|
25
|
-
// The row's OTHER endpoint (always) plus any already-known term recognizable
|
|
26
|
-
// by exact word/bigram match in the passage (bounded to a handful) are linked
|
|
27
|
-
// to the unknown term via one plain, closed-vocabulary co-occurrence
|
|
28
|
-
// predicate, `mgx:coOccursWith` — deliberately NOT distributional/embedding
|
|
29
|
-
// meaning induction (PLAN_AGENTS.md's own scoping): this buys traceable
|
|
30
|
-
// context, never automatic sense disambiguation.
|
|
31
|
-
//
|
|
32
|
-
// Reuses src/memory/core.mjs's existing appendFacts/loadMemory/normFactTerm
|
|
33
|
-
// machinery unmodified — a captured term is a completely ordinary Fact
|
|
34
|
-
// individual (same trust/provenance/Source pipeline every other fact gets),
|
|
35
|
-
// just carrying two new-but-closed predicates instead of an ACE-OWL one.
|
|
36
|
-
//
|
|
37
|
-
// Wiring: `seedMemory` (conceptnet.mjs) accepts an opt-in
|
|
38
|
-
// `captureUnknownContext: true` (default false — every existing seed call
|
|
39
|
-
// stays byte-identical) that calls `ingestUnknownFromAssertions` after the
|
|
40
|
-
// mapped facts are computed, dynamically imported (the same "avoid a static
|
|
41
|
-
// import cycle" discipline extensions.mjs's seedActiveCorpusEntries already
|
|
42
|
-
// uses for conceptnet.mjs itself).
|
|
43
|
-
//
|
|
44
|
-
// NOT covered here (out of scope for this module, see the caller's report):
|
|
45
|
-
// the LIVE chat teach/miss path (src/chat.mjs) has its own, separate
|
|
46
|
-
// unknown-word moment — a visitor's utterance mentioning a term the grammar
|
|
47
|
-
// can't classify — which has no "assertion batch" or ConceptNet-shaped
|
|
48
|
-
// surfaceText to draw a passage from at all. That needs its own hook (the
|
|
49
|
-
// raw utterance text IS the passage there); this module only ever consumes
|
|
50
|
-
// {start, rel, end, surfaceText?} shaped rows, so it cannot be reused as-is
|
|
51
|
-
// for that path without a chat.mjs-side adapter. See the report for the
|
|
52
|
-
// exact shape that hook would need — deliberately not built here.
|
|
14
|
+
// Wired as an opt-in `captureUnknownContext: true` on seedMemory (conceptnet.mjs),
|
|
15
|
+
// dynamically imported to avoid a load-time cycle with conceptnet.mjs.
|
|
53
16
|
|
|
54
17
|
import { appendFacts, loadMemory, normFactTerm, FACT_CLASS } from "../memory/core.mjs";
|
|
55
18
|
import { termText } from "./conceptnet.mjs";
|
|
56
19
|
|
|
57
|
-
/** unknown term -> the passage it was captured from
|
|
58
|
-
* text itself, capped by normFactTerm's own TEXT_CAP like any fact term). */
|
|
20
|
+
/** unknown term -> the passage it was captured from. */
|
|
59
21
|
export const CONTEXT_PASSAGE_PREDICATE = "mgx:contextPassage";
|
|
60
|
-
/**
|
|
61
|
-
* in the SAME passage. Deliberately the ONE relation this module ever
|
|
62
|
-
* emits for "these two showed up together" — no relation-type inference. */
|
|
22
|
+
/** term -> another term seen in the SAME passage — no relation-type inference. */
|
|
63
23
|
export const CO_OCCURS_PREDICATE = "mgx:coOccursWith";
|
|
64
24
|
|
|
65
|
-
// Function words filtered out of passage word/bigram scanning
|
|
66
|
-
//
|
|
67
|
-
// not context). Closed, small, hand-curated — not a general stopword list.
|
|
25
|
+
// Function words filtered out of passage word/bigram scanning (a "the"<->term edge
|
|
26
|
+
// would be noise, not context). Closed, small, hand-curated.
|
|
68
27
|
const STOPWORDS = new Set([
|
|
69
28
|
"a", "an", "the", "is", "are", "was", "were", "be", "being", "been",
|
|
70
29
|
"to", "of", "for", "in", "on", "at", "with", "by", "as", "and", "or",
|
|
@@ -73,11 +32,8 @@ const STOPWORDS = new Set([
|
|
|
73
32
|
|
|
74
33
|
const MAX_EXTRA_LINKS_PER_PASSAGE = 3; // bounded — not an open-ended sweep
|
|
75
34
|
|
|
76
|
-
/** The set of terms tmct already "recognizes": every subject/object across
|
|
77
|
-
*
|
|
78
|
-
* THIS batch is about to write introduce. A term with a real Fact anywhere
|
|
79
|
-
* already has structured knowledge — only a term that never gets one is a
|
|
80
|
-
* candidate for context-only capture. Pure; does not mutate `memory`. */
|
|
35
|
+
/** The set of terms tmct already "recognizes": every subject/object across every
|
|
36
|
+
* reified Fact in memory, union every term this batch's mapped facts introduce. Pure. */
|
|
81
37
|
export function knownTermsFrom(memory, mappedFacts) {
|
|
82
38
|
const known = new Set();
|
|
83
39
|
for (const ind of memory?.individuals || []) {
|
|
@@ -95,11 +51,9 @@ export function knownTermsFrom(memory, mappedFacts) {
|
|
|
95
51
|
return known;
|
|
96
52
|
}
|
|
97
53
|
|
|
98
|
-
/** The human-readable context passage for one
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* are committed, closed-vocabulary text — this never generates free text.
|
|
102
|
-
* Returns null when neither source is available (never fatal). */
|
|
54
|
+
/** The human-readable context passage for one dropped assertion — ConceptNet's own
|
|
55
|
+
* `surfaceText` (bracket-stripped) when present, else the map's `surface` template
|
|
56
|
+
* filled with the two endpoint terms. Returns null when neither is available. */
|
|
103
57
|
export function passageFor(assertion, row) {
|
|
104
58
|
const raw = assertion?.surfaceText;
|
|
105
59
|
if (typeof raw === "string" && raw.trim()) {
|
|
@@ -113,11 +67,8 @@ export function passageFor(assertion, row) {
|
|
|
113
67
|
return null;
|
|
114
68
|
}
|
|
115
69
|
|
|
116
|
-
/** Single words + adjacent bigrams in `passage` that are already-known terms
|
|
117
|
-
*
|
|
118
|
-
* endpoints — already linked directly). Bounded to
|
|
119
|
-
* MAX_EXTRA_LINKS_PER_PASSAGE hits; independent of `known`'s size (a Set
|
|
120
|
-
* lookup per token/bigram, never a substring sweep over every known term). */
|
|
70
|
+
/** Single words + adjacent bigrams in `passage` that are already-known terms, excluding
|
|
71
|
+
* `exclude` (the row's own two endpoints). Bounded to MAX_EXTRA_LINKS_PER_PASSAGE hits. */
|
|
121
72
|
function knownMentionsIn(passage, known, exclude) {
|
|
122
73
|
const tokens = String(passage || "").toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
123
74
|
const hits = [];
|
|
@@ -138,27 +89,15 @@ function knownMentionsIn(passage, known, exclude) {
|
|
|
138
89
|
}
|
|
139
90
|
|
|
140
91
|
/**
|
|
141
|
-
* Capture unknown terms out of the assertions a corpus-seeding batch is
|
|
142
|
-
*
|
|
143
|
-
* `ace = "none"` (the genuinely-dropped rows) — a mapped (ace != "none") row
|
|
144
|
-
* always gets a real reified Fact via toFacts()/appendFacts() already, so it
|
|
145
|
-
* is never a "silently dropped" case this module needs to rescue.
|
|
92
|
+
* Capture unknown terms out of the assertions a corpus-seeding batch is about to (or
|
|
93
|
+
* just did) write. Scans only rows whose relation maps to `ace = "none"`.
|
|
146
94
|
*
|
|
147
|
-
* { assertions, map, mappedFacts, memory?, provenancePrefix?, limit? }:
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* re-captured as "unknown").
|
|
152
|
-
* - memory: a pre-loaded payload (seedMemory already has one) — loaded
|
|
153
|
-
* fresh via loadMemory(dir) when omitted.
|
|
154
|
-
* - provenancePrefix: tags the captured facts, default "corpus:unknown".
|
|
155
|
-
* - limit: caps how many DISTINCT unknown terms get captured in one call
|
|
156
|
-
* (default 500) — a wide slice's RelatedTo rows alone number in the tens
|
|
157
|
-
* of thousands; this keeps one run bounded, not a flood.
|
|
95
|
+
* { assertions, map, mappedFacts, memory?, provenancePrefix?, limit? }: assertions/map are
|
|
96
|
+
* the same loadSlice()/loadMap() results seedMemory has; mappedFacts is this batch's
|
|
97
|
+
* toFacts() output; memory defaults to loadMemory(dir); limit (default 500) caps distinct
|
|
98
|
+
* unknown terms captured per call.
|
|
158
99
|
*
|
|
159
|
-
* Returns { captured, linked, appended, skipped }
|
|
160
|
-
* unknown terms newly given a contextPassage fact, `linked` = co-occurrence
|
|
161
|
-
* edges written (the direct pair plus any bounded extra known-term hits).
|
|
100
|
+
* Returns { captured, linked, appended, skipped }.
|
|
162
101
|
*/
|
|
163
102
|
export async function ingestUnknownFromAssertions(dir, {
|
|
164
103
|
assertions, map, mappedFacts = [], memory, provenancePrefix = "corpus:unknown", limit = 500,
|
package/src/embed.mjs
CHANGED
|
@@ -1,27 +1,15 @@
|
|
|
1
|
-
// embed.mjs — deterministic static-embedding lookup (model2vec potion-base-8M
|
|
2
|
-
//
|
|
1
|
+
// embed.mjs — deterministic static-embedding lookup (model2vec potion-base-8M,
|
|
2
|
+
// 29,528 WordPiece subwords × 256 fp32 dims). Pure table lookup + float
|
|
3
|
+
// arithmetic, no ONNX runtime, no network after the one-time fetch — the same
|
|
4
|
+
// text always embeds to the same vector.
|
|
3
5
|
//
|
|
4
|
-
// The
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// calls, no network after the one-time fetch — pure table lookup + float arithmetic, so
|
|
8
|
-
// the same text always embeds to the same vector ($0, deterministic, offline).
|
|
6
|
+
// The safetensors reader and WordPiece tokenizer below are hand-rolled: both
|
|
7
|
+
// formats are simple enough to parse directly, avoiding an ONNX/HF tokenizer
|
|
8
|
+
// dependency for it.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// + greedy longest-match with "##" continuations) — both are small enough to hand-roll
|
|
14
|
-
// with node built-ins, so neither the @yarflam/potion-base-8m fallback package nor an HF
|
|
15
|
-
// tokenizer dependency is taken. Numerical intent follows model2vec's own encode (subword
|
|
16
|
-
// ids WITHOUT the [CLS]/[SEP] template, mean pool, normalize per config.json) — exact
|
|
17
|
-
// float parity with the Python lib is not claimed; determinism and rank usefulness are.
|
|
18
|
-
//
|
|
19
|
-
// The weights are NEVER committed and NEVER in the npm package: they live in the
|
|
20
|
-
// gitignored vendor/ tree (fetched by scripts/fetch-embeddings.mjs, `npm run
|
|
21
|
-
// refs:embeddings`, SHA-256-pinned like the repo's other binary artefacts). Everything
|
|
22
|
-
// here degrades gracefully — loadEmbedder() returns null when the weights dir is absent,
|
|
23
|
-
// and callers (codegraph.mjs's opt-in embedRank, scripts/rank-gate.mjs) no-op with a
|
|
24
|
-
// clear note instead of failing, so CI/tests never require the 30 MB download.
|
|
10
|
+
// Weights are gitignored (vendor/, fetched by scripts/fetch-embeddings.mjs)
|
|
11
|
+
// and never in the npm package; loadEmbedder() returns null when absent so
|
|
12
|
+
// CI/tests never require the download.
|
|
25
13
|
|
|
26
14
|
import { readFileSync, existsSync } from "node:fs";
|
|
27
15
|
import { join, dirname } from "node:path";
|