@polycode-projects/the-mechanical-code-talker 1.4.0 → 1.5.2
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 +1 -1
- package/ROADMAP.md +98 -11
- package/corpus/README.md +23 -22
- package/corpus/seon/README.md +7 -6
- package/corpus/seon/concepts.jsonl +119 -0
- package/corpus/tier2/general.jsonl +49 -0
- package/corpus/tier2/generate.mjs +68 -0
- package/corpus/tier2/manifest.json +14 -0
- package/data/templates/constructions/agent-noun-relations.toml +98 -0
- package/data/templates/responses.jsonl +1 -0
- package/package.json +5 -1
- package/src/ask-vocab.mjs +39 -1
- package/src/ask.mjs +278 -32
- package/src/chat.mjs +681 -212
- package/src/completions/complete.mjs +138 -0
- package/src/completions/group.mjs +171 -0
- package/src/completions/infer.mjs +395 -0
- package/src/completions/prune.mjs +156 -0
- package/src/completions/rank.mjs +154 -0
- package/src/completions/search.mjs +85 -0
- package/src/corpus/conceptnet.mjs +36 -3
- package/src/corpus/unknown-ingest.mjs +209 -0
- package/src/extensions.mjs +14 -4
- package/src/finish.mjs +61 -18
- package/src/grammar/ace.mjs +24 -338
- package/src/grammar/lexicon.mjs +37 -194
- package/src/interpret/pipeline.mjs +23 -2
- package/src/interpret/strategies/constructions.mjs +207 -0
- package/src/interpret/strategies/grammar.mjs +24 -3
- package/src/interpret/strategies/keywords.mjs +34 -0
- package/src/memory/blocks.mjs +7 -2
- package/src/memory/core.mjs +283 -20
- package/src/memory/shacl.mjs +114 -0
- package/src/prose.mjs +5 -1
- package/src/syllogise.mjs +0 -0
- package/src/grammar/lexicon-core.json +0 -302
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// interpret/strategies/constructions.mjs — strategy N+1: construction-grammar
|
|
2
|
+
// template banks (PLAN_ADVANCED_GRAMMAR.md track (d)). Per-construction closed
|
|
3
|
+
// template families loaded as DATA from data/templates/constructions/*.toml
|
|
4
|
+
// (pattern -> AST skeleton, slot types validated against the closed RELATIONS/
|
|
5
|
+
// ENTITY_TO_TYPE vocabulary ask-vocab.mjs already owns), registered here as its
|
|
6
|
+
// OWN additive class ("construction") — the same "own-class strategy" pattern
|
|
7
|
+
// interpret/strategies/ace.mjs and noise-strip.mjs already use, so a construction
|
|
8
|
+
// match outranks a same-text keyword-spot GUESS outright (interpret/merge.mjs
|
|
9
|
+
// picks the highest-confidence CLASS; within-class disagreement is the honest
|
|
10
|
+
// {ambiguousParse} tie, which this strategy deliberately avoids triggering
|
|
11
|
+
// against keyword-spot by living in its own class) rather than colliding with it.
|
|
12
|
+
//
|
|
13
|
+
// The point (mirrors grammar.mjs's own file-header precedent, "same shape, new
|
|
14
|
+
// grammatical coverage, not a new mechanism"): grammar GROWTH as committed data,
|
|
15
|
+
// not more normalize.mjs/grammar.mjs code — data/templates/grammar-rules.toml
|
|
16
|
+
// and data/templates/responses.jsonl already work this way. Continues
|
|
17
|
+
// grammar.mjs's T1-T10 numbering (T11+, see the TOML file's own [[construction]]
|
|
18
|
+
// `id` fields) without renumbering anything grammar.mjs already owns.
|
|
19
|
+
//
|
|
20
|
+
// Loader discipline (mirrors src/finish.mjs's loadGrammarRules/grammarRules
|
|
21
|
+
// pattern exactly): synchronous (the pipeline is sync-capable), cached once per
|
|
22
|
+
// process, and DEFENSIVE — a missing directory, unparseable TOML, or an entry
|
|
23
|
+
// that fails validation (an unrecognized `kind`/`entityType`, a malformed
|
|
24
|
+
// pattern) is silently DROPPED, never thrown and never guessed into the nearest
|
|
25
|
+
// match. "One broken strategy/entry never takes the pipeline down"
|
|
26
|
+
// (interpret/pipeline.mjs's own file-header discipline) extends here to one
|
|
27
|
+
// broken DATA ROW never taking the strategy down.
|
|
28
|
+
|
|
29
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import { join, dirname } from "node:path";
|
|
32
|
+
import { parse as parseToml } from "smol-toml";
|
|
33
|
+
|
|
34
|
+
import { RELATIONS, ENTITY_TO_TYPE } from "../../ask-vocab.mjs";
|
|
35
|
+
import { escapeRegex } from "../normalize.mjs";
|
|
36
|
+
|
|
37
|
+
const STRATEGY_DIR = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
/** The construction-bank directory (data, not code) — every *.toml file inside
|
|
39
|
+
* is loaded, in filename order, so a future bank is a new committed file, not
|
|
40
|
+
* an edit to this loader. */
|
|
41
|
+
export const CONSTRUCTIONS_DIR = join(STRATEGY_DIR, "..", "..", "..", "data", "templates", "constructions");
|
|
42
|
+
|
|
43
|
+
const VALID_KINDS = new Set(Object.keys(RELATIONS));
|
|
44
|
+
const VALID_ENTITY_TYPES = new Set(Object.values(ENTITY_TO_TYPE));
|
|
45
|
+
const VALID_SHAPES = new Set(["ask", "reverse", "forward", "where", "when", "meta", "mentions"]);
|
|
46
|
+
|
|
47
|
+
/** Read every *.toml file in `dir` (sorted, deterministic) and return the raw
|
|
48
|
+
* parsed tables concatenated: {relations:[...], constructions:[...]}. A
|
|
49
|
+
* missing directory or an unparseable file is DEFENSIVE (per-file: a broken
|
|
50
|
+
* file is skipped, not fatal to the others) — callers get whatever validly
|
|
51
|
+
* parsed, never a thrown error from a data-authoring mistake. */
|
|
52
|
+
export function readConstructionFiles(dir = CONSTRUCTIONS_DIR) {
|
|
53
|
+
let files;
|
|
54
|
+
try {
|
|
55
|
+
files = readdirSync(dir).filter((f) => f.endsWith(".toml")).sort();
|
|
56
|
+
} catch {
|
|
57
|
+
return { relations: [], constructions: [] };
|
|
58
|
+
}
|
|
59
|
+
const relations = [];
|
|
60
|
+
const constructions = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = parseToml(readFileSync(join(dir, file), "utf8"));
|
|
65
|
+
} catch {
|
|
66
|
+
continue; // one malformed file never takes the others down
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(parsed.relation)) relations.push(...parsed.relation);
|
|
69
|
+
if (Array.isArray(parsed.construction)) constructions.push(...parsed.construction);
|
|
70
|
+
}
|
|
71
|
+
return { relations, constructions };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Validate + index the raw [[relation]] rows into noun -> {kind, entityType}.
|
|
75
|
+
* Closed-vocabulary validation (the whole point of track (d)'s "slot types
|
|
76
|
+
* validated against ENTITY_TO_TYPE/VERB_TO_KIND" deliverable): `kind` MUST be
|
|
77
|
+
* one of RELATIONS' own keys and `entityType` (when present) MUST be one of
|
|
78
|
+
* ENTITY_TO_TYPE's canonical class names — an entry failing either check is
|
|
79
|
+
* dropped, never coerced to the nearest-looking valid value. First occurrence
|
|
80
|
+
* of a noun wins (closed-table "first match" discipline, same as every other
|
|
81
|
+
* table in this codebase); a later duplicate is silently ignored. */
|
|
82
|
+
export function buildAgentNounTable(relations) {
|
|
83
|
+
const table = {};
|
|
84
|
+
for (const r of relations || []) {
|
|
85
|
+
if (!r || typeof r.noun !== "string" || !r.noun.trim()) continue;
|
|
86
|
+
if (typeof r.kind !== "string" || !VALID_KINDS.has(r.kind)) continue;
|
|
87
|
+
if (r.entityType !== undefined && (typeof r.entityType !== "string" || !VALID_ENTITY_TYPES.has(r.entityType))) continue;
|
|
88
|
+
const noun = r.noun.trim().toLowerCase();
|
|
89
|
+
if (table[noun]) continue;
|
|
90
|
+
table[noun] = { kind: r.kind, entityType: r.entityType || null };
|
|
91
|
+
}
|
|
92
|
+
return table;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Compile one pattern string ("<AGENT> of <TERM>") against the closed agent-
|
|
96
|
+
* noun alternation into {re, agentIndex, termIndex}, or null when the pattern
|
|
97
|
+
* doesn't carry exactly one <AGENT> and one <TERM> token (a malformed pattern
|
|
98
|
+
* — dropped, not guessed at). Literal text is escaped and whitespace-
|
|
99
|
+
* normalized (\s+), matching every other anchored-template regex in this
|
|
100
|
+
* codebase (grammar.mjs's own TEMPLATES). Case-insensitive; tolerates one
|
|
101
|
+
* optional trailing "?", same as grammar.mjs's own templates. */
|
|
102
|
+
function compilePattern(pattern, agentNouns) {
|
|
103
|
+
if (typeof pattern !== "string" || !pattern.trim() || !agentNouns.length) return null;
|
|
104
|
+
const agentAlt = agentNouns.slice().sort((a, b) => b.length - a.length).map(escapeRegex).join("|");
|
|
105
|
+
const parts = pattern.split(/(<AGENT>|<TERM>)/).filter((p) => p !== "");
|
|
106
|
+
let source = "";
|
|
107
|
+
const slots = [];
|
|
108
|
+
for (const part of parts) {
|
|
109
|
+
if (part === "<AGENT>") {
|
|
110
|
+
slots.push("agent");
|
|
111
|
+
source += `(${agentAlt})`;
|
|
112
|
+
} else if (part === "<TERM>") {
|
|
113
|
+
slots.push("term");
|
|
114
|
+
source += `(.+?)`;
|
|
115
|
+
} else {
|
|
116
|
+
source += part.split(/(\s+)/).map((seg) => (/^\s+$/.test(seg) ? "\\s+" : escapeRegex(seg))).join("");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const agentCount = slots.filter((s) => s === "agent").length;
|
|
120
|
+
const termCount = slots.filter((s) => s === "term").length;
|
|
121
|
+
if (agentCount !== 1 || termCount !== 1) return null;
|
|
122
|
+
return {
|
|
123
|
+
re: new RegExp(`^${source}\\??$`, "i"),
|
|
124
|
+
agentIndex: slots.indexOf("agent") + 1,
|
|
125
|
+
termIndex: slots.indexOf("term") + 1,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Validate + compile the raw [[construction]] rows into runnable templates:
|
|
130
|
+
* {id, name, shape, re, agentIndex, termIndex}. Requires a valid `id`
|
|
131
|
+
* (non-empty string, first-occurrence-wins on duplicates), a `shape` from the
|
|
132
|
+
* anchored-template shape vocabulary (VALID_SHAPES), and a pattern that
|
|
133
|
+
* compiles cleanly against the agent-noun table — anything else is dropped. */
|
|
134
|
+
export function buildConstructionTemplates(constructions, agentNounTable) {
|
|
135
|
+
const agentNouns = Object.keys(agentNounTable);
|
|
136
|
+
const seen = new Set();
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const c of constructions || []) {
|
|
139
|
+
if (!c || typeof c.id !== "string" || !c.id.trim() || seen.has(c.id)) continue;
|
|
140
|
+
if (typeof c.shape !== "string" || !VALID_SHAPES.has(c.shape)) continue;
|
|
141
|
+
const compiled = compilePattern(c.pattern, agentNouns);
|
|
142
|
+
if (!compiled) continue;
|
|
143
|
+
seen.add(c.id);
|
|
144
|
+
out.push({ id: c.id, name: c.name || c.id, shape: c.shape, ...compiled });
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let bankCache = null;
|
|
150
|
+
|
|
151
|
+
/** The cached, compiled construction bank (relations table + runnable
|
|
152
|
+
* templates), loaded once per process. Defensive: any failure anywhere in the
|
|
153
|
+
* load/validate/compile chain degrades to an empty bank (the strategy simply
|
|
154
|
+
* never fires) rather than crashing the pipeline that imports this module. */
|
|
155
|
+
export function constructionBank(dir = CONSTRUCTIONS_DIR) {
|
|
156
|
+
if (bankCache !== null && dir === CONSTRUCTIONS_DIR) return bankCache;
|
|
157
|
+
let bank;
|
|
158
|
+
try {
|
|
159
|
+
const { relations, constructions } = readConstructionFiles(dir);
|
|
160
|
+
const agentNounTable = buildAgentNounTable(relations);
|
|
161
|
+
const templates = buildConstructionTemplates(constructions, agentNounTable);
|
|
162
|
+
bank = { agentNounTable, templates };
|
|
163
|
+
} catch {
|
|
164
|
+
bank = { agentNounTable: {}, templates: [] };
|
|
165
|
+
}
|
|
166
|
+
if (dir === CONSTRUCTIONS_DIR) bankCache = bank;
|
|
167
|
+
return bank;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Strategy 1: scan the compiled construction templates, first match wins
|
|
171
|
+
* (mirrors grammar.mjs's parseAnchored exactly). A structural regex match
|
|
172
|
+
* whose agent noun somehow isn't in the table (shouldn't happen — the
|
|
173
|
+
* alternation is built FROM the table) falls through defensively rather than
|
|
174
|
+
* building a half-formed parse. Pure. */
|
|
175
|
+
export function parseConstruction(text, bank = constructionBank()) {
|
|
176
|
+
for (const t of bank.templates) {
|
|
177
|
+
const m = text.match(t.re);
|
|
178
|
+
if (!m) continue;
|
|
179
|
+
const agentText = m[t.agentIndex].toLowerCase();
|
|
180
|
+
const relation = bank.agentNounTable[agentText];
|
|
181
|
+
if (!relation) continue;
|
|
182
|
+
const object = m[t.termIndex].trim();
|
|
183
|
+
if (!object) continue;
|
|
184
|
+
return {
|
|
185
|
+
shape: t.shape, entityType: relation.entityType || null,
|
|
186
|
+
modifier: "direct", kind: relation.kind, object,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Pipeline registration (interpret/pipeline.mjs): construction-grammar
|
|
193
|
+
* templates as their OWN class ("construction"), confidence 0.9 — the same
|
|
194
|
+
* evidentiary weight as grammar.mjs's anchored T1-T10 (an anchored, closed
|
|
195
|
+
* pattern match), so it outright outranks a same-text "graph-query"-class
|
|
196
|
+
* keyword-spot guess (0.7) instead of triggering a same-class {ambiguousParse}
|
|
197
|
+
* tie against it (see this file's header). */
|
|
198
|
+
export const constructionsStrategy = {
|
|
199
|
+
id: "constructions",
|
|
200
|
+
class: "construction",
|
|
201
|
+
run(text) {
|
|
202
|
+
const parsed = parseConstruction(text);
|
|
203
|
+
return parsed
|
|
204
|
+
? { strategyId: "constructions", class: "construction", candidates: [{ parsed, confidence: 0.9 }] }
|
|
205
|
+
: null;
|
|
206
|
+
},
|
|
207
|
+
};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
VERB_TO_KIND, ENTITY_TO_TYPE, MODIFIER_TO_KIND,
|
|
9
9
|
META_MEANING_VERBS, WHERE_MARKERS, MENTION_MARKERS,
|
|
10
|
-
INHERITS_REVERSE_VERBS, stripTrailingScopeFiller,
|
|
10
|
+
INHERITS_REVERSE_VERBS, stripTrailingScopeFiller, stripTrailingDiscourseTag,
|
|
11
11
|
} from "../../ask-vocab.mjs";
|
|
12
12
|
import { escapeRegex } from "../normalize.mjs";
|
|
13
13
|
|
|
@@ -96,12 +96,15 @@ const TEMPLATES = [
|
|
|
96
96
|
// Fix 3: stripTrailingScopeFiller (ask-vocab.mjs) trims a curated trailing clause
|
|
97
97
|
// ("what is a Module in this graph" -> "Module") off the object before it's
|
|
98
98
|
// returned, so a scoping tail never corrupts the lookup term either the bare-form
|
|
99
|
-
// check above or downstream resolution/rendering perform.
|
|
99
|
+
// check above or downstream resolution/rendering perform. HANDOVER.md 2026-07-10
|
|
100
|
+
// item 8: stripTrailingDiscourseTag trims a bare trailing "then"/"though" the
|
|
101
|
+
// same way ("what is a component then" -> "component") — applied first, since a
|
|
102
|
+
// discourse tag sits outermost when both happen to stack.
|
|
100
103
|
{
|
|
101
104
|
name: "meta-whatis",
|
|
102
105
|
re: new RegExp(`^what\\s+(?:is|are)\\s+(?:(an?)\\s+)?(.+?)\\??$`, "i"),
|
|
103
106
|
build: (m) => {
|
|
104
|
-
const object = m[2].trim();
|
|
107
|
+
const object = stripTrailingDiscourseTag(m[2].trim());
|
|
105
108
|
if (!m[1] && !ENTITY_TO_TYPE[object.toLowerCase()]) return null; // bare form: closed-set only
|
|
106
109
|
return { shape: "meta", entityType: null, modifier: "direct", kind: "meta", object: stripTrailingScopeFiller(object) };
|
|
107
110
|
},
|
|
@@ -135,6 +138,24 @@ const TEMPLATES = [
|
|
|
135
138
|
? { shape: "when", entityType: null, modifier: "direct", kind: "touches", object: m[1].trim() }
|
|
136
139
|
: null),
|
|
137
140
|
},
|
|
141
|
+
// T9 commit-history NP (PLAN_CHAT_FEEL item 6 remainder): "the commit history of
|
|
142
|
+
// X" / "commit history for X" — an NP form of T8's SAME "when did X change"
|
|
143
|
+
// intent; reuses shape="when" verbatim so evaluation/rendering are byte-
|
|
144
|
+
// identical, only the recognizer surface differs.
|
|
145
|
+
{
|
|
146
|
+
name: "commit-history",
|
|
147
|
+
re: /^(?:the\s+)?commit\s+history\s+(?:of|for)\s+(.+?)\??$/i,
|
|
148
|
+
build: (m) => ({ shape: "when", entityType: null, modifier: "direct", kind: "touches", object: m[1].trim() }),
|
|
149
|
+
},
|
|
150
|
+
// T10 cochange-partners NP (PLAN_CHAT_FEEL item 6 remainder): "cochange partners
|
|
151
|
+
// of X" — an NP form of the existing "which modules cochange with X" verb-phrase
|
|
152
|
+
// shape (ask-vocab.mjs's cochange verb table); reuses shape="reverse"/
|
|
153
|
+
// kind="cochange" so evaluation is byte-identical.
|
|
154
|
+
{
|
|
155
|
+
name: "cochange-partners",
|
|
156
|
+
re: /^co-?change\s+partners\s+(?:of|for|with)\s+(.+?)\??$/i,
|
|
157
|
+
build: (m) => ({ shape: "reverse", entityType: "Module", modifier: "direct", kind: "cochange", object: m[1].trim() }),
|
|
158
|
+
},
|
|
138
159
|
];
|
|
139
160
|
|
|
140
161
|
/** Strategy 1: the original P0 anchored grammar — the whole (normalized) string
|
|
@@ -96,6 +96,27 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
96
96
|
return { shape: kind, entityType: null, modifier: "direct", kind, object: objText };
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
// has/have/had-changed carve-out (PLAN_CHAT_FEEL item 6 remainder): "has X
|
|
100
|
+
// changed" / "have X touched" / "had X ever been updated" — the present/past-
|
|
101
|
+
// perfect yes/no frame over the SAME touches-family verb the "when" branch below
|
|
102
|
+
// already answers ("when did X change"). Routed BEFORE the general verb-priority
|
|
103
|
+
// scan: "has"/"have"/"had" is ALSO a curated `defines` verb ("this module HAS
|
|
104
|
+
// three functions") and, sitting first in the sentence, would otherwise win that
|
|
105
|
+
// scan outright, misreading "has X changed" as a `defines` reverse-query over
|
|
106
|
+
// the object "X changed" — a confidently-wrong grain, not an honest miss. Gated
|
|
107
|
+
// on the sentence's OWN final word being a genuine touches verb (never a guess
|
|
108
|
+
// at which verb the sentence "really" means) — an ordinary defines question
|
|
109
|
+
// ("has app.mjs three functions") never ends in one, so this can't shadow it.
|
|
110
|
+
const PERFECT_AUX = new Set(["has", "have", "had"]);
|
|
111
|
+
if (PERFECT_AUX.has(lcWords[0])) {
|
|
112
|
+
let end = lcWords.length;
|
|
113
|
+
while (end > 1 && (lcWords[end - 1] === "ever" || lcWords[end - 1] === "been")) end -= 1;
|
|
114
|
+
const tailVerb = end > 1 ? VERB_TO_KIND[lcWords[end - 1]] : null;
|
|
115
|
+
if (tailVerb === "touches") {
|
|
116
|
+
const objText = words.slice(1, end - 1).filter((_, j) => !STOPWORDS.has(lcWords[1 + j])).join(" ").trim();
|
|
117
|
+
if (objText) return { shape: "when", entityType: null, modifier: "direct", kind: "touches", object: objText };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
99
120
|
let canonWords = lcWords;
|
|
100
121
|
let verbHit = findPhrase(lcWords, VERB_TO_KIND);
|
|
101
122
|
if (!verbHit && nlp) {
|
|
@@ -192,6 +213,19 @@ export function parseKeywordSpot(text, nlp = null) {
|
|
|
192
213
|
if (objText) return { shape: "when", entityType: null, modifier: "direct", kind: "touches", object: objText };
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
// who-last shape (HANDOVER.md 2026-07-10 item 5): "who last touched X" / "who
|
|
217
|
+
// touched X last" used to fall into the ordinary reverse-list shape below and
|
|
218
|
+
// list EVERY touching commit's author, ignoring "last" entirely — the mirror
|
|
219
|
+
// gap of the "when" shape just above, which already answers a single newest
|
|
220
|
+
// commit. "who"/"last" are both STOPWORDS (normalize.mjs), so they never
|
|
221
|
+
// survive into beforeText/afterText either way — checked here, before they're
|
|
222
|
+
// stripped, the same way the "when" check above reads `lcWords` directly
|
|
223
|
+
// rather than the post-strip text.
|
|
224
|
+
if (kind === "touches" && lcWords.includes("who") && lcWords.includes("last")) {
|
|
225
|
+
const objText = beforeText || afterText;
|
|
226
|
+
if (objText) return { shape: "whoLast", entityType: null, modifier: "direct", kind: "touches", object: objText };
|
|
227
|
+
}
|
|
228
|
+
|
|
195
229
|
// reversible passive (Cycle 6, archive/PLAN_CYCLE_4.md): "PATIENT is VERBed BY AGENT" — an
|
|
196
230
|
// agent-marking "by" plus a passive auxiliary flips the active reading, so the AGENT
|
|
197
231
|
// (after "by") is the edge SUBJECT and the PATIENT the edge OBJECT. Object-first
|
package/src/memory/blocks.mjs
CHANGED
|
@@ -43,7 +43,7 @@ const INDEX_NAME = "index.json";
|
|
|
43
43
|
|
|
44
44
|
export const PAGERANK_DAMPING = 0.85;
|
|
45
45
|
export const PAGERANK_ITERATIONS = 20;
|
|
46
|
-
const OVERLAP_MIN = 2;
|
|
46
|
+
export const OVERLAP_MIN = 2; // shared tokens for a similarity edge
|
|
47
47
|
const MAX_TOKENS_PER_BLOCK = 800; // beyond tokenizeProse's per-doc cap: union over lines
|
|
48
48
|
|
|
49
49
|
const blocksDir = (dir) => join(dir, BLOCKS_DIR_REL);
|
|
@@ -87,8 +87,13 @@ export async function loadBlockIndex(dir) {
|
|
|
87
87
|
* `tokensById` is a plain { id: tokens[] } map; an undirected edge joins two
|
|
88
88
|
* blocks sharing at least `overlapMin` tokens. Returns { ids, neighbours }
|
|
89
89
|
* (neighbours[i] is an array of adjacent indices into ids).
|
|
90
|
+
*
|
|
91
|
+
* Exported (as of Stage 0 of PLAN_COMPLETIONS.md) so src/completions/group.mjs can build
|
|
92
|
+
* connected components over the same similarity graph rankBlocks/degreeOf already use,
|
|
93
|
+
* rather than re-deriving the shared-token-overlap logic — this IS the clustering
|
|
94
|
+
* primitive this graph was missing; grouping is layered on top of it, not duplicated.
|
|
90
95
|
*/
|
|
91
|
-
function buildNeighbours(tokensById, overlapMin) {
|
|
96
|
+
export function buildNeighbours(tokensById, overlapMin) {
|
|
92
97
|
const ids = Object.keys(tokensById || {});
|
|
93
98
|
const N = ids.length;
|
|
94
99
|
const sets = ids.map((id) => new Set(tokensById[id] || []));
|