@polycode-projects/the-mechanical-code-talker 0.4.0 → 0.6.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 +64 -28
- package/ROADMAP.md +6 -6
- package/bin/tmct.mjs +56 -0
- package/corpus/README.md +77 -5
- package/corpus/conceptnet/README.md +54 -2
- package/corpus/conceptnet/quality-filter.mjs +95 -0
- package/corpus/conceptnet/slice.jsonl +0 -378
- package/corpus/seon/LICENSE-NOTICE +37 -0
- package/corpus/seon/README.md +121 -0
- package/corpus/seon/concepts.jsonl +238 -0
- package/corpus/seon/definitions.jsonl +288 -0
- package/corpus/tier2/aws.jsonl +39 -0
- package/corpus/tier2/generate.mjs +253 -0
- package/corpus/tier2/java.jsonl +31 -0
- package/corpus/tier2/manifest.json +48 -0
- package/corpus/tier2/python.jsonl +30 -0
- package/data/templates/grammar-rules.toml +18 -10
- package/data/templates/responses.jsonl +2 -0
- package/package.json +10 -2
- package/src/ask-vocab.mjs +19 -1
- package/src/ask.mjs +90 -6
- package/src/chat.mjs +647 -60
- package/src/codegraph.mjs +28 -5
- package/src/conformance.mjs +166 -0
- package/src/corpus/conceptnet.mjs +24 -6
- package/src/grammar/lexicon-core.json +8 -0
- package/src/memory/inspect.mjs +25 -0
- package/src/server.mjs +88 -7
package/src/codegraph.mjs
CHANGED
|
@@ -1396,7 +1396,20 @@ export function renderHistory(graph, ind) {
|
|
|
1396
1396
|
}
|
|
1397
1397
|
|
|
1398
1398
|
/** Modules that call into the target's module (one hop over `calls`). */
|
|
1399
|
+
// Symbol-grain classes whose call graph lives on the fn/method-precise `callsSymbol`
|
|
1400
|
+
// edge, not the module-coarse `calls`. When the resolved target IS one of these, callers/
|
|
1401
|
+
// callees must read the SYMBOL node's own edges — mapping it to its enclosing module (the
|
|
1402
|
+
// old behaviour) both mislabels the answer with `mod:<path>` and scans the wrong edge set,
|
|
1403
|
+
// so "Widget.render --callsSymbol--> fnAlpha" was reported as "no recorded callers".
|
|
1404
|
+
const CALL_SYMBOL_CLASSES = new Set(["Function", "Method"]);
|
|
1405
|
+
|
|
1399
1406
|
export function renderCallers(graph, ind) {
|
|
1407
|
+
// symbol grain: a fine symbol's callers are the SUBJECTS of callsSymbol edges into it.
|
|
1408
|
+
if (CALL_SYMBOL_CLASSES.has(ind.class)) {
|
|
1409
|
+
const callers = [...new Set(edgesOfKind(graph, "callsSymbol").filter((e) => e.object === ind.id).map((e) => e.subjectLabel || e.subject))];
|
|
1410
|
+
if (!callers.length) return `${ind.label}: no recorded callers (fine-grained call edges are conservative — absence is not proof). Try tmct_impact for the full reverse closure.`;
|
|
1411
|
+
return `${ind.label} — called by ${callers.length} symbol(s):\n ${capJoin(callers, CALL_CAP, "\n ")}`;
|
|
1412
|
+
}
|
|
1400
1413
|
const modId = moduleIdOf(graph, ind);
|
|
1401
1414
|
if (!modId) return `cannot map ${ind.label} to a module.`;
|
|
1402
1415
|
const modLabel = graph.byId.get(modId)?.label || modId;
|
|
@@ -1405,8 +1418,15 @@ export function renderCallers(graph, ind) {
|
|
|
1405
1418
|
return `${modLabel} — called by ${callers.length} module(s):\n ${capJoin(callers, CALL_CAP, "\n ")}`;
|
|
1406
1419
|
}
|
|
1407
1420
|
|
|
1408
|
-
/**
|
|
1421
|
+
/** Callees of the target: the fn/method-precise callsSymbol edges when it is a fine symbol,
|
|
1422
|
+
* else the module-coarse `calls` one hop from its module. */
|
|
1409
1423
|
export function renderCallees(graph, ind) {
|
|
1424
|
+
// symbol grain: a fine symbol's callees are the OBJECTS of its callsSymbol edges.
|
|
1425
|
+
if (CALL_SYMBOL_CLASSES.has(ind.class)) {
|
|
1426
|
+
const callees = [...new Set(edgesOfKind(graph, "callsSymbol").filter((e) => e.subject === ind.id).map((e) => e.objectLabel || e.object))];
|
|
1427
|
+
if (!callees.length) return `${ind.label}: no recorded callees (calls only stdlib/external, or fine-grained call edges are not in the extracted graph).`;
|
|
1428
|
+
return `${ind.label} — calls into ${callees.length} symbol(s):\n ${capJoin(callees, CALL_CAP, "\n ")}`;
|
|
1429
|
+
}
|
|
1410
1430
|
const modId = moduleIdOf(graph, ind);
|
|
1411
1431
|
if (!modId) return `cannot map ${ind.label} to a module.`;
|
|
1412
1432
|
const modLabel = graph.byId.get(modId)?.label || modId;
|
|
@@ -1953,20 +1973,23 @@ export function renderCochanges(graph, ind) {
|
|
|
1953
1973
|
|
|
1954
1974
|
const EXPORTS_CAP = 40;
|
|
1955
1975
|
|
|
1956
|
-
/** A module's public export surface: each
|
|
1957
|
-
* defines it (so re-export hubs like __init__ are explicit).
|
|
1976
|
+
/** A module's public export surface: each exported name → the module that actually
|
|
1977
|
+
* defines it (so re-export hubs like __init__ / an index barrel are explicit). Reads the
|
|
1978
|
+
* `reexports` edge (mgx:reExports), which the extractor emits for ANY public-API construct
|
|
1979
|
+
* — Python `__all__` AND JS/TS `export` / `export { … } from …` — so the wording stays
|
|
1980
|
+
* language-neutral rather than implying a Python-only `__all__`. */
|
|
1958
1981
|
export function renderExports(graph, ind) {
|
|
1959
1982
|
const modId = moduleIdOf(graph, ind);
|
|
1960
1983
|
if (!modId) return `cannot map ${ind.label} to a module.`;
|
|
1961
1984
|
const modLabel = graph.byId.get(modId)?.label || modId;
|
|
1962
1985
|
const edges = edgesOfKind(graph, "reexports").filter((e) => e.subject === modId);
|
|
1963
|
-
if (!edges.length) return `${modLabel}: no public exports recorded (no
|
|
1986
|
+
if (!edges.length) return `${modLabel}: no public exports recorded (no export list / __all__ found, or none resolved).`;
|
|
1964
1987
|
const list = edges.slice(0, EXPORTS_CAP).map((e) => {
|
|
1965
1988
|
const origin = graph.byId.get(e.object);
|
|
1966
1989
|
const where = origin ? siteOf(origin) : null;
|
|
1967
1990
|
const from = where ? ` ← ${where.path}` : "";
|
|
1968
1991
|
return `${e.objectLabel || e.object}${from}`;
|
|
1969
1992
|
});
|
|
1970
|
-
return `${modLabel} — public API (${edges.length} export(s)
|
|
1993
|
+
return `${modLabel} — public API (${edges.length} export(s)):\n ${list.join("\n ")}` +
|
|
1971
1994
|
(edges.length > EXPORTS_CAP ? `\n …+${edges.length - EXPORTS_CAP} more` : "");
|
|
1972
1995
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// conformance.mjs — the Repository-Interface CONTRACT TEST SUITE as a reusable kit.
|
|
2
|
+
//
|
|
3
|
+
// PLAN_REPOSITORY_INTERFACE.md deliverable 3: an implementation is CONFORMANT iff it
|
|
4
|
+
// passes `runConformance(name, makeProvider)`. tmct's own fixture + bootstrap providers
|
|
5
|
+
// pass it in `npm test`; an EXTERNAL producer (seonix) imports this kit from the
|
|
6
|
+
// published package and runs the SAME suite against its native provider to claim
|
|
7
|
+
// conformance — conformance is the suite, not prose. It is provider-agnostic: it
|
|
8
|
+
// asserts the SHAPE + the error contract; data-bearing truth is asserted by the caller
|
|
9
|
+
// where its provider carries data.
|
|
10
|
+
//
|
|
11
|
+
// Public surface (exported here + via the package "./conformance" subpath):
|
|
12
|
+
// runConformance(name, makeProvider), assertResult, assertIndividual, assertEdge.
|
|
13
|
+
import { test } from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import {
|
|
16
|
+
INTERFACE_VERSION,
|
|
17
|
+
MISS_REASONS,
|
|
18
|
+
EDGE_KINDS,
|
|
19
|
+
SERVICES,
|
|
20
|
+
SOURCE_SERVICES,
|
|
21
|
+
isHit,
|
|
22
|
+
isMiss,
|
|
23
|
+
} from "./repository-interface.mjs";
|
|
24
|
+
|
|
25
|
+
const REASONS = new Set(Object.values(MISS_REASONS));
|
|
26
|
+
|
|
27
|
+
// ---- Individual / Edge / Result shape validators (the shared wire types) ------
|
|
28
|
+
|
|
29
|
+
export function assertIndividual(ind, where) {
|
|
30
|
+
assert.equal(typeof ind.id, "string", `${where}: Individual.id is a string`);
|
|
31
|
+
assert.equal(typeof ind.label, "string", `${where}: Individual.label is a string`);
|
|
32
|
+
assert.equal(typeof ind.class, "string", `${where}: Individual.class is a string (a tmct: class token)`);
|
|
33
|
+
assert.ok(Array.isArray(ind.attributes), `${where}: Individual.attributes is an array`);
|
|
34
|
+
for (const a of ind.attributes) {
|
|
35
|
+
assert.ok("key" in a && "value" in a && "prop" in a, `${where}: attribute has {key,value,prop}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertEdge(e, where) {
|
|
40
|
+
for (const k of ["subject", "object", "predicate", "prop"]) {
|
|
41
|
+
assert.ok(k in e, `${where}: Edge carries required "${k}"`);
|
|
42
|
+
}
|
|
43
|
+
assert.equal(typeof e.subject, "string", `${where}: Edge.subject is a string`);
|
|
44
|
+
assert.equal(typeof e.object, "string", `${where}: Edge.object is a string`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function assertResult(r, where) {
|
|
48
|
+
assert.ok(r && typeof r === "object", `${where}: a Result object`);
|
|
49
|
+
assert.equal(typeof r.ok, "boolean", `${where}: Result.ok is a boolean`);
|
|
50
|
+
if (r.ok) {
|
|
51
|
+
assert.ok("value" in r, `${where}: a hit carries value`);
|
|
52
|
+
} else {
|
|
53
|
+
assert.ok(r.miss && REASONS.has(r.miss.reason), `${where}: a miss carries a CLOSED-set reason (got ${r.miss?.reason})`);
|
|
54
|
+
assert.equal(typeof r.miss.detail, "string", `${where}: miss.detail is a string`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// The provider-agnostic conformance kit. Run it against any implementation.
|
|
60
|
+
// =============================================================================
|
|
61
|
+
export function runConformance(name, makeProvider) {
|
|
62
|
+
test(`[${name}] advertises the full interface + version`, () => {
|
|
63
|
+
const svc = makeProvider();
|
|
64
|
+
assert.equal(svc.version, INTERFACE_VERSION, "declares the interface version");
|
|
65
|
+
for (const service of SERVICES) {
|
|
66
|
+
assert.ok(svc.capabilities.includes(service), `capability advertises "${service}"`);
|
|
67
|
+
assert.equal(typeof svc[service], "function", `implements "${service}" as a function`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test(`[${name}] every service returns a well-formed Result (or an honest empty)`, () => {
|
|
72
|
+
const svc = makeProvider();
|
|
73
|
+
// Resolution-family with a term that certainly does not exist → a well-formed result.
|
|
74
|
+
for (const [service, args] of [
|
|
75
|
+
["resolve", ["definitely-not-a-symbol-xyz"]],
|
|
76
|
+
["describe", ["no:such:id"]],
|
|
77
|
+
["members", ["no:such:id"]],
|
|
78
|
+
["subclasses", ["no:such:id"]],
|
|
79
|
+
["exports", ["no:such:id"]],
|
|
80
|
+
["signature", ["no:such:id"]],
|
|
81
|
+
["impact", ["no:such:id"]],
|
|
82
|
+
["history", ["no:such:id"]],
|
|
83
|
+
["snippet", ["no:such:id"]],
|
|
84
|
+
["context", ["no-such-symbol"]],
|
|
85
|
+
["architecture", [{}]],
|
|
86
|
+
["untested", []],
|
|
87
|
+
["stats", []],
|
|
88
|
+
["search", ["", {}]],
|
|
89
|
+
["ask", ["what is here"]],
|
|
90
|
+
]) {
|
|
91
|
+
const r = svc[service](...args);
|
|
92
|
+
assertResult(r, `${name}.${service}`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test(`[${name}] the error contract: a clean miss is a value, never a throw`, () => {
|
|
97
|
+
const svc = makeProvider();
|
|
98
|
+
// An unresolved term is a first-class UNRESOLVED_TERM miss on every id-taking service.
|
|
99
|
+
for (const service of ["describe", "members", "subclasses", "exports", "signature", "impact", "history"]) {
|
|
100
|
+
const r = svc[service]("no:such:id:at:all");
|
|
101
|
+
assert.ok(isMiss(r), `${service} on an absent id misses`);
|
|
102
|
+
assert.equal(r.miss.reason, MISS_REASONS.UNRESOLVED_TERM, `${service} → UNRESOLVED_TERM`);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test(`[${name}] edges: closed kind vocabulary; unknown kind is misuse (throws)`, () => {
|
|
107
|
+
const svc = makeProvider();
|
|
108
|
+
// A valid kind on a missing id misses (UNRESOLVED_TERM), never throws.
|
|
109
|
+
const r = svc.edges("no:such:id", EDGE_KINDS[0]);
|
|
110
|
+
assertResult(r, `${name}.edges`);
|
|
111
|
+
// An unknown kind is a programmer error, not a domain miss.
|
|
112
|
+
assert.throws(() => svc.edges("no:such:id", "not-a-real-kind"), TypeError);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test(`[${name}] source services answer NO_SOURCE (not a throw) when no working tree`, () => {
|
|
116
|
+
const svc = makeProvider();
|
|
117
|
+
if (svc.sourceAccess) return; // a source-capable provider is exempt from this shape
|
|
118
|
+
for (const service of SOURCE_SERVICES) {
|
|
119
|
+
// Use whatever the provider resolves; on empty graphs this is UNRESOLVED_TERM, on
|
|
120
|
+
// data-bearing graphs NO_SOURCE — both are valid closed-set misses.
|
|
121
|
+
const arg = service === "context" ? "x" : "no:such:id";
|
|
122
|
+
const r = svc[service](arg);
|
|
123
|
+
assert.ok(isMiss(r), `${service} misses without a working tree`);
|
|
124
|
+
assert.ok(
|
|
125
|
+
[MISS_REASONS.NO_SOURCE, MISS_REASONS.UNRESOLVED_TERM].includes(r.miss.reason),
|
|
126
|
+
`${service} miss reason is NO_SOURCE or UNRESOLVED_TERM (got ${r.miss.reason})`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test(`[${name}] stats / untested / architecture never miss — empty is a hit`, () => {
|
|
132
|
+
const svc = makeProvider();
|
|
133
|
+
const stats = svc.stats();
|
|
134
|
+
assert.ok(isHit(stats), "stats is always a hit");
|
|
135
|
+
assert.equal(typeof stats.value.total, "number");
|
|
136
|
+
assert.ok(Array.isArray(stats.value.classes));
|
|
137
|
+
assert.ok(isHit(svc.untested()), "untested is always a hit");
|
|
138
|
+
assert.ok(Array.isArray(svc.untested().value.modules));
|
|
139
|
+
assert.ok(isHit(svc.architecture({})), "architecture is always a hit");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test(`[${name}] concurrent/re-entrant: two handles, interleaved reads, stable & independent`, async () => {
|
|
143
|
+
const a = makeProvider();
|
|
144
|
+
const b = makeProvider();
|
|
145
|
+
// Fire the whole read surface concurrently across two independent handles; assert
|
|
146
|
+
// every result is well-formed and that a second identical call is byte-stable.
|
|
147
|
+
const calls = [
|
|
148
|
+
() => a.stats(),
|
|
149
|
+
() => b.stats(),
|
|
150
|
+
() => a.architecture({}),
|
|
151
|
+
() => a.untested(),
|
|
152
|
+
() => b.search("", {}),
|
|
153
|
+
() => a.resolve("x"),
|
|
154
|
+
() => b.ask("anything"),
|
|
155
|
+
() => a.describe("no:such:id"),
|
|
156
|
+
];
|
|
157
|
+
const first = await Promise.all(calls.map((c) => Promise.resolve().then(c)));
|
|
158
|
+
for (const r of first) assertResult(r, `${name}.concurrent`);
|
|
159
|
+
const second = await Promise.all(calls.map((c) => Promise.resolve().then(c)));
|
|
160
|
+
// Determinism across handles: same query, same JSON.
|
|
161
|
+
assert.equal(JSON.stringify(first.map((r) => r.ok)), JSON.stringify(second.map((r) => r.ok)));
|
|
162
|
+
// The two handles are independent objects (no shared mutable state leak).
|
|
163
|
+
assert.notEqual(a, b);
|
|
164
|
+
assert.deepEqual(a.stats().value, b.stats().value, "same graph → same stats across handles");
|
|
165
|
+
});
|
|
166
|
+
}
|
|
@@ -31,6 +31,17 @@ const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
|
31
31
|
export const SLICE_FILE = join(PKG_ROOT, "corpus", "conceptnet", "slice.jsonl");
|
|
32
32
|
export const MAP_FILE = join(PKG_ROOT, "src", "corpus", "conceptnet-map.toml");
|
|
33
33
|
|
|
34
|
+
// The tier-1 curated Software-Engineering ontology (SEON). concepts.jsonl is in
|
|
35
|
+
// the SAME slice shape as ConceptNet ({start, rel, end, weight}), so it loads +
|
|
36
|
+
// maps through the identical loadSlice/loadMap/toFacts path — just with a
|
|
37
|
+
// "corpus:seon" provenance prefix. definitions.jsonl is a separate {term,
|
|
38
|
+
// definition, sense} list the chat answer layer prefers for a lexicon term's
|
|
39
|
+
// "what is a <term>". tier-2 corpuses (aws/python/java) share the slice shape too.
|
|
40
|
+
export const SEON_CONCEPTS_FILE = join(PKG_ROOT, "corpus", "seon", "concepts.jsonl");
|
|
41
|
+
export const SEON_DEFINITIONS_FILE = join(PKG_ROOT, "corpus", "seon", "definitions.jsonl");
|
|
42
|
+
export const TIER2_DIR = join(PKG_ROOT, "corpus", "tier2");
|
|
43
|
+
export const TIER2_MANIFEST_FILE = join(TIER2_DIR, "manifest.json");
|
|
44
|
+
|
|
34
45
|
const ACE_PATTERNS = new Set(["subClassOf", "type", "ObjectProperty", "someValuesFrom", "disjointWith", "property", "none"]);
|
|
35
46
|
|
|
36
47
|
/** Load the slice JSONL as a stream (never the whole file as one string) and
|
|
@@ -92,8 +103,13 @@ export const termText = (uri) => {
|
|
|
92
103
|
* (provenance is a STRING — exactly what src/memory/core.mjs appendFact
|
|
93
104
|
* takes; it names the corpus and the originating ConceptNet relation).
|
|
94
105
|
* Rows whose relation maps ace="none" are skipped — deliberate non-emission.
|
|
95
|
-
* A relation with NO row in the map throws: that is table drift, not data.
|
|
96
|
-
|
|
106
|
+
* A relation with NO row in the map throws: that is table drift, not data.
|
|
107
|
+
*
|
|
108
|
+
* `provenancePrefix` names the corpus half of the provenance string; it defaults
|
|
109
|
+
* to "corpus:conceptnet" so the ConceptNet seed stays BYTE-IDENTICAL to before.
|
|
110
|
+
* The seon / tier-2 corpuses reuse the same slice shape, tagged "corpus:seon" or
|
|
111
|
+
* "corpus:tier2:<id>" so a reader can tell a curated SE fact from ConceptNet noise. */
|
|
112
|
+
export function toFacts(assertions, map, provenancePrefix = "corpus:conceptnet") {
|
|
97
113
|
const facts = [];
|
|
98
114
|
for (const a of assertions) {
|
|
99
115
|
const row = map.get(a.rel);
|
|
@@ -108,7 +124,7 @@ export function toFacts(assertions, map) {
|
|
|
108
124
|
subject,
|
|
109
125
|
predicate: row.predicate,
|
|
110
126
|
object,
|
|
111
|
-
provenance:
|
|
127
|
+
provenance: `${provenancePrefix} ${a.rel}`,
|
|
112
128
|
});
|
|
113
129
|
}
|
|
114
130
|
return facts;
|
|
@@ -127,10 +143,12 @@ export function toFacts(assertions, map) {
|
|
|
127
143
|
* Idempotent twice over: appendFact's content-hashed ids make a blind
|
|
128
144
|
* re-append an upsert, and we pre-read the store once to skip triples that
|
|
129
145
|
* are already there (so re-seeding costs one read, not N rewrites).
|
|
130
|
-
* Returns { appended, skipped, total }.
|
|
131
|
-
|
|
146
|
+
* Returns { appended, skipped, total }. `provenancePrefix` is threaded through to
|
|
147
|
+
* toFacts (default "corpus:conceptnet" → byte-identical seed) so a seon/tier-2
|
|
148
|
+
* corpus can tag its facts "corpus:seon" / "corpus:tier2:<id>". */
|
|
149
|
+
export async function seedMemory(dir, { limit, slicePath = SLICE_FILE, mapPath = MAP_FILE, prefer, provenancePrefix } = {}) {
|
|
132
150
|
const [assertions, map] = await Promise.all([loadSlice(slicePath), loadMap(mapPath)]);
|
|
133
|
-
let facts = toFacts(assertions, map);
|
|
151
|
+
let facts = toFacts(assertions, map, provenancePrefix);
|
|
134
152
|
if (Array.isArray(prefer) && prefer.length) {
|
|
135
153
|
const rank = new Map(prefer.map((p, i) => [p, i]));
|
|
136
154
|
// stable partition: Array.prototype.sort is stable in Node, so equal-rank
|
package/src/memory/inspect.mjs
CHANGED
|
@@ -128,6 +128,31 @@ export function renderMemory({ memory, blocks }, { verbose = false } = {}) {
|
|
|
128
128
|
lines.push("", "blocks — none folded yet (a session folds when it ends).");
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// ---- explore hooks: real, runnable example queries built from what's actually
|
|
132
|
+
// stored, so /memory is a springboard for drilling in, not just a dump ----
|
|
133
|
+
if (individuals.length) {
|
|
134
|
+
const clean = (t) => typeof t === "string" && /^[a-z][a-z0-9]+(?: [a-z0-9]{2,}){0,2}$/.test(t) && t.length <= 22 && !/^\d+$/.test(t);
|
|
135
|
+
const facts = readFactRows(memory);
|
|
136
|
+
// Rank candidate "what is a X" terms by CATEGORY SIZE (how many facts point at
|
|
137
|
+
// them) so the hooks land on rich, recognisable categories (function, class, …),
|
|
138
|
+
// not a lone ConceptNet oddity.
|
|
139
|
+
const freq = new Map();
|
|
140
|
+
for (const f of facts) if (clean(f.object)) freq.set(f.object, (freq.get(f.object) || 0) + 1);
|
|
141
|
+
const terms = [...freq.entries()]
|
|
142
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
143
|
+
.map(([t]) => t)
|
|
144
|
+
.slice(0, 3);
|
|
145
|
+
const sample = facts.find((f) => clean(f.subject) && terms.includes(f.object));
|
|
146
|
+
const ex = terms.map((t) => ` what is a ${t}`);
|
|
147
|
+
if (sample) ex.push(` is a ${sample.subject} a ${sample.object}`);
|
|
148
|
+
if (terms[0]) ex.push(` what did i tell you about ${terms[0]}`);
|
|
149
|
+
if (ex.length) {
|
|
150
|
+
lines.push("", "explore — ask any of these (real terms from the store above):");
|
|
151
|
+
lines.push(...ex);
|
|
152
|
+
lines.push(" /memory verbose — the full store · /stats — the code-graph overview");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
131
156
|
return lines.join("\n");
|
|
132
157
|
}
|
|
133
158
|
|
package/src/server.mjs
CHANGED
|
@@ -49,9 +49,73 @@ import {
|
|
|
49
49
|
} from "./codegraph.mjs";
|
|
50
50
|
import { ask } from "./ask.mjs";
|
|
51
51
|
import { createGraphService } from "./providers/graph-service.mjs";
|
|
52
|
+
// Read-ONLY consumers of the conversational-memory graph (src/memory/core.mjs) — the 500
|
|
53
|
+
// corpus facts live there, NOT in the code-map graph.json every tool below loads. Used by
|
|
54
|
+
// the FALL-THROUGH bridge (below): when the code-map resolves NOTHING for a concept query
|
|
55
|
+
// (/subclasses /describe /members /find), answer from the reified isa-family facts instead
|
|
56
|
+
// of a flat "no entity". Never written here; memory writes stay owned by memory/*.
|
|
57
|
+
import { loadMemory, readFactRows, normFactTerm } from "./memory/core.mjs";
|
|
52
58
|
|
|
53
59
|
const SNIPPET_MAX_LINES = 200;
|
|
54
60
|
|
|
61
|
+
// The reified isa-family predicates a memory Fact carries ("<subject> rdfs:subClassOf
|
|
62
|
+
// <object>" / "rdf:type"): subject IS-A object. Subclasses of X = facts whose OBJECT is X;
|
|
63
|
+
// superclasses of X = facts whose SUBJECT is X. (Matches chat.mjs's ISA_PREDICATES.)
|
|
64
|
+
const ISA_PREDICATES = new Set(["rdfs:subClassOf", "rdf:type"]);
|
|
65
|
+
const MEMORY_LIST_CAP = 40;
|
|
66
|
+
|
|
67
|
+
/** Load the conversational-memory Facts as trust-bearing rows, failure-tolerant (no memory
|
|
68
|
+
* store / unreadable → [], so the tool still returns its honest code-map miss). repoRoot is
|
|
69
|
+
* the dir that CONTAINS .tmct/ (graphFile = <repo>/.tmct/graph.json), which is exactly the
|
|
70
|
+
* `dir` loadMemory joins MEMORY_GRAPH_REL onto. */
|
|
71
|
+
async function memoryFactRows(config) {
|
|
72
|
+
try {
|
|
73
|
+
return readFactRows(await loadMemory(dirname(dirname(config.graphFile))));
|
|
74
|
+
} catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A short provenance receipt for a set of memory rows — distinct source strings, capped. */
|
|
80
|
+
function memoryProvenance(rows) {
|
|
81
|
+
const provs = [...new Set(rows.map((r) => r.provenance).filter(Boolean))];
|
|
82
|
+
if (!provs.length) return "provenance: memory/corpus facts";
|
|
83
|
+
const shown = provs.slice(0, 2).join("; ");
|
|
84
|
+
return `provenance: ${shown}${provs.length > 2 ? `, +${provs.length - 2} more source(s)` : ""}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** FALL-THROUGH: subclasses of a concept from the reified isa-family facts (subjects of
|
|
88
|
+
* "<subj> subClassOf <term>"). Null when the term names no such facts (so the caller can
|
|
89
|
+
* keep the honest code-map miss). Provenance is always cited. */
|
|
90
|
+
function renderMemorySubclasses(rows, term) {
|
|
91
|
+
const t = normFactTerm(term);
|
|
92
|
+
const hits = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && r.object === t);
|
|
93
|
+
if (!hits.length) return null;
|
|
94
|
+
const labels = [...new Set(hits.map((r) => r.subject))].sort();
|
|
95
|
+
const shown = labels.slice(0, MEMORY_LIST_CAP);
|
|
96
|
+
const tail = labels.length > MEMORY_LIST_CAP ? `\n …+${labels.length - MEMORY_LIST_CAP} more` : "";
|
|
97
|
+
return `"${term}" is not a code-map entity — answering from memory/corpus facts. ` +
|
|
98
|
+
`${labels.length} known subclass(es):\n ${shown.join("\n ")}${tail}\n(${memoryProvenance(hits)})`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** FALL-THROUGH: a concept's DEFINITION from the isa-family facts — its superclasses ("is
|
|
102
|
+
* a …") plus a count/sample of its known subclasses. Null when the term names no facts. */
|
|
103
|
+
function renderMemoryDefinition(rows, term) {
|
|
104
|
+
const t = normFactTerm(term);
|
|
105
|
+
const isa = rows.filter((r) => ISA_PREDICATES.has(r.predicate) && (r.subject === t || r.object === t));
|
|
106
|
+
if (!isa.length) return null;
|
|
107
|
+
const supers = [...new Set(isa.filter((r) => r.subject === t).map((r) => r.object))];
|
|
108
|
+
const subs = [...new Set(isa.filter((r) => r.object === t).map((r) => r.subject))].sort();
|
|
109
|
+
const lines = [`"${term}" is not a code-map entity — answering from memory/corpus facts.`];
|
|
110
|
+
if (supers.length) lines.push(`is a: ${supers.slice(0, MEMORY_LIST_CAP).join(", ")}`);
|
|
111
|
+
if (subs.length) {
|
|
112
|
+
const tail = subs.length > MEMORY_LIST_CAP ? `, +${subs.length - MEMORY_LIST_CAP} more` : "";
|
|
113
|
+
lines.push(`known subclasses (${subs.length}): ${subs.slice(0, MEMORY_LIST_CAP).join(", ")}${tail}`);
|
|
114
|
+
}
|
|
115
|
+
lines.push(`(${memoryProvenance(isa)})`);
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
55
119
|
// Tiered tool surface: the hot tools carry full descriptions/schemas in this
|
|
56
120
|
// catalog; every COLD tool (describe/members/impact/history/…) is still served
|
|
57
121
|
// by dispatchTool below and is reachable via the CLI `cli <tool>` route +
|
|
@@ -298,8 +362,11 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
298
362
|
if (name === "tmct_describe") {
|
|
299
363
|
const symbol = String(args?.symbol || "").trim();
|
|
300
364
|
if (!symbol) throw new ToolError("symbol is required");
|
|
301
|
-
const { match, candidates } =
|
|
302
|
-
return renderDescribe(graph, match, { candidates });
|
|
365
|
+
const { match, candidates } = resolveSymbol(svc.graph, symbol);
|
|
366
|
+
if (match) return renderDescribe(graph, match, { candidates }); // code-map wins when present
|
|
367
|
+
const fb = renderMemoryDefinition(await memoryFactRows(config), symbol);
|
|
368
|
+
if (fb) return fb;
|
|
369
|
+
resolveOrThrow(svc, symbol, "symbol"); // no code-map + no memory fact → the honest miss
|
|
303
370
|
}
|
|
304
371
|
if (name === "tmct_snippet") {
|
|
305
372
|
const symbol = String(args?.symbol || "").trim();
|
|
@@ -347,23 +414,37 @@ export async function dispatchTool(name, args, { config, source = defaultSource
|
|
|
347
414
|
const query = String(args?.query || "").trim();
|
|
348
415
|
const kind = String(args?.kind || "").trim();
|
|
349
416
|
if (!query && !kind) throw new ToolError("query is required");
|
|
350
|
-
|
|
417
|
+
const out = renderSearch(graph, query, {
|
|
351
418
|
kind,
|
|
352
419
|
decorator: String(args?.decorator || "").trim(),
|
|
353
420
|
name: String(args?.name || "").trim(),
|
|
354
421
|
});
|
|
422
|
+
// FALL-THROUGH: a code-map miss ("no module matches …") on a plain concept query still
|
|
423
|
+
// answers from the memory/corpus isa-family facts when the concept is known there.
|
|
424
|
+
if (!kind && /^no module matches/.test(out)) {
|
|
425
|
+
const fb = renderMemoryDefinition(await memoryFactRows(config), query);
|
|
426
|
+
if (fb) return fb;
|
|
427
|
+
}
|
|
428
|
+
return out;
|
|
355
429
|
}
|
|
356
430
|
if (name === "tmct_members") {
|
|
357
431
|
const symbol = String(args?.class || "").trim();
|
|
358
432
|
if (!symbol) throw new ToolError("class is required");
|
|
359
|
-
const { match } =
|
|
360
|
-
return renderMembers(graph, match);
|
|
433
|
+
const { match } = resolveSymbol(svc.graph, symbol);
|
|
434
|
+
if (match) return renderMembers(graph, match); // code-map wins when present
|
|
435
|
+
// a concept's "members" in the corpus sense are its subclasses (its instances).
|
|
436
|
+
const fb = renderMemorySubclasses(await memoryFactRows(config), symbol);
|
|
437
|
+
if (fb) return fb;
|
|
438
|
+
resolveOrThrow(svc, symbol, "class"); // the honest miss
|
|
361
439
|
}
|
|
362
440
|
if (name === "tmct_subclasses") {
|
|
363
441
|
const symbol = String(args?.class || "").trim();
|
|
364
442
|
if (!symbol) throw new ToolError("class is required");
|
|
365
|
-
const { match } =
|
|
366
|
-
return renderSubclasses(graph, match);
|
|
443
|
+
const { match } = resolveSymbol(svc.graph, symbol);
|
|
444
|
+
if (match) return renderSubclasses(graph, match); // code-map wins when present
|
|
445
|
+
const fb = renderMemorySubclasses(await memoryFactRows(config), symbol);
|
|
446
|
+
if (fb) return fb;
|
|
447
|
+
resolveOrThrow(svc, symbol, "class"); // no code-map subclass + no memory fact → honest miss
|
|
367
448
|
}
|
|
368
449
|
if (name === "tmct_architecture") {
|
|
369
450
|
return renderArchitecture(graph, { pkg: String(args?.package || "").trim() });
|