@polycode-projects/the-mechanical-code-talker 1.11.6 → 2.0.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 +244 -48
- package/ROADMAP.md +23 -34
- package/bin/tmct.mjs +117 -81
- package/corpus/LICENSES.json +118 -0
- package/corpus/README.md +17 -13
- package/corpus/conceptnet/README.md +5 -5
- package/corpus/conceptnet/fetch-slice.mjs +1 -1
- package/corpus/conceptnet/filter-dump.mjs +1 -1
- package/corpus/generated/README.md +9 -10
- package/corpus/namenet/README.md +39 -0
- package/corpus/seon/README.md +2 -2
- package/corpus/tier2/generate.mjs +58 -9
- package/corpus/tier2/manifest.json +44 -0
- package/corpus/wordnet/README.md +37 -0
- package/corpus/wordnet/generate.mjs +1 -1
- package/data/templates/constructions/agent-noun-relations.toml +2 -2
- package/data/templates/grammar-rules.toml +1 -1
- package/package.json +13 -22
- package/src/{ask-nlp.mjs → adapters/ask-nlp.mjs} +1 -1
- package/src/{config.mjs → adapters/config.mjs} +1 -1
- package/src/{corpus → adapters/corpus}/conceptnet-map.toml +4 -4
- package/src/{corpus → adapters/corpus}/conceptnet.mjs +3 -3
- package/src/adapters/corpus/construction-banks.mjs +43 -0
- package/src/{corpus → adapters/corpus}/templates.mjs +1 -1
- package/src/{embed.mjs → adapters/embed.mjs} +1 -11
- package/src/{graph-build.mjs → adapters/graph-build.mjs} +6 -6
- package/src/{memory → adapters/memory}/blocks.mjs +2 -2
- package/src/{memory → adapters/memory}/core.mjs +38 -94
- package/src/adapters/prose-tokens.mjs +98 -0
- package/src/{providers → adapters/providers}/bootstrap.mjs +2 -2
- package/src/{providers → adapters/providers}/fixture.mjs +3 -3
- package/src/{providers → adapters/providers}/graph-service.mjs +9 -4
- package/src/{source-slice.mjs → adapters/source-slice.mjs} +2 -2
- package/src/{source.mjs → adapters/source.mjs} +1 -1
- package/src/{toml-config.mjs → adapters/toml-config.mjs} +1 -1
- package/src/{answer-variants.json → domain/answer-variants.json} +1 -1
- package/src/domain/answer-variants.mjs +23 -0
- package/src/{ask-vocab.mjs → domain/ask-vocab.mjs} +37 -5
- package/src/{ask.mjs → domain/ask.mjs} +216 -52
- package/src/{codegraph.mjs → domain/codegraph.mjs} +31 -315
- package/src/{completions → domain/completions}/complete.mjs +16 -10
- package/src/{completions → domain/completions}/graph-adapter.mjs +10 -4
- package/src/{completions → domain/completions}/group.mjs +14 -6
- package/src/{completions → domain/completions}/infer.mjs +57 -39
- package/src/domain/completions/injected.mjs +21 -0
- package/src/{completions → domain/completions}/rank.mjs +15 -8
- package/src/{completions → domain/completions}/search.mjs +4 -2
- package/src/{grammar → domain/grammar}/ace.mjs +3 -3
- package/src/{grammar → domain/grammar}/assert.mjs +12 -8
- package/src/{grammar → domain/grammar}/lexicon-core.json +1 -1
- package/src/{grammar → domain/grammar}/lexicon.mjs +4 -6
- package/src/domain/hash.mjs +147 -0
- package/src/{interpret → domain/interpret}/fuzzy.mjs +42 -4
- package/src/domain/interpret/nlp-registry.mjs +20 -0
- package/src/{interpret → domain/interpret}/normalize.mjs +35 -5
- package/src/{interpret → domain/interpret}/pipeline.mjs +1 -5
- package/src/{interpret → domain/interpret}/strategies/ace.mjs +1 -1
- package/src/{interpret → domain/interpret}/strategies/constructions.mjs +30 -52
- package/src/{interpret → domain/interpret}/strategies/keywords.mjs +48 -26
- package/src/domain/memory/capability.mjs +235 -0
- package/src/domain/memory/fold.mjs +54 -0
- package/src/domain/memory/session-turns.mjs +7 -0
- package/src/{memory → domain/memory}/trust.mjs +48 -0
- package/src/{paraphrase.mjs → domain/paraphrase.mjs} +2 -2
- package/src/{prose.mjs → domain/prose.mjs} +1 -1
- package/src/domain/real-word-collisions.json +1 -0
- package/src/{router → domain/router}/call-validator.mjs +1 -1
- package/src/{router → domain/router}/drive.mjs +34 -25
- package/src/{router → domain/router}/goal-reasoner.mjs +1 -1
- package/src/{router → domain/router}/guardrail.mjs +1 -1
- package/src/{router → domain/router}/planner.mjs +1 -1
- package/src/{router → domain/router}/registry.mjs +5 -5
- package/src/{router → domain/router}/resolver.mjs +16 -13
- package/src/{router → domain/router}/results.mjs +1 -1
- package/src/{router → domain/router}/set-algebra.mjs +1 -1
- package/src/{router → domain/router}/taught.mjs +10 -9
- package/src/{syllogise.mjs → domain/syllogise.mjs} +21 -4
- package/src/domain/vector.mjs +12 -0
- package/src/services/chat-session.mjs +451 -0
- package/src/{chat.mjs → services/chat.mjs} +1238 -680
- package/src/{cli-args.mjs → services/cli-args.mjs} +2 -2
- package/src/services/completions.mjs +55 -0
- package/src/{extensions.mjs → services/extensions.mjs} +7 -7
- package/src/{finish.mjs → services/finish.mjs} +2 -2
- package/src/{memory → services}/fold.mjs +0 -0
- package/src/{import-file.mjs → services/import-file.mjs} +3 -3
- package/src/{index.mjs → services/index.mjs} +21 -12
- package/src/{init.mjs → services/init.mjs} +9 -9
- package/src/{ledger-viz.mjs → services/ledger-viz.mjs} +38 -7
- package/src/{plan-viz.mjs → services/plan-viz.mjs} +65 -26
- package/src/{sentences.mjs → services/sentences.mjs} +1 -1
- package/src/{sessions.mjs → services/sessions.mjs} +4 -5
- package/src/{telemetry.mjs → services/telemetry.mjs} +1 -1
- package/src/{server-http.mjs → surfaces/http/server-http.mjs} +11 -65
- package/src/{tui → surfaces/tui}/app.mjs +3 -3
- package/src/{memory-ask-browser-entry.mjs → surfaces/web/memory-ask-browser-entry.mjs} +5 -5
- package/src/{memory-ask-browser.bundle.js → surfaces/web/memory-ask-browser.bundle.js} +9465 -6366
- package/src/tools/catalog.mjs +29 -0
- package/src/{conformance.mjs → tools/conformance.mjs} +2 -2
- package/src/tools/definitions.mjs +288 -0
- package/src/tools/graph-load.mjs +20 -0
- package/src/tools/handlers/index.mjs +54 -0
- package/src/tools/handlers/kit.mjs +33 -0
- package/src/tools/handlers/tmct-architecture.mjs +7 -0
- package/src/tools/handlers/tmct-ask.mjs +14 -0
- package/src/tools/handlers/tmct-callees.mjs +6 -0
- package/src/tools/handlers/tmct-callers.mjs +6 -0
- package/src/tools/handlers/tmct-calls.mjs +6 -0
- package/src/tools/handlers/tmct-class-history.mjs +6 -0
- package/src/tools/handlers/tmct-cochanges.mjs +6 -0
- package/src/tools/handlers/tmct-context-more.mjs +9 -0
- package/src/tools/handlers/tmct-context.mjs +163 -0
- package/src/tools/handlers/tmct-describe.mjs +15 -0
- package/src/tools/handlers/tmct-exports.mjs +9 -0
- package/src/tools/handlers/tmct-file-history.mjs +6 -0
- package/src/tools/handlers/tmct-history.mjs +6 -0
- package/src/tools/handlers/tmct-impact.mjs +9 -0
- package/src/tools/handlers/tmct-members.mjs +16 -0
- package/src/tools/handlers/tmct-method-history.mjs +6 -0
- package/src/tools/handlers/tmct-search.mjs +22 -0
- package/src/tools/handlers/tmct-signature.mjs +6 -0
- package/src/tools/handlers/tmct-snippet.mjs +37 -0
- package/src/tools/handlers/tmct-subclasses.mjs +16 -0
- package/src/tools/handlers/tmct-tests-for.mjs +6 -0
- package/src/tools/handlers/tmct-untested.mjs +7 -0
- package/src/tools/memory-fallthrough.mjs +65 -0
- package/src/{schema-docs.mjs → tools/schema-docs.mjs} +1 -1
- package/src/tools/server.mjs +61 -0
- package/src/answer-variants.mjs +0 -39
- package/src/hash.mjs +0 -24
- package/src/server.mjs +0 -501
- /package/src/{corpus → adapters/corpus}/unknown-ingest.mjs +0 -0
- /package/src/{graph-merge.mjs → adapters/graph-merge.mjs} +0 -0
- /package/src/{memory → adapters/memory}/inspect.mjs +0 -0
- /package/src/{memory → adapters/memory}/shacl.mjs +0 -0
- /package/src/{prose-nlp.mjs → adapters/prose-nlp.mjs} +0 -0
- /package/src/{repository-interface.mjs → adapters/repository-interface.mjs} +0 -0
- /package/src/{uuid.mjs → adapters/uuid.mjs} +0 -0
- /package/src/{wink-model.mjs → adapters/wink-model.mjs} +0 -0
- /package/src/{completions → domain/completions}/prune.mjs +0 -0
- /package/src/{concept.mjs → domain/concept.mjs} +0 -0
- /package/src/{domain.mjs → domain/domain.mjs} +0 -0
- /package/src/{interpret → domain/interpret}/merge.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/grammar.mjs +0 -0
- /package/src/{interpret → domain/interpret}/strategies/noise-strip.mjs +0 -0
- /package/src/{memory → domain/memory}/bias.mjs +0 -0
- /package/src/{planning.mjs → domain/planning.mjs} +0 -0
- /package/src/{viz-theme.mjs → services/viz-theme.mjs} +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { lookupByProseTokens, proseLayerHits, splitIdentifierWords } from "./prose.mjs";
|
|
2
|
-
import { cosine } from "./
|
|
3
|
-
import { CREATED_AT_PROP, UPDATED_AT_PROP
|
|
2
|
+
import { cosine } from "./vector.mjs";
|
|
3
|
+
import { CREATED_AT_PROP, UPDATED_AT_PROP } from "./memory/trust.mjs";
|
|
4
4
|
|
|
5
5
|
// Pure (no-network, no-fs) query logic over the typed `entities` payload that the
|
|
6
6
|
// deterministic indexer writes to <repo>/.tmct/graph.json (shape produced by
|
|
7
|
-
// src/graph-build.mjs):
|
|
7
|
+
// src/adapters/graph-build.mjs):
|
|
8
8
|
//
|
|
9
9
|
// {
|
|
10
10
|
// generated_at, classes: [{name, count, sample[]}],
|
|
@@ -47,6 +47,14 @@ export function parseEntities(payload) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Code entities (Modules) in the loaded graph — the "is there a code graph here"
|
|
51
|
+
* test. 0 means a graph-less bootstrap OR a graph.json with no code entities (the
|
|
52
|
+
* degenerate trap); both orient rather than over-promise. */
|
|
53
|
+
export function moduleCountOf(graph) {
|
|
54
|
+
if (!graph || !Array.isArray(graph.individuals)) return 0;
|
|
55
|
+
return graph.individuals.filter((i) => (i.class || "") === "Module").length;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
// ---- relation-kind classifier (for impact + tests-coverage) -------------------
|
|
51
59
|
|
|
52
60
|
const KINDS = ["imports", "calls", "defines", "tests", "touches", "contains", "inherits", "callsSymbol", "touchesSymbol"];
|
|
@@ -83,16 +91,11 @@ const PROP_KIND = {
|
|
|
83
91
|
"mgx:inreplyto": "inReplyTo",
|
|
84
92
|
"mgx:statedby": "statedBy",
|
|
85
93
|
"mgx:canonicalisedfrom": "canonicalisedFrom",
|
|
86
|
-
// structural links deriveFactTermGraph synthesizes on every Fact (Fact -> its own subject/object Term)
|
|
87
|
-
"mgx:factsubjectterm": "factSubjectTerm",
|
|
88
|
-
"mgx:factobjectterm": "factObjectTerm",
|
|
89
94
|
};
|
|
90
95
|
|
|
91
96
|
export function relationKind(group) {
|
|
92
97
|
const prop = String(group?.prop || "").toLowerCase();
|
|
93
98
|
if (PROP_KIND[prop]) return PROP_KIND[prop];
|
|
94
|
-
// deriveFactTermGraph namespaces taught predicates as "factrel:<predicate>"
|
|
95
|
-
if (prop.startsWith("factrel:")) return group.predicate || null;
|
|
96
99
|
const pred = String(group?.predicate || "").toLowerCase();
|
|
97
100
|
// symbol-granular fallbacks first, so a near-miss still classifies fine-grained
|
|
98
101
|
if (/symbol/.test(pred)) {
|
|
@@ -536,9 +539,6 @@ const SPIRAL_DEPTH_DEFAULT = 3;
|
|
|
536
539
|
const SPIRAL_NODE_LIMIT_DEFAULT = 12;
|
|
537
540
|
const SPIRAL_Q_DEFAULT = 0.9; // keep only the least-connected 90% at each step
|
|
538
541
|
const SPIRAL_EXPAND_KINDS = ["imports", "calls", "callsSymbol", "inherits"]; // cochange dropped
|
|
539
|
-
// The memory graph's edge-kind inventory a memory-graph spiralExpand call walks
|
|
540
|
-
// instead of code-graph Modules. mgx:asksAbout is excluded — it lives in the code graph.
|
|
541
|
-
export const MEMORY_SPIRAL_EXPAND_KINDS = ["saidInSession", "inReplyTo", "statedBy", "canonicalisedFrom"];
|
|
542
542
|
const SPIRAL_EMIT_FRAC = 0.5;
|
|
543
543
|
const SPIRAL_HOP_DECAY = 0.6;
|
|
544
544
|
const SPIRAL_PROX_FRAC = 0.2;
|
|
@@ -769,19 +769,6 @@ export function spiralExpand(graph, scored = [], {
|
|
|
769
769
|
return results;
|
|
770
770
|
}
|
|
771
771
|
|
|
772
|
-
/** The individual with the most recent `createdAtProp` attribute (ties break
|
|
773
|
-
* on lowest id); null if none carry the attribute. ISO-8601 timestamps
|
|
774
|
-
* compare correctly as plain strings, so no Date parsing is needed. */
|
|
775
|
-
export function mostRecentIndividual(graph, createdAtProp = CREATED_AT_PROP) {
|
|
776
|
-
let best = null; // { ind, v }
|
|
777
|
-
for (const ind of graph?.individuals || []) {
|
|
778
|
-
const v = (ind?.attributes || []).find((a) => a?.prop === createdAtProp)?.value;
|
|
779
|
-
if (!v) continue;
|
|
780
|
-
if (!best || v > best.v || (v === best.v && String(ind.id) < String(best.ind.id))) best = { ind, v };
|
|
781
|
-
}
|
|
782
|
-
return best ? best.ind : null;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
772
|
/** Shared module-ranking core behind renderSearch and searchModulesRanked.
|
|
786
773
|
* IDF-weights each query token, scores path/symbol/exact-symbol matches, and
|
|
787
774
|
* re-ranks with a bounded import-proximity bonus. Pure; deterministic. */
|
|
@@ -1117,247 +1104,6 @@ export function edgesOfKind(graph, kind) {
|
|
|
1117
1104
|
return out;
|
|
1118
1105
|
}
|
|
1119
1106
|
|
|
1120
|
-
/** A node's "last touched" moment: its own updatedAt/createdAt attribute, or
|
|
1121
|
-
* the max `createdAt` over every edge touching it, whichever is newer. ""
|
|
1122
|
-
* when nothing carries a timestamp. Skips edges with no `createdAt` rather than throwing. */
|
|
1123
|
-
export function derivedUpdatedAt(graph, ind, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1124
|
-
if (!ind) return "";
|
|
1125
|
-
const attrs = ind.attributes || [];
|
|
1126
|
-
const own = attrs.find((a) => a?.prop === updatedAtProp)?.value || attrs.find((a) => a?.prop === createdAtProp)?.value || "";
|
|
1127
|
-
let best = own || "";
|
|
1128
|
-
for (const g of graph?.relations || []) {
|
|
1129
|
-
for (const e of g.edges || []) {
|
|
1130
|
-
if (!e || (e.subject !== ind.id && e.object !== ind.id)) continue;
|
|
1131
|
-
const c = e.createdAt;
|
|
1132
|
-
if (!c) continue; // no timestamp on this edge — skip, never throw
|
|
1133
|
-
if (!best || c > best) best = c;
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
return best;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/** Turn a `spiralExpand` walk into the `{nodes, edges}` shape `tmct viz`
|
|
1140
|
-
* renders, shared between the CLI and the browser bundle's client-side
|
|
1141
|
-
* re-walk. `edges` includes any relation-group edge connecting two walked
|
|
1142
|
-
* nodes, not just kinds the walk itself traversed, de-duped on (subject, object, predicate). */
|
|
1143
|
-
export function buildVizNodesAndEdges(graph, walked, { createdAtProp = CREATED_AT_PROP, updatedAtProp = UPDATED_AT_PROP } = {}) {
|
|
1144
|
-
const nodeIds = new Set(walked.map((w) => w.id));
|
|
1145
|
-
const nodes = walked.map(({ id, hop }) => {
|
|
1146
|
-
const ind = graph.byId.get(id) || null;
|
|
1147
|
-
const attrs = ind?.attributes || [];
|
|
1148
|
-
const createdAt = attrs.find((a) => a?.prop === createdAtProp)?.value || "";
|
|
1149
|
-
return {
|
|
1150
|
-
id, hop, label: ind?.label || id, class: ind?.class || "", createdAt,
|
|
1151
|
-
updatedAt: derivedUpdatedAt(graph, ind, { createdAtProp, updatedAtProp }),
|
|
1152
|
-
};
|
|
1153
|
-
});
|
|
1154
|
-
const edges = [];
|
|
1155
|
-
const seen = new Set();
|
|
1156
|
-
for (const group of graph.relations || []) {
|
|
1157
|
-
for (const e of group.edges || []) {
|
|
1158
|
-
if (!nodeIds.has(e.subject) || !nodeIds.has(e.object)) continue;
|
|
1159
|
-
const key = `${e.subject} ${e.object} ${group.predicate}`;
|
|
1160
|
-
if (seen.has(key)) continue;
|
|
1161
|
-
seen.add(key);
|
|
1162
|
-
edges.push({ source: e.subject, target: e.object, kind: group.predicate });
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
return { nodes, edges };
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
const MEMORY_FACT_CLASS = "Fact";
|
|
1169
|
-
const MEMORY_TERM_CLASS = "Term";
|
|
1170
|
-
|
|
1171
|
-
/** Derive a term-relation view of a memory graph's reified Facts (never
|
|
1172
|
-
* mutates `graph`, viz-only). A Fact stores subject/predicate/object as
|
|
1173
|
-
* plain string attributes, so the concept structure is invisible to a walk
|
|
1174
|
-
* over `graph.relations` as-is; this synthesizes one Term individual per
|
|
1175
|
-
* distinct subject/object string, one relation group per distinct fact
|
|
1176
|
-
* predicate, and two fixed Fact->Term link groups so a walk seeded on a Fact
|
|
1177
|
-
* can reach the term graph at all. Returns `{ graph: <augmented graph>,
|
|
1178
|
-
* factRelationKinds: [<predicate>, …] }`; a graph with no Facts is a no-op. */
|
|
1179
|
-
export function deriveFactTermGraph(graph) {
|
|
1180
|
-
const termById = new Map(); // term:<t> -> individual
|
|
1181
|
-
const groupByPredicate = new Map(); // predicate -> relation group
|
|
1182
|
-
const subjectLinks = []; // Fact -> its subject Term
|
|
1183
|
-
const objectLinks = []; // Fact -> its object Term
|
|
1184
|
-
const termId = (t) => `term:${t}`;
|
|
1185
|
-
const ensureTerm = (t) => {
|
|
1186
|
-
const id = termId(t);
|
|
1187
|
-
if (!termById.has(id)) termById.set(id, { id, label: t, class: MEMORY_TERM_CLASS, attributes: [] });
|
|
1188
|
-
return id;
|
|
1189
|
-
};
|
|
1190
|
-
|
|
1191
|
-
for (const ind of graph?.individuals || []) {
|
|
1192
|
-
if ((ind?.class || "") !== MEMORY_FACT_CLASS) continue;
|
|
1193
|
-
const attrs = ind.attributes || [];
|
|
1194
|
-
const s = attrs.find((a) => a?.prop === "rdf:subject")?.value;
|
|
1195
|
-
const p = attrs.find((a) => a?.prop === "rdf:predicate")?.value;
|
|
1196
|
-
const o = attrs.find((a) => a?.prop === "rdf:object")?.value;
|
|
1197
|
-
if (!s || !p || !o) continue; // a malformed/legacy Fact — skip, never throw
|
|
1198
|
-
const subjectTermId = ensureTerm(s);
|
|
1199
|
-
const objectTermId = ensureTerm(o);
|
|
1200
|
-
let group = groupByPredicate.get(p);
|
|
1201
|
-
if (!group) {
|
|
1202
|
-
group = { predicate: p, prop: `factrel:${p}`, count: 0, edges: [] };
|
|
1203
|
-
groupByPredicate.set(p, group);
|
|
1204
|
-
}
|
|
1205
|
-
group.edges.push({ subject: subjectTermId, object: objectTermId, subjectLabel: s, objectLabel: o });
|
|
1206
|
-
group.count = group.edges.length;
|
|
1207
|
-
subjectLinks.push({ subject: ind.id, object: subjectTermId, subjectLabel: ind.label, objectLabel: s });
|
|
1208
|
-
objectLinks.push({ subject: ind.id, object: objectTermId, subjectLabel: ind.label, objectLabel: o });
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (!termById.size) return { graph, factRelationKinds: [] };
|
|
1212
|
-
|
|
1213
|
-
const individuals = [...(graph.individuals || []), ...termById.values()];
|
|
1214
|
-
const byId = new Map(graph.byId);
|
|
1215
|
-
for (const term of termById.values()) byId.set(term.id, term);
|
|
1216
|
-
const relations = [
|
|
1217
|
-
...(graph.relations || []),
|
|
1218
|
-
...groupByPredicate.values(),
|
|
1219
|
-
{ predicate: "factSubjectTerm", prop: "mgx:factSubjectTerm", count: subjectLinks.length, edges: subjectLinks },
|
|
1220
|
-
{ predicate: "factObjectTerm", prop: "mgx:factObjectTerm", count: objectLinks.length, edges: objectLinks },
|
|
1221
|
-
];
|
|
1222
|
-
|
|
1223
|
-
return {
|
|
1224
|
-
graph: { ...graph, individuals, byId, relations },
|
|
1225
|
-
factRelationKinds: [...groupByPredicate.keys()],
|
|
1226
|
-
};
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
/** The two fixed structural link kinds `deriveFactTermGraph` always emits.
|
|
1230
|
-
* Bundled into the "relation" (concept) walk, not "meta" (provenance), so a
|
|
1231
|
-
* user toggling to meta-only still gets the provenance-only view. */
|
|
1232
|
-
export const MEMORY_FACT_LINK_KINDS = ["factSubjectTerm", "factObjectTerm"];
|
|
1233
|
-
|
|
1234
|
-
/** The combined kinds list a memory-graph walk uses for a given edge-kind
|
|
1235
|
-
* mode: "meta" (provenance-only), "relation" (concept view), or "both"
|
|
1236
|
-
* (default). Lives here, not viz.mjs, so the CLI and the browser bundle's
|
|
1237
|
-
* client-side re-walk share the same function (viz.mjs does real fs I/O and
|
|
1238
|
-
* can't be bundled for the browser). */
|
|
1239
|
-
export function edgeKindsFor(mode, factRelationKinds) {
|
|
1240
|
-
const relationKinds = [...factRelationKinds, ...MEMORY_FACT_LINK_KINDS];
|
|
1241
|
-
if (mode === "meta") return [...MEMORY_SPIRAL_EXPAND_KINDS];
|
|
1242
|
-
if (mode === "relation") return relationKinds;
|
|
1243
|
-
return [...MEMORY_SPIRAL_EXPAND_KINDS, ...relationKinds]; // "both" (default)
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const LEGEND_MAX_BUCKETS = 20; // too many chips to be usable
|
|
1247
|
-
const LEGEND_MIN_BUCKETS = 2; // nothing to filter with only one bucket
|
|
1248
|
-
const LEGEND_COLLAPSE_TOP_N = 15; // "top 15 by count, rest grouped as Other" — stays under the max
|
|
1249
|
-
|
|
1250
|
-
/** Normalized Shannon entropy (`H / log2(k)`, `k` = bucket count) of a
|
|
1251
|
-
* {value, count} bucket list — 1.0 for a perfectly even split, ~0 for one
|
|
1252
|
-
* dominant bucket swallowing everything, undefined (returns 0) for k<2. */
|
|
1253
|
-
function normalizedEntropy(buckets) {
|
|
1254
|
-
const k = buckets.length;
|
|
1255
|
-
if (k < 2) return 0;
|
|
1256
|
-
const total = buckets.reduce((sum, b) => sum + b.count, 0);
|
|
1257
|
-
if (!total) return 0;
|
|
1258
|
-
let h = 0;
|
|
1259
|
-
for (const b of buckets) {
|
|
1260
|
-
if (!b.count) continue;
|
|
1261
|
-
const p = b.count / total;
|
|
1262
|
-
h -= p * Math.log2(p);
|
|
1263
|
-
}
|
|
1264
|
-
return h / Math.log2(k);
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
/** Collapse a raw {value,count} bucket list down to at most LEGEND_MAX_BUCKETS
|
|
1268
|
-
* entries: keep the top LEGEND_COLLAPSE_TOP_N by count, fold the rest into a
|
|
1269
|
-
* single "Other" bucket, applied generically (not predicate-only) so
|
|
1270
|
-
* any dimension that happens to be high-cardinality degrades the same way.
|
|
1271
|
-
* A no-op (returns `buckets` unchanged, same array) when already <= the cap.
|
|
1272
|
-
* Exported (not just used internally by pickLegendDimension) so the browser
|
|
1273
|
-
* bundle's client-side legend (live per-view recomputation, viz.mjs's own
|
|
1274
|
-
* computeLegendBuckets) collapses high-cardinality dimensions the SAME way,
|
|
1275
|
-
* never a second hand-rolled copy that could drift. */
|
|
1276
|
-
export function collapseToTopN(buckets) {
|
|
1277
|
-
if (buckets.length <= LEGEND_MAX_BUCKETS) return buckets;
|
|
1278
|
-
const sorted = [...buckets].sort((a, b) => b.count - a.count || (a.value < b.value ? -1 : 1));
|
|
1279
|
-
const kept = sorted.slice(0, LEGEND_COLLAPSE_TOP_N);
|
|
1280
|
-
const restCount = sorted.slice(LEGEND_COLLAPSE_TOP_N).reduce((sum, b) => sum + b.count, 0);
|
|
1281
|
-
return restCount ? [...kept, { value: "Other", count: restCount }] : kept;
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function bucketCounts(values) {
|
|
1285
|
-
const counts = new Map();
|
|
1286
|
-
for (const v of values) {
|
|
1287
|
-
if (v == null || v === "") continue;
|
|
1288
|
-
counts.set(v, (counts.get(v) || 0) + 1);
|
|
1289
|
-
}
|
|
1290
|
-
return [...counts.entries()].map(([value, count]) => ({ value, count }));
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
/** The normalized provenance-prefix label for a Fact's FIRST recorded
|
|
1294
|
-
* provenance tag (a Fact may carry a " | "-joined union of several; the
|
|
1295
|
-
* legend buckets on the primary/first-recorded one, not a multiset) —
|
|
1296
|
-
* reuses memory/core.mjs's own provenanceTagToSource parser (the SAME
|
|
1297
|
-
* collapse-the-session-id/timestamp-suffix, keep-the-corpus/source-name
|
|
1298
|
-
* logic the trust layer already relies on) rather than re-deriving it.
|
|
1299
|
-
* Null when the Fact carries no provenance tag at all (a legacy/malformed
|
|
1300
|
-
* row) or the tag doesn't parse to a known Source kind. */
|
|
1301
|
-
function provenanceBucketLabel(rawTag) {
|
|
1302
|
-
const tag = String(rawTag || "").split(" | ")[0].trim();
|
|
1303
|
-
if (!tag) return null;
|
|
1304
|
-
const src = provenanceTagToSource(tag);
|
|
1305
|
-
if (!src) return null;
|
|
1306
|
-
if (src.kind === "corpus" || src.kind === "corpusWeak") {
|
|
1307
|
-
return `${src.kind === "corpusWeak" ? "corpus-weak" : "corpus"}:${src.name || "unknown"}`;
|
|
1308
|
-
}
|
|
1309
|
-
if (src.kind === "extracted") return `extracted:${src.name || "unknown"}`;
|
|
1310
|
-
if (src.kind === "entailed") return `entailed:${src.rule || "unknown"}`;
|
|
1311
|
-
if (src.kind === "operator") return "ace:chat";
|
|
1312
|
-
if (src.kind === "teach") return "teach:chat";
|
|
1313
|
-
if (src.kind === "web") return "web";
|
|
1314
|
-
return src.kind;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
/** A single walked node's bucket value under one legend dimension. `"class"`
|
|
1318
|
-
* reads every node; `"predicate"`/`"provenance"` only return non-null for a
|
|
1319
|
-
* Fact-class node. */
|
|
1320
|
-
export function legendValueFor(graph, node, dimension) {
|
|
1321
|
-
if (dimension === "class") return node?.class || "(none)";
|
|
1322
|
-
if (!node || node.class !== MEMORY_FACT_CLASS) return null;
|
|
1323
|
-
const attrs = graph?.byId?.get?.(node.id)?.attributes || [];
|
|
1324
|
-
if (dimension === "predicate") return attrs.find((a) => a?.prop === "rdf:predicate")?.value || null;
|
|
1325
|
-
if (dimension === "provenance") return provenanceBucketLabel(attrs.find((a) => a?.prop === "mgx:factProvenance")?.value);
|
|
1326
|
-
return null;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
/** Auto-pick the filter/legend dimension: since Fact dominates class-based
|
|
1330
|
-
* legends once real memory-graph data is seeded, this scores class/predicate/
|
|
1331
|
-
* provenance by normalized Shannon entropy over their bucket distribution
|
|
1332
|
-
* and picks the best-scoring qualifying one (LEGEND_MIN_BUCKETS..MAX_BUCKETS).
|
|
1333
|
-
* Falls back to "class" when nothing qualifies. Returns `{ primary,
|
|
1334
|
-
* dimensions: { class, predicate, provenance } }`. */
|
|
1335
|
-
export function pickLegendDimension(graph, nodes) {
|
|
1336
|
-
const classBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "class")));
|
|
1337
|
-
const predicateBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "predicate")));
|
|
1338
|
-
const provenanceBuckets = bucketCounts((nodes || []).map((n) => legendValueFor(graph, n, "provenance")));
|
|
1339
|
-
|
|
1340
|
-
const score = (rawBuckets) => {
|
|
1341
|
-
const buckets = collapseToTopN(rawBuckets);
|
|
1342
|
-
const qualifies = buckets.length >= LEGEND_MIN_BUCKETS && buckets.length <= LEGEND_MAX_BUCKETS;
|
|
1343
|
-
return { score: normalizedEntropy(buckets), qualifies, buckets };
|
|
1344
|
-
};
|
|
1345
|
-
|
|
1346
|
-
const dimensions = {
|
|
1347
|
-
class: score(classBuckets),
|
|
1348
|
-
predicate: score(predicateBuckets),
|
|
1349
|
-
provenance: score(provenanceBuckets),
|
|
1350
|
-
};
|
|
1351
|
-
|
|
1352
|
-
let primary = "class";
|
|
1353
|
-
let bestScore = -1;
|
|
1354
|
-
for (const [name, d] of Object.entries(dimensions)) {
|
|
1355
|
-
if (!d.qualifies) continue;
|
|
1356
|
-
if (d.score > bestScore) { bestScore = d.score; primary = name; }
|
|
1357
|
-
}
|
|
1358
|
-
return { primary, dimensions };
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
1107
|
/** moduleIdOf by raw edge-endpoint id: resolves through byId when the individual exists,
|
|
1362
1108
|
* else falls back to parsing an `fn:<path>#name` id directly (callsSymbol objects may name
|
|
1363
1109
|
* symbols with no individual of their own). Null if it cannot be mapped. */
|
|
@@ -1528,16 +1274,32 @@ export function renderArchitecture(graph, { pkg = "" } = {}) {
|
|
|
1528
1274
|
}
|
|
1529
1275
|
|
|
1530
1276
|
const COVERAGE_CAP = 40;
|
|
1277
|
+
const TESTS_GRAIN_NOTE = "tests edges are recorded module to module, so this is module-grain coverage.";
|
|
1531
1278
|
|
|
1532
1279
|
/** The test modules covering a symbol/module — from the `tests` (mgx:testsCoverage)
|
|
1533
|
-
* relation. Replaces grepping `tests/` for who imports the target.
|
|
1280
|
+
* relation. Replaces grepping `tests/` for who imports the target.
|
|
1281
|
+
*
|
|
1282
|
+
* `tests` edges run module to module, so a FUNCTION has no coverage of its
|
|
1283
|
+
* own to report and the answer is its defining module's. Asked about a
|
|
1284
|
+
* function, say both: which module the question hopped to, and that the
|
|
1285
|
+
* coverage is module-grain. Without that the answer names a module the user
|
|
1286
|
+
* never mentioned and silently passes off module coverage as the function's.
|
|
1287
|
+
* The grain note sits outside the indented list on purpose — indented, it
|
|
1288
|
+
* reads as one more test module. */
|
|
1534
1289
|
export function renderTestsFor(graph, ind) {
|
|
1535
1290
|
const modId = moduleIdOf(graph, ind);
|
|
1536
1291
|
if (!modId) return `cannot map ${ind.label} to a module.`;
|
|
1537
|
-
|
|
1292
|
+
// A symbol's module id is derived from its own recorded site, so it can name
|
|
1293
|
+
// a module the index doesn't carry an individual for. The site's path is
|
|
1294
|
+
// then the only honest spelling of it — never the raw id.
|
|
1295
|
+
const modLabel = graph.byId.get(modId)?.label || siteOf(ind)?.path || modId;
|
|
1538
1296
|
const tests = [...new Set(edgesOfKind(graph, "tests").filter((e) => e.object === modId).map((e) => e.subjectLabel || e.subject))];
|
|
1539
|
-
|
|
1540
|
-
|
|
1297
|
+
const covered = tests.length > 0;
|
|
1298
|
+
const verdict = covered
|
|
1299
|
+
? `covered by ${tests.length} test module(s):\n ${capJoin(tests, COVERAGE_CAP, "\n ")}`
|
|
1300
|
+
: "no covering tests recorded (no test module imports it).";
|
|
1301
|
+
if (modId === ind.id) return `${modLabel}: ${verdict}`;
|
|
1302
|
+
return `${ind.label} is defined in ${modLabel}, which ${covered ? "is" : "has"} ${verdict}\n${TESTS_GRAIN_NOTE}`;
|
|
1541
1303
|
}
|
|
1542
1304
|
|
|
1543
1305
|
/** Source modules with no covering test module — a coverage gap view. Test
|
|
@@ -2217,52 +1979,6 @@ export function renderGraphOnlyBundle(plan, mask) {
|
|
|
2217
1979
|
return out.join("\n");
|
|
2218
1980
|
}
|
|
2219
1981
|
|
|
2220
|
-
// ---- cold-tool catalog (written to <repo>/.tmct/TOOLS.md by the index step) -----
|
|
2221
|
-
|
|
2222
|
-
/** Markdown catalog of the COLD tools (everything except the hot catalog tools): each
|
|
2223
|
-
* with a one-line purpose and the exact Bash invocation via the CLI `cli <tool>` route.
|
|
2224
|
-
* Pure — `cliPath` is the absolute path to bin/cli.mjs the caller wants embedded. */
|
|
2225
|
-
export function renderToolsCatalog(cliPath) {
|
|
2226
|
-
const cold = [
|
|
2227
|
-
["tmct_describe", "Locate one symbol and list its typed edges (both directions) with provenance.", { symbol: "django/utils/text.py" }],
|
|
2228
|
-
["tmct_signature", "One symbol's API surface (params, returns, raises/catches, flags, decorators, doc) without the body.", { symbol: "Truncator.chars" }],
|
|
2229
|
-
["tmct_impact", "Transitive reverse closure over imports/calls — what breaks if a module changes, by depth, with tests.", { module: "django/utils/text.py" }],
|
|
2230
|
-
["tmct_search", "Free-text/ranked lookup over the code-map to find the right module or symbol.", { query: "template filters", kind: "function" }],
|
|
2231
|
-
["tmct_members", "A class's methods + attributes (file:line, decorators) in one slice.", { class: "Truncator" }],
|
|
2232
|
-
["tmct_subclasses", "A class's base classes plus the transitive set of classes that extend it.", { class: "Field" }],
|
|
2233
|
-
["tmct_architecture", "Package/module map + the most-imported hub modules (optionally scoped to a package).", { package: "django/template" }],
|
|
2234
|
-
["tmct_exports", "A module's public __all__ surface, each name resolved to the module that defines it.", { module: "django/db/models/__init__.py" }],
|
|
2235
|
-
["tmct_tests_for", "The test modules covering a symbol or module, from the typed test edges.", { symbol: "django/utils/text.py" }],
|
|
2236
|
-
["tmct_untested", "Source modules with no covering test module — a coverage-gap view (no arguments).", {}],
|
|
2237
|
-
["tmct_history", "Recent commits that touched a symbol's module (newest first).", { symbol: "django/utils/text.py" }],
|
|
2238
|
-
["tmct_file_history", "Commits that touched a symbol's module, each with author / date / subject.", { symbol: "django/utils/text.py" }],
|
|
2239
|
-
["tmct_method_history", "Commits that touched a specific method symbol (fine-grained), with author / date / subject.", { symbol: "Truncator.chars" }],
|
|
2240
|
-
["tmct_class_history", "Commits that touched a specific class symbol (fine-grained), with author / date / subject.", { symbol: "Truncator" }],
|
|
2241
|
-
["tmct_callers", "Modules that call into a symbol's module (one hop).", { symbol: "django/utils/text.py" }],
|
|
2242
|
-
["tmct_callees", "Modules a symbol's module calls into (one hop).", { symbol: "django/utils/text.py" }],
|
|
2243
|
-
["tmct_calls", "The in-repo symbols a function calls (fn→fn), each with file:line.", { symbol: "slugify" }],
|
|
2244
|
-
["tmct_cochanges", "Modules that historically change in the same commit as a symbol's module (git co-change).", { symbol: "django/utils/text.py" }],
|
|
2245
|
-
["tmct_context_more", "The bundle sections a lean tmct_context omitted (siblings / tests / cochange / class members / re-exports).", { symbol: "django/utils/text.py" }],
|
|
2246
|
-
];
|
|
2247
|
-
const lines = [
|
|
2248
|
-
"# tmct cold-tool catalog",
|
|
2249
|
-
"",
|
|
2250
|
-
"The hot tools — `tmct_context` (start here to add/modify code; supports `depth: min|auto|full`) and `tmct_snippet` (exact source of one symbol) — carry full schemas in the TOOLS catalog.",
|
|
2251
|
-
"",
|
|
2252
|
-
"The cold tools below invoke via the CLI:",
|
|
2253
|
-
"",
|
|
2254
|
-
];
|
|
2255
|
-
for (const [name, purpose, args] of cold) {
|
|
2256
|
-
lines.push(`## ${name}`);
|
|
2257
|
-
lines.push(purpose);
|
|
2258
|
-
lines.push("```bash");
|
|
2259
|
-
lines.push(`node ${cliPath} cli ${name} '${JSON.stringify(args)}'`);
|
|
2260
|
-
lines.push("```");
|
|
2261
|
-
lines.push("");
|
|
2262
|
-
}
|
|
2263
|
-
return lines.join("\n");
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
1982
|
// ---- change-coupling (git co-change) — "what usually changes together" ----------
|
|
2267
1983
|
|
|
2268
1984
|
/** [{label, weight}] modules co-changed with modId, sorted by count desc. Pure. */
|
|
@@ -7,13 +7,12 @@ import { groupHits } from "./group.mjs";
|
|
|
7
7
|
import { rankSentences } from "./rank.mjs";
|
|
8
8
|
import { inferRelations } from "./infer.mjs";
|
|
9
9
|
import { pruneCompletion } from "./prune.mjs";
|
|
10
|
-
import {
|
|
11
|
-
import { finish, grammarRules } from "../finish.mjs";
|
|
10
|
+
import { requireInjected } from "./injected.mjs";
|
|
12
11
|
|
|
13
12
|
/** grammarRules() with sentence-capitalisation force-enabled (disabled in live chat only to
|
|
14
13
|
* protect single-answer lowercase-opener goldens, which don't apply to a multi-sentence
|
|
15
14
|
* completion). */
|
|
16
|
-
function completionGrammarRules() {
|
|
15
|
+
function completionGrammarRules(grammarRules) {
|
|
17
16
|
return grammarRules().map((r) => (r.id === "sentence-capitalisation" ? { ...r, enabled: true } : r));
|
|
18
17
|
}
|
|
19
18
|
|
|
@@ -36,8 +35,11 @@ const DEFAULT_MAX_SENTENCES_PER_GROUP = 3; // see prune.mjs's own file header fo
|
|
|
36
35
|
* @param {string} [opts.query] the query rankSentences()/pruning focus on; defaults to `prompt`;
|
|
37
36
|
* pass `null` for self-weighted (LexRank-style) ranking instead
|
|
38
37
|
* @param {number} [opts.maxSentencesPerGroup=3] prune.mjs's top-K-per-group cutoff
|
|
39
|
-
* @param {object} [opts.graph] optional loaded graph (src/codegraph.mjs parseEntities() shape)
|
|
38
|
+
* @param {object} [opts.graph] optional loaded graph (src/domain/codegraph.mjs parseEntities() shape)
|
|
40
39
|
* handed to finish()'s maskSegments to protect known entity labels during the grammar pass
|
|
40
|
+
* @param {object} opts.store REQUIRED — the memory/block store handles this pipeline reads
|
|
41
|
+
* through; `loadMemory` here, plus whatever each stage below requires of its own
|
|
42
|
+
* @param {object} opts.finisher REQUIRED — the prose finisher's `{ finish, grammarRules }`
|
|
41
43
|
* @returns {Promise<{
|
|
42
44
|
* text: string,
|
|
43
45
|
* sourceSpans: Array<{sourceBlockId:string, groupId:string, sentence:string}>,
|
|
@@ -51,22 +53,26 @@ export async function generateCompletion(dir, prompt, opts = {}) {
|
|
|
51
53
|
const {
|
|
52
54
|
blockK, graphService = null, graphLimit, overlapMin,
|
|
53
55
|
memory: memoryOpt, query = prompt, maxSentencesPerGroup = DEFAULT_MAX_SENTENCES_PER_GROUP,
|
|
54
|
-
graph,
|
|
56
|
+
graph, store, finisher,
|
|
55
57
|
} = opts;
|
|
58
|
+
const { loadMemory } = requireInjected(store, ["loadMemory"], { caller: "generateCompletion", option: "store" });
|
|
59
|
+
const { finish, grammarRules } = requireInjected(
|
|
60
|
+
finisher, ["finish", "grammarRules"], { caller: "generateCompletion", option: "finisher" },
|
|
61
|
+
);
|
|
56
62
|
|
|
57
63
|
// Stage 1 — broad search
|
|
58
|
-
const hits = await broadSearch(dir, prompt, { blockK, graphService, graphLimit });
|
|
64
|
+
const hits = await broadSearch(dir, prompt, { blockK, graphService, graphLimit, store });
|
|
59
65
|
|
|
60
66
|
// Stage 2 — grouping
|
|
61
|
-
const groups = groupHits(hits, { overlapMin });
|
|
67
|
+
const groups = groupHits(hits, { overlapMin, store });
|
|
62
68
|
|
|
63
69
|
// Stage 3 — cross-group inference
|
|
64
70
|
const memory = memoryOpt || await loadMemory(dir);
|
|
65
|
-
const relations = groups.length >= 2 ? await inferRelations(groups, memory) : [];
|
|
71
|
+
const relations = groups.length >= 2 ? await inferRelations(groups, memory, { store }) : [];
|
|
66
72
|
|
|
67
73
|
// Stage 4 — extractive sentence ranking, per group (query-focused unless the caller opted out)
|
|
68
74
|
const rankedByGroup = {};
|
|
69
|
-
for (const g of groups) rankedByGroup[g.id] = rankSentences(g, { query });
|
|
75
|
+
for (const g of groups) rankedByGroup[g.id] = rankSentences(g, { query, store });
|
|
70
76
|
|
|
71
77
|
// Stage 5 — pruning: decide keep/drop, with an itemized, auditable drop log
|
|
72
78
|
const { kept, dropped } = pruneCompletion(
|
|
@@ -84,7 +90,7 @@ export async function generateCompletion(dir, prompt, opts = {}) {
|
|
|
84
90
|
const rawText = kept.map((k) => k.sentence).join(" ");
|
|
85
91
|
|
|
86
92
|
// Stage 6 — grammar/voice pass (reuses finish() verbatim; see completionGrammarRules() above).
|
|
87
|
-
const finished = finish({ answer: rawText, via: "completion" }, { graph, rules: completionGrammarRules() });
|
|
93
|
+
const finished = finish({ answer: rawText, via: "completion" }, { graph, rules: completionGrammarRules(grammarRules) });
|
|
88
94
|
|
|
89
95
|
// sourceSpans traces every output sentence back to its block id + group id, from `kept`
|
|
90
96
|
// (pre-grammar-pass) — stays index-aligned since finish() only mutates casing/punctuation.
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// renderDescribe and readFactRows(memory), falling back to svc.ask() only when it genuinely
|
|
8
8
|
// parsed the term.
|
|
9
9
|
|
|
10
|
-
import { createGraphService } from "../providers/graph-service.mjs";
|
|
11
10
|
import { resolveSymbol, renderDescribe } from "../codegraph.mjs";
|
|
12
|
-
import {
|
|
11
|
+
import { ask } from "../ask.mjs";
|
|
12
|
+
import { requireInjected } from "./injected.mjs";
|
|
13
13
|
|
|
14
14
|
/** renderDescribe() renders one line per fact with no terminal punctuation of its own;
|
|
15
15
|
* rank.mjs's splitSentences() treats each line as its own candidate sentence, and
|
|
@@ -27,10 +27,16 @@ function withTerminalPunctuation(text) {
|
|
|
27
27
|
/**
|
|
28
28
|
* @param {object|null} graph a parseEntities() result, or null when no code graph is loaded
|
|
29
29
|
* @param {object|null} [memory=null] a loadMemory() payload, or null when there's no Fact store
|
|
30
|
+
* @param {object} [opts]
|
|
31
|
+
* @param {object} opts.store REQUIRED — the `{ createGraphService, readFactRows }` handles
|
|
32
|
+
* this adapter builds its two sentence sources from
|
|
30
33
|
* @returns {{search: Function, ask: Function}} a Repository-Interface-shaped graphService
|
|
31
34
|
*/
|
|
32
|
-
export function createCompletionsGraphAdapter(graph, memory = null) {
|
|
33
|
-
const
|
|
35
|
+
export function createCompletionsGraphAdapter(graph, memory = null, { store } = {}) {
|
|
36
|
+
const { createGraphService, readFactRows } = requireInjected(
|
|
37
|
+
store, ["createGraphService", "readFactRows"], { caller: "createCompletionsGraphAdapter", option: "store" },
|
|
38
|
+
);
|
|
39
|
+
const svc = graph ? createGraphService(graph, { ask }) : null;
|
|
34
40
|
|
|
35
41
|
return {
|
|
36
42
|
search(q, { limit } = {}) {
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// memory/blocks.mjs's buildNeighbours()/OVERLAP_MIN). Block granularity, no sub-block spans.
|
|
4
4
|
// Each group's label is its top shared-IDF tokens (df/N over the hit set, not the corpus).
|
|
5
5
|
|
|
6
|
-
import { buildNeighbours, OVERLAP_MIN, tokenizeBlock } from "../memory/blocks.mjs";
|
|
7
6
|
import { STOPWORDS } from "../prose.mjs";
|
|
7
|
+
import { requireInjected } from "./injected.mjs";
|
|
8
8
|
|
|
9
9
|
const LABEL_TOKEN_COUNT = 5;
|
|
10
10
|
|
|
@@ -14,8 +14,8 @@ const isContentToken = (t) => /^[a-z0-9]+$/.test(t) && !STOPWORDS.has(t);
|
|
|
14
14
|
|
|
15
15
|
/** tokenizeBlock(text), narrowed to real content tokens (see isContentToken above) — the
|
|
16
16
|
* token set this module actually clusters and labels on. */
|
|
17
|
-
function
|
|
18
|
-
return tokenizeBlock(text).filter(isContentToken);
|
|
17
|
+
function makeContentTokens(tokenizeBlock) {
|
|
18
|
+
return (text) => tokenizeBlock(text).filter(isContentToken);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/** Plain union-find (path halving, union-by-index) — small N here (a single broad search's
|
|
@@ -41,13 +41,21 @@ function unionFind(n) {
|
|
|
41
41
|
*
|
|
42
42
|
* @param {Array<{id:string, text:string}>} hits
|
|
43
43
|
* @param {object} [opts]
|
|
44
|
-
* @param {number} [opts.overlapMin
|
|
44
|
+
* @param {number} [opts.overlapMin] shared-token threshold for a similarity edge; defaults to
|
|
45
|
+
* the store's own OVERLAP_MIN
|
|
46
|
+
* @param {object} opts.store REQUIRED — the block store's `{ buildNeighbours, tokenizeBlock,
|
|
47
|
+
* OVERLAP_MIN }` clustering handles
|
|
45
48
|
* @returns {Array<{ id: string, members: Array<{id:string, text:string}>, memberIds: string[],
|
|
46
49
|
* tokens: string[], label: string }>}
|
|
47
50
|
* One entry per connected component (a singleton hit is still a one-member group).
|
|
48
51
|
* Deterministic order: groups by lowest member id; members within a group by id.
|
|
49
52
|
*/
|
|
50
|
-
export function groupHits(hits, { overlapMin
|
|
53
|
+
export function groupHits(hits, { overlapMin, store } = {}) {
|
|
54
|
+
const { buildNeighbours, tokenizeBlock, OVERLAP_MIN } = requireInjected(
|
|
55
|
+
store, ["buildNeighbours", "tokenizeBlock", "OVERLAP_MIN"], { caller: "groupHits", option: "store" },
|
|
56
|
+
);
|
|
57
|
+
const contentTokens = makeContentTokens(tokenizeBlock);
|
|
58
|
+
const edgeThreshold = overlapMin ?? OVERLAP_MIN;
|
|
51
59
|
const list = Array.isArray(hits) ? hits.filter((h) => h && h.id != null) : [];
|
|
52
60
|
if (!list.length) return [];
|
|
53
61
|
|
|
@@ -63,7 +71,7 @@ export function groupHits(hits, { overlapMin = OVERLAP_MIN } = {}) {
|
|
|
63
71
|
const tokensById = {};
|
|
64
72
|
for (const h of deduped) tokensById[h.id] = contentTokens(h.text || "");
|
|
65
73
|
|
|
66
|
-
const { ids, neighbours } = buildNeighbours(tokensById,
|
|
74
|
+
const { ids, neighbours } = buildNeighbours(tokensById, edgeThreshold);
|
|
67
75
|
const { find, union } = unionFind(ids.length);
|
|
68
76
|
for (let i = 0; i < ids.length; i += 1) {
|
|
69
77
|
for (const j of neighbours[i]) union(i, j);
|