@polycode-projects/the-mechanical-code-talker 0.2.0 → 0.4.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 +77 -3
- package/ROADMAP.md +416 -3
- package/bin/tmct.mjs +308 -12
- package/corpus/README.md +52 -0
- package/corpus/conceptnet/LICENSE-NOTICE +37 -0
- package/corpus/conceptnet/README.md +103 -0
- package/corpus/conceptnet/fetch-slice.mjs +136 -0
- package/corpus/conceptnet/filter-dump.mjs +89 -0
- package/corpus/conceptnet/slice.jsonl +14258 -0
- package/data/phrasebook/software-phrases.txt +231 -0
- package/data/templates/grammar-rules.toml +89 -0
- package/data/templates/responses.jsonl +68 -0
- package/package.json +40 -3
- package/src/ask-nlp.mjs +22 -10
- package/src/ask-vocab.mjs +35 -1
- package/src/ask.mjs +171 -494
- package/src/chat.mjs +709 -81
- package/src/corpus/conceptnet-map.toml +251 -0
- package/src/corpus/conceptnet.mjs +167 -0
- package/src/corpus/templates.mjs +188 -0
- package/src/finish.mjs +443 -0
- package/src/grammar/ace.mjs +341 -0
- package/src/grammar/assert.mjs +40 -0
- package/src/grammar/lexicon-core.json +287 -0
- package/src/grammar/lexicon.mjs +202 -0
- package/src/hash.mjs +32 -0
- package/src/index.mjs +21 -5
- package/src/init.mjs +264 -0
- package/src/interpret/fuzzy.mjs +89 -0
- package/src/interpret/merge.mjs +148 -0
- package/src/interpret/normalize.mjs +151 -0
- package/src/interpret/pipeline.mjs +112 -0
- package/src/interpret/strategies/grammar.mjs +137 -0
- package/src/interpret/strategies/keywords.mjs +241 -0
- package/src/interpret/strategies/noise-strip.mjs +114 -0
- package/src/memory/blocks.mjs +221 -0
- package/src/memory/core.mjs +533 -0
- package/src/memory/fold.mjs +0 -0
- package/src/memory/inspect.mjs +141 -0
- package/src/memory/trust.mjs +113 -0
- package/src/prose-nlp.mjs +14 -16
- package/src/providers/bootstrap.mjs +24 -0
- package/src/providers/fixture.mjs +118 -0
- package/src/providers/graph-service.mjs +312 -0
- package/src/repository-interface.mjs +318 -0
- package/src/server.mjs +44 -28
- package/src/sessions.mjs +137 -4
- package/src/source.mjs +44 -5
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/tui/app.mjs +173 -0
- package/src/wink-model.mjs +74 -0
- package/bin/cli.mjs +0 -226
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// The reference Repository-Interface service over a parsed code graph.
|
|
2
|
+
// PLAN_REPOSITORY_INTERFACE.md — "the executable specification".
|
|
3
|
+
//
|
|
4
|
+
// createGraphService(graph) returns a typed service object implementing EVERY
|
|
5
|
+
// service in src/repository-interface.mjs over the `{ individuals, byId,
|
|
6
|
+
// relations, … }` shape parseEntities() yields. Every method returns a typed
|
|
7
|
+
// Result (hit/miss) — a clean miss is a value, never a throw. The two providers
|
|
8
|
+
// tmct ships (fixture, bootstrap) are this same builder over a small real graph
|
|
9
|
+
// and over the empty bootstrap graph respectively.
|
|
10
|
+
//
|
|
11
|
+
// This is a GRAPH-ONLY provider: it advertises the source services (snippet,
|
|
12
|
+
// context) but answers them with an honest miss(NO_SOURCE) — it exposes no
|
|
13
|
+
// working tree. A host with a working tree (seonix, the chat shell) layers source
|
|
14
|
+
// access on top. Pure graph queries, no fs, no LLM.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
resolveSymbol,
|
|
18
|
+
siteOf,
|
|
19
|
+
edgesOfKind,
|
|
20
|
+
relationKind,
|
|
21
|
+
impactClosure,
|
|
22
|
+
} from "../codegraph.mjs";
|
|
23
|
+
import { ask } from "../ask.mjs";
|
|
24
|
+
import {
|
|
25
|
+
hit,
|
|
26
|
+
miss,
|
|
27
|
+
toIndividual,
|
|
28
|
+
toEdge,
|
|
29
|
+
MISS_REASONS,
|
|
30
|
+
EDGE_KINDS,
|
|
31
|
+
SERVICES,
|
|
32
|
+
SOURCE_SERVICES,
|
|
33
|
+
INTERFACE_VERSION,
|
|
34
|
+
} from "../repository-interface.mjs";
|
|
35
|
+
|
|
36
|
+
const attrOf = (ind, key) => (ind?.attributes || []).find((a) => a.key === key)?.value ?? null;
|
|
37
|
+
|
|
38
|
+
/** Group an individual id's incoming/outgoing edges across all relations, projected
|
|
39
|
+
* to interface Edges. */
|
|
40
|
+
function edgesAround(graph, id) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const incoming = [];
|
|
43
|
+
for (const g of graph.relations) {
|
|
44
|
+
for (const e of g.edges) {
|
|
45
|
+
if (e.subject === id) out.push(toEdge(e, { predicate: g.predicate, prop: g.prop }));
|
|
46
|
+
if (e.object === id) incoming.push(toEdge(e, { predicate: g.predicate, prop: g.prop }));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { out, incoming };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The kind-tagged relation group's predicate/prop for a given edge kind (for Edge
|
|
53
|
+
* projection). Returns the first relation group classifying to `kind`. */
|
|
54
|
+
function groupMetaForKind(graph, kind) {
|
|
55
|
+
for (const g of graph.relations) if (relationKind(g) === kind) return { predicate: g.predicate, prop: g.prop };
|
|
56
|
+
return { predicate: kind, prop: null };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} graph a parseEntities() result
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {boolean} [opts.sourceAccess=false] whether source services can read bodies
|
|
63
|
+
* @returns the typed service object
|
|
64
|
+
*/
|
|
65
|
+
export function createGraphService(graph, { sourceAccess = false } = {}) {
|
|
66
|
+
const byId = graph.byId;
|
|
67
|
+
|
|
68
|
+
const resolveId = (id) => byId.get(id) || null;
|
|
69
|
+
|
|
70
|
+
const svc = {
|
|
71
|
+
version: INTERFACE_VERSION,
|
|
72
|
+
/** Advertised services. Source services are listed but honestly answer NO_SOURCE
|
|
73
|
+
* unless a source-capable provider overrides them. */
|
|
74
|
+
capabilities: [...SERVICES],
|
|
75
|
+
sourceAccess: Boolean(sourceAccess),
|
|
76
|
+
/** The underlying graph — tmct-internal presentation (render*) reads it; not
|
|
77
|
+
* part of the wire contract. */
|
|
78
|
+
graph,
|
|
79
|
+
|
|
80
|
+
resolve(term) {
|
|
81
|
+
const { match, candidates } = resolveSymbol(graph, String(term ?? ""));
|
|
82
|
+
if (!match) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: String(term ?? "") });
|
|
83
|
+
return hit({ match: toIndividual(match), candidates: candidates.map(toIndividual) });
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
describe(id) {
|
|
87
|
+
const ind = resolveId(id);
|
|
88
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: id });
|
|
89
|
+
const { out, incoming } = edgesAround(graph, id);
|
|
90
|
+
return hit({ individual: toIndividual(ind), out, incoming });
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
members(classId) {
|
|
94
|
+
const ind = resolveId(classId);
|
|
95
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: classId });
|
|
96
|
+
const contains = edgesOfKind(graph, "contains").filter((e) => e.subject === classId);
|
|
97
|
+
const methods = [];
|
|
98
|
+
const attributes = [];
|
|
99
|
+
for (const e of contains) {
|
|
100
|
+
const m = byId.get(e.object);
|
|
101
|
+
const proj = m ? toIndividual(m) : { id: e.object, label: e.objectLabel || e.object, class: "Entity", attributes: [] };
|
|
102
|
+
if ((m?.class || "") === "Attribute") attributes.push(proj);
|
|
103
|
+
else methods.push(proj);
|
|
104
|
+
}
|
|
105
|
+
return hit({ methods, attributes });
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
subclasses(classId) {
|
|
109
|
+
const ind = resolveId(classId);
|
|
110
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: classId });
|
|
111
|
+
const inherits = edgesOfKind(graph, "inherits");
|
|
112
|
+
const bases = inherits
|
|
113
|
+
.filter((e) => e.subject === classId)
|
|
114
|
+
.map((e) => byId.get(e.object) ? toIndividual(byId.get(e.object)) : { id: e.object, label: e.objectLabel || e.object, class: "Class", attributes: [] });
|
|
115
|
+
// transitive reverse inheritance closure (who extends this)
|
|
116
|
+
const childrenOf = new Map();
|
|
117
|
+
for (const e of inherits) {
|
|
118
|
+
if (!childrenOf.has(e.object)) childrenOf.set(e.object, []);
|
|
119
|
+
childrenOf.get(e.object).push(e.subject);
|
|
120
|
+
}
|
|
121
|
+
const seen = new Set([classId]);
|
|
122
|
+
const subs = [];
|
|
123
|
+
let frontier = [classId];
|
|
124
|
+
for (let d = 0; d < 8 && frontier.length; d += 1) {
|
|
125
|
+
const next = [];
|
|
126
|
+
for (const cur of frontier) {
|
|
127
|
+
for (const childId of childrenOf.get(cur) || []) {
|
|
128
|
+
if (seen.has(childId)) continue;
|
|
129
|
+
seen.add(childId);
|
|
130
|
+
const c = byId.get(childId);
|
|
131
|
+
subs.push(c ? toIndividual(c) : { id: childId, label: childId, class: "Class", attributes: [] });
|
|
132
|
+
next.push(childId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
frontier = next;
|
|
136
|
+
}
|
|
137
|
+
return hit({ bases, subclasses: subs });
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
exports(moduleId) {
|
|
141
|
+
const ind = resolveId(moduleId);
|
|
142
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: moduleId });
|
|
143
|
+
const edges = edgesOfKind(graph, "reexports").filter((e) => e.subject === moduleId);
|
|
144
|
+
const exports = edges.map((e) =>
|
|
145
|
+
byId.get(e.object) ? toIndividual(byId.get(e.object)) : { id: e.object, label: e.objectLabel || e.object, class: "Entity", attributes: [] });
|
|
146
|
+
return hit({ exports });
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
signature(id) {
|
|
150
|
+
const ind = resolveId(id);
|
|
151
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: id });
|
|
152
|
+
const flags = [];
|
|
153
|
+
for (const [attr, name] of [["isStatic", "static"], ["isAbstract", "abstract"], ["isConstant", "constant"]]) {
|
|
154
|
+
if (attrOf(ind, attr)) flags.push(name);
|
|
155
|
+
}
|
|
156
|
+
const vis = attrOf(ind, "visibility");
|
|
157
|
+
if (vis) flags.push(vis);
|
|
158
|
+
return hit({
|
|
159
|
+
id: ind.id,
|
|
160
|
+
label: ind.label,
|
|
161
|
+
class: ind.class || "Entity",
|
|
162
|
+
params: attrOf(ind, "params"),
|
|
163
|
+
returns: attrOf(ind, "returns"),
|
|
164
|
+
raises: attrOf(ind, "raises"),
|
|
165
|
+
decorators: attrOf(ind, "decorators"),
|
|
166
|
+
doc: attrOf(ind, "doc"),
|
|
167
|
+
selfFields: attrOf(ind, "self_fields"),
|
|
168
|
+
flags,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
edges(id, kind) {
|
|
173
|
+
if (!EDGE_KINDS.includes(kind)) {
|
|
174
|
+
throw new TypeError(`edges(): unknown kind "${kind}" (not in EDGE_KINDS)`);
|
|
175
|
+
}
|
|
176
|
+
const ind = resolveId(id);
|
|
177
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: id });
|
|
178
|
+
const meta = groupMetaForKind(graph, kind);
|
|
179
|
+
const edges = edgesOfKind(graph, kind)
|
|
180
|
+
.filter((e) => e.subject === id)
|
|
181
|
+
.map((e) => toEdge(e, meta));
|
|
182
|
+
return hit({ kind, edges });
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
impact(moduleId) {
|
|
186
|
+
const ind = resolveId(moduleId);
|
|
187
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: moduleId });
|
|
188
|
+
const levels = impactClosure(graph, ind);
|
|
189
|
+
const total = levels.reduce((n, l) => n + l.length, 0);
|
|
190
|
+
return hit({ total, levels });
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
snippet(id) {
|
|
194
|
+
const ind = resolveId(id);
|
|
195
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: id });
|
|
196
|
+
const site = siteOf(ind);
|
|
197
|
+
if (!svc.sourceAccess) {
|
|
198
|
+
return miss(MISS_REASONS.NO_SOURCE, {
|
|
199
|
+
term: id,
|
|
200
|
+
detail: site ? `span is ${site.path}:${site.start}-${site.end}; this provider exposes no working tree` : "no source span in the graph",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (!site) return miss(MISS_REASONS.NO_SOURCE, { term: id, detail: "no source span in the graph (likely a module)" });
|
|
204
|
+
// A source-capable subclass overrides snippet to read the body; the graph-only
|
|
205
|
+
// base returns the span with a null body.
|
|
206
|
+
return hit({ path: site.path, span: { start: site.start, end: site.end }, body: null });
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
context(symbol) {
|
|
210
|
+
const { match } = resolveSymbol(graph, String(symbol ?? ""));
|
|
211
|
+
if (!match) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: String(symbol ?? "") });
|
|
212
|
+
return miss(MISS_REASONS.NO_SOURCE, {
|
|
213
|
+
term: String(symbol ?? ""),
|
|
214
|
+
detail: "the edit bundle reaches into the working tree; this provider exposes no source",
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
architecture({ package: pkg = "" } = {}) {
|
|
219
|
+
const norm = String(pkg || "").trim().toLowerCase().replace(/^\.?\//, "");
|
|
220
|
+
const modules = graph.individuals.filter(
|
|
221
|
+
(i) => (i.class || "") === "Module" && (!norm || String(i.label || "").toLowerCase().startsWith(norm)),
|
|
222
|
+
);
|
|
223
|
+
const pkgCount = new Map();
|
|
224
|
+
for (const m of modules) {
|
|
225
|
+
const dir = m.label.includes("/") ? m.label.slice(0, m.label.lastIndexOf("/")) : "(root)";
|
|
226
|
+
pkgCount.set(dir, (pkgCount.get(dir) || 0) + 1);
|
|
227
|
+
}
|
|
228
|
+
const modSet = new Set(modules.map((m) => m.id));
|
|
229
|
+
const inDeg = new Map();
|
|
230
|
+
for (const e of edgesOfKind(graph, "imports")) {
|
|
231
|
+
if (modSet.has(e.object)) inDeg.set(e.object, (inDeg.get(e.object) || 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
const hubs = [...inDeg.entries()]
|
|
234
|
+
.sort((a, b) => b[1] - a[1])
|
|
235
|
+
.map(([id, n]) => ({ id, label: byId.get(id)?.label || id, importers: n }));
|
|
236
|
+
const packages = [...pkgCount.entries()].sort((a, b) => b[1] - a[1]);
|
|
237
|
+
return hit({ modules: modules.length, packages, hubs });
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
untested() {
|
|
241
|
+
const covered = new Set(edgesOfKind(graph, "tests").map((e) => e.object));
|
|
242
|
+
const modules = graph.individuals
|
|
243
|
+
.filter((i) => (i.class || "") === "Module" && !covered.has(i.id) && !/\.test\./.test(i.label || ""))
|
|
244
|
+
.map(toIndividual);
|
|
245
|
+
return hit({ modules });
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
stats() {
|
|
249
|
+
const counts = new Map();
|
|
250
|
+
for (const i of graph.individuals) {
|
|
251
|
+
const c = i.class || "Entity";
|
|
252
|
+
counts.set(c, (counts.get(c) || 0) + 1);
|
|
253
|
+
}
|
|
254
|
+
const classes = [...counts.entries()]
|
|
255
|
+
.map(([cls, count]) => ({ class: cls, count }))
|
|
256
|
+
.sort((a, b) => b.count - a.count || a.class.localeCompare(b.class));
|
|
257
|
+
return hit({ total: graph.individuals.length, classes, truncated: (graph.truncated || []).length > 0 });
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
history(id) {
|
|
261
|
+
const ind = resolveId(id);
|
|
262
|
+
if (!ind) return miss(MISS_REASONS.UNRESOLVED_TERM, { term: id });
|
|
263
|
+
const touchEdges = [
|
|
264
|
+
...edgesOfKind(graph, "touches"),
|
|
265
|
+
...edgesOfKind(graph, "touchesSymbol"),
|
|
266
|
+
].filter((e) => e.object === id);
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
const commits = [];
|
|
269
|
+
for (const e of touchEdges) {
|
|
270
|
+
if (seen.has(e.subject)) continue;
|
|
271
|
+
seen.add(e.subject);
|
|
272
|
+
const c = byId.get(e.subject);
|
|
273
|
+
commits.push({
|
|
274
|
+
id: e.subject,
|
|
275
|
+
label: e.subjectLabel || c?.label || e.subject,
|
|
276
|
+
author: c ? attrOf(c, "author") ?? attrOf(c, "commitAuthor") : null,
|
|
277
|
+
date: c ? attrOf(c, "date") ?? attrOf(c, "commitDate") : null,
|
|
278
|
+
message: c ? attrOf(c, "message") ?? attrOf(c, "commitMessage") : null,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
return hit({ commits });
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
search(query, { kind = "", name = "", decorator = "" } = {}) {
|
|
285
|
+
const q = String(query || "").trim().toLowerCase();
|
|
286
|
+
const k = String(kind || "").trim().toLowerCase();
|
|
287
|
+
const nm = String(name || "").trim().toLowerCase();
|
|
288
|
+
const dec = String(decorator || "").trim().toLowerCase();
|
|
289
|
+
const results = graph.individuals
|
|
290
|
+
.filter((i) => {
|
|
291
|
+
const label = String(i.label || "").toLowerCase();
|
|
292
|
+
if (k && (i.class || "").toLowerCase() !== k) return false;
|
|
293
|
+
if (nm && !label.includes(nm)) return false;
|
|
294
|
+
if (dec && !String(attrOf(i, "decorators") || "").toLowerCase().includes(dec)) return false;
|
|
295
|
+
if (q && !label.includes(q)) return false;
|
|
296
|
+
return true;
|
|
297
|
+
})
|
|
298
|
+
.map(toIndividual);
|
|
299
|
+
return hit({ results });
|
|
300
|
+
},
|
|
301
|
+
|
|
302
|
+
ask(query) {
|
|
303
|
+
const { content, tmct_ask } = ask(graph, String(query || ""));
|
|
304
|
+
return hit({ content, tmct_ask });
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return svc;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** The source-reaching services a graph-only provider satisfies with NO_SOURCE. */
|
|
312
|
+
export { SOURCE_SERVICES };
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
// The Repository Interface — tmct's OWNED, versioned contract between "interpret
|
|
2
|
+
// the query" (tmct, the brittle side) and "ask the graph for truth" (a provider,
|
|
3
|
+
// the stable side). PLAN_REPOSITORY_INTERFACE.md.
|
|
4
|
+
//
|
|
5
|
+
// tmct defines and versions this shape; a provider (seonix, a fixture, a browser
|
|
6
|
+
// page) IMPLEMENTS it over its native graph. Both sides already agree on the
|
|
7
|
+
// TYPES — every Individual.class is a `tmct:` class and every Edge.predicate a
|
|
8
|
+
// `tmct:` object property (ontology/tmct-core.ttl is the type dictionary) — so
|
|
9
|
+
// the interface only names the OPERATIONS over those shared types.
|
|
10
|
+
//
|
|
11
|
+
// The error contract is the load-bearing rule: a clean miss is a first-class
|
|
12
|
+
// RETURN VALUE from a small CLOSED set of reasons (MISS_REASONS), never a throw.
|
|
13
|
+
// Presentation (the render* layer, the chat surface) lives on tmct's side of the
|
|
14
|
+
// seam and turns these typed results into prose; the interface returns data.
|
|
15
|
+
//
|
|
16
|
+
// This module is pure data + tiny constructors — no fs, no graph, no LLM.
|
|
17
|
+
|
|
18
|
+
/** SemVer of the interface. Additive-by-default: new services / optional args are
|
|
19
|
+
* minor bumps; the suite for version N stays green under N+1. A breaking change
|
|
20
|
+
* is a new MAJOR with its own suite (see the versioning policy in the plan). */
|
|
21
|
+
export const INTERFACE_VERSION = "1.0.0";
|
|
22
|
+
|
|
23
|
+
/** The OWL vocabulary the types are grounded in. */
|
|
24
|
+
export const ONTOLOGY_IRI = "urn:tmct:core";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The CLOSED set of miss reasons. A service that cannot answer returns
|
|
28
|
+
* `miss(reason, detail)` with `reason` drawn from exactly these — the interpreter
|
|
29
|
+
* renders the reason, never free-text error prose.
|
|
30
|
+
*
|
|
31
|
+
* - UNRESOLVED_TERM the term named no individual in the graph.
|
|
32
|
+
* - CAPABILITY_ABSENT the provider does not implement this service (negotiated away).
|
|
33
|
+
* - TRUNCATED_GRAPH the answer exists but the provider shipped a truncated sample.
|
|
34
|
+
* - NO_SOURCE a source-reaching service, but the provider exposes no working tree.
|
|
35
|
+
*/
|
|
36
|
+
export const MISS_REASONS = Object.freeze({
|
|
37
|
+
UNRESOLVED_TERM: "UNRESOLVED_TERM",
|
|
38
|
+
CAPABILITY_ABSENT: "CAPABILITY_ABSENT",
|
|
39
|
+
TRUNCATED_GRAPH: "TRUNCATED_GRAPH",
|
|
40
|
+
NO_SOURCE: "NO_SOURCE",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** The closed vocabulary of edge kinds `edges(id, kind)` traverses, each aligned
|
|
44
|
+
* to a `tmct:` object property (ontology/tmct-core.ttl). Symbol-granular kinds
|
|
45
|
+
* (callsSymbol/touchesSymbol) stay separate from module-coarse calls/touches. */
|
|
46
|
+
export const EDGE_KINDS = Object.freeze([
|
|
47
|
+
"imports", "calls", "callsSymbol", "defines", "tests",
|
|
48
|
+
"touches", "touchesSymbol", "contains", "inherits", "cochange", "reexports",
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
/** edge-kind → the `tmct:` object property it realizes (the OWL grounding). */
|
|
52
|
+
export const EDGE_KIND_TO_TMCT = Object.freeze({
|
|
53
|
+
imports: "tmct:imports",
|
|
54
|
+
calls: "tmct:calls",
|
|
55
|
+
callsSymbol: "tmct:calls",
|
|
56
|
+
defines: "tmct:defines",
|
|
57
|
+
tests: "tmct:covers",
|
|
58
|
+
touches: "tmct:touches",
|
|
59
|
+
touchesSymbol: "tmct:touches",
|
|
60
|
+
contains: "tmct:contains",
|
|
61
|
+
inherits: "tmct:extends",
|
|
62
|
+
cochange: "tmct:dependsOn",
|
|
63
|
+
reexports: "tmct:exports",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** The named services, grouped as in the plan's six-group inventory. Names are
|
|
67
|
+
* the interface's stable identifiers; a provider advertises which it implements
|
|
68
|
+
* as its `capabilities`. `source: true` marks the working-tree-reaching services
|
|
69
|
+
* a graph-only provider may satisfy with an honest NO_SOURCE miss. */
|
|
70
|
+
export const SERVICE_GROUPS = Object.freeze({
|
|
71
|
+
resolution: ["resolve", "describe", "members", "subclasses", "exports", "signature"],
|
|
72
|
+
traversal: ["edges", "impact"],
|
|
73
|
+
source: ["snippet", "context"],
|
|
74
|
+
aggregate: ["architecture", "untested", "stats"],
|
|
75
|
+
temporal: ["history"],
|
|
76
|
+
search: ["search", "ask"],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
/** Flat list of every service name. */
|
|
80
|
+
export const SERVICES = Object.freeze(
|
|
81
|
+
Object.values(SERVICE_GROUPS).flat(),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/** The subset that reaches past the graph into a working tree. */
|
|
85
|
+
export const SOURCE_SERVICES = Object.freeze(new Set(SERVICE_GROUPS.source));
|
|
86
|
+
|
|
87
|
+
// ---- result constructors — the honest-miss ethos as tiny values --------------
|
|
88
|
+
|
|
89
|
+
/** A successful result carrying typed `value`. */
|
|
90
|
+
export function hit(value) {
|
|
91
|
+
return { ok: true, value };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A first-class miss: `reason` ∈ MISS_REASONS, optional `detail` (free text for
|
|
95
|
+
* the human render) and `term` (what was looked up). Never thrown. */
|
|
96
|
+
export function miss(reason, { detail = "", term = null } = {}) {
|
|
97
|
+
if (!MISS_REASONS[reason]) {
|
|
98
|
+
throw new TypeError(`miss(): unknown reason "${reason}" (not in MISS_REASONS)`);
|
|
99
|
+
}
|
|
100
|
+
return { ok: false, miss: { reason, detail, term } };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const isHit = (r) => Boolean(r && r.ok === true);
|
|
104
|
+
export const isMiss = (r) => Boolean(r && r.ok === false);
|
|
105
|
+
|
|
106
|
+
// ---- type projections — raw graph rows → the interface's shared shapes --------
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Project a raw graph individual to the interface `Individual`:
|
|
110
|
+
* { id, label, class, attributes: [{ key, value, prop }] }
|
|
111
|
+
* `class` is a `tmct:` class token; `attributes[].prop` is the SEON/mgx token the
|
|
112
|
+
* value was asserted under (OWL grounding preserved). Pure; tolerant of partials.
|
|
113
|
+
* @returns {Individual|null}
|
|
114
|
+
*/
|
|
115
|
+
export function toIndividual(ind) {
|
|
116
|
+
if (!ind || !ind.id) return null;
|
|
117
|
+
return {
|
|
118
|
+
id: String(ind.id),
|
|
119
|
+
label: ind.label != null ? String(ind.label) : String(ind.id),
|
|
120
|
+
class: ind.class || "Entity",
|
|
121
|
+
attributes: (Array.isArray(ind.attributes) ? ind.attributes : []).map((a) => ({
|
|
122
|
+
key: a.key,
|
|
123
|
+
value: a.value,
|
|
124
|
+
prop: a.prop || null,
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Project a raw relation edge to the interface `Edge`:
|
|
131
|
+
* { subject, object, predicate, prop, subjectLabel?, objectLabel?, weight? }
|
|
132
|
+
* The required quartet is subject/object/predicate/prop; labels/weight are
|
|
133
|
+
* additive conveniences a provider MAY carry.
|
|
134
|
+
* @returns {Edge}
|
|
135
|
+
*/
|
|
136
|
+
export function toEdge(rawEdge, { predicate = "", prop = null } = {}) {
|
|
137
|
+
const e = {
|
|
138
|
+
subject: rawEdge.subject,
|
|
139
|
+
object: rawEdge.object,
|
|
140
|
+
predicate,
|
|
141
|
+
prop: prop || null,
|
|
142
|
+
};
|
|
143
|
+
if (rawEdge.subjectLabel != null) e.subjectLabel = rawEdge.subjectLabel;
|
|
144
|
+
if (rawEdge.objectLabel != null) e.objectLabel = rawEdge.objectLabel;
|
|
145
|
+
if (rawEdge.weight != null) e.weight = rawEdge.weight;
|
|
146
|
+
return e;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- capability negotiation — CAPABILITY_ABSENT as a value, not a wall --------
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Invoke a service through capability negotiation. If the provider does not
|
|
153
|
+
* advertise `service` in `svc.capabilities`, return `miss(CAPABILITY_ABSENT)`
|
|
154
|
+
* rather than throwing — the interpreter degrades the query, never errors. When
|
|
155
|
+
* present, calls `svc[service](...args)` and returns its result verbatim.
|
|
156
|
+
*
|
|
157
|
+
* This is how a host calls an interface it cannot assume is complete; the
|
|
158
|
+
* reference providers implement every service, so they never trip it.
|
|
159
|
+
*/
|
|
160
|
+
export function invoke(svc, service, ...args) {
|
|
161
|
+
const caps = svc && svc.capabilities;
|
|
162
|
+
const has = Array.isArray(caps) ? caps.includes(service) : caps instanceof Set ? caps.has(service) : false;
|
|
163
|
+
if (!has || typeof svc[service] !== "function") {
|
|
164
|
+
return miss(MISS_REASONS.CAPABILITY_ABSENT, { detail: `service "${service}" is not implemented by this provider` });
|
|
165
|
+
}
|
|
166
|
+
return svc[service](...args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- the MACHINE-READABLE SHAPE ----------------------------------------------
|
|
170
|
+
// A single JSON-serializable object enumerating every service, its args, result
|
|
171
|
+
// type, and possible misses. tmct OWNS and versions this; docs/repository-
|
|
172
|
+
// interface.md is the prose peer and docs/repository-interface.schema.json is the
|
|
173
|
+
// committed serialization (the contract suite asserts they agree — no drift).
|
|
174
|
+
|
|
175
|
+
const IND = "Individual";
|
|
176
|
+
const EDGE = "Edge";
|
|
177
|
+
|
|
178
|
+
/** Concurrency note shared by every read service: safe across handles — the graph
|
|
179
|
+
* is read-only truth and the only mutable state is the caller-owned session
|
|
180
|
+
* handle, never touched here. Proven by the suite's concurrent-session cases. */
|
|
181
|
+
const CONCURRENT_SAFE = "concurrent-safe: reads immutable graph truth; no shared mutable state";
|
|
182
|
+
|
|
183
|
+
export const REPOSITORY_INTERFACE = Object.freeze({
|
|
184
|
+
version: INTERFACE_VERSION,
|
|
185
|
+
ontology: ONTOLOGY_IRI,
|
|
186
|
+
missReasons: Object.values(MISS_REASONS),
|
|
187
|
+
edgeKinds: [...EDGE_KINDS],
|
|
188
|
+
types: {
|
|
189
|
+
Individual: {
|
|
190
|
+
fields: {
|
|
191
|
+
id: "string (opaque provider id)",
|
|
192
|
+
label: "string (human/display name)",
|
|
193
|
+
class: "string — a tmct: class token (module|class|function|method|attribute|variable|commit|test|…)",
|
|
194
|
+
attributes: "Array<{ key: string, value: string, prop: string|null }> — prop is the SEON/mgx token grounding the value",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
Edge: {
|
|
198
|
+
fields: {
|
|
199
|
+
subject: "string (individual id)",
|
|
200
|
+
object: "string (individual id)",
|
|
201
|
+
predicate: "string — the relation predicate",
|
|
202
|
+
prop: "string|null — the SEON/mgx property token; see edgeKinds → tmct: mapping",
|
|
203
|
+
subjectLabel: "string? (additive)",
|
|
204
|
+
objectLabel: "string? (additive)",
|
|
205
|
+
weight: "number? (additive; e.g. cochange coupling)",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
Result: { shape: "{ ok: true, value: T } | { ok: false, miss: Miss }" },
|
|
209
|
+
Miss: { shape: "{ reason: MISS_REASONS, detail: string, term: string|null }" },
|
|
210
|
+
},
|
|
211
|
+
capabilitiesModel:
|
|
212
|
+
"A provider advertises `capabilities: string[]` (service names it implements). " +
|
|
213
|
+
"A service outside the set is negotiated away to miss(CAPABILITY_ABSENT) — never an error. " +
|
|
214
|
+
"Source services (snippet, context) may be advertised yet answer miss(NO_SOURCE) when no working tree exists.",
|
|
215
|
+
services: {
|
|
216
|
+
resolve: {
|
|
217
|
+
group: "resolution", args: { term: "string" },
|
|
218
|
+
result: `{ match: ${IND}, candidates: ${IND}[] }`,
|
|
219
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
220
|
+
purpose: "The resolveSymbol seam: map a free term to an individual + runner-up candidates. Every id-taking service consumes a resolved id.",
|
|
221
|
+
},
|
|
222
|
+
describe: {
|
|
223
|
+
group: "resolution", args: { id: "string" },
|
|
224
|
+
result: `{ individual: ${IND}, out: ${EDGE}[], incoming: ${EDGE}[] }`,
|
|
225
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
226
|
+
purpose: "Full typed portrait of one individual: its attributes and its outgoing/incoming edges.",
|
|
227
|
+
},
|
|
228
|
+
members: {
|
|
229
|
+
group: "resolution", args: { classId: "string" },
|
|
230
|
+
result: `{ methods: ${IND}[], attributes: ${IND}[] }`,
|
|
231
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
232
|
+
purpose: "A class's methods + attributes (via the contains relation) — replaces reading the class body.",
|
|
233
|
+
},
|
|
234
|
+
subclasses: {
|
|
235
|
+
group: "resolution", args: { classId: "string" },
|
|
236
|
+
result: `{ bases: ${IND}[], subclasses: ${IND}[] }`,
|
|
237
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
238
|
+
purpose: "Forward bases + the transitive reverse inheritance closure (who extends this).",
|
|
239
|
+
},
|
|
240
|
+
exports: {
|
|
241
|
+
group: "resolution", args: { moduleId: "string" },
|
|
242
|
+
result: `{ exports: ${IND}[] }`,
|
|
243
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
244
|
+
purpose: "A module's curated public API (resolved __all__ / re-exports).",
|
|
245
|
+
},
|
|
246
|
+
signature: {
|
|
247
|
+
group: "resolution", args: { id: "string" },
|
|
248
|
+
result: "{ id, label, class, params, returns, raises, decorators, doc, selfFields, flags }",
|
|
249
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
250
|
+
purpose: "The compact API surface of a symbol without its body.",
|
|
251
|
+
},
|
|
252
|
+
edges: {
|
|
253
|
+
group: "traversal", args: { id: "string", kind: "EDGE_KINDS member" },
|
|
254
|
+
result: `{ kind: string, edges: ${EDGE}[] }`,
|
|
255
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
256
|
+
purpose: "Outgoing edges of one closed kind from an individual (an honest empty array is not a miss).",
|
|
257
|
+
note: "An unknown kind (outside EDGE_KINDS) is a programming error and throws TypeError, not a miss.",
|
|
258
|
+
},
|
|
259
|
+
impact: {
|
|
260
|
+
group: "traversal", args: { moduleId: "string" },
|
|
261
|
+
result: `{ total: number, levels: Array<Array<{ id, label, via, tests }>> }`,
|
|
262
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
263
|
+
purpose: "The transitive dependent closure the interpreter cannot compute without provider truth.",
|
|
264
|
+
},
|
|
265
|
+
snippet: {
|
|
266
|
+
group: "source", args: { id: "string" },
|
|
267
|
+
result: "{ path: string, span: { start, end }, body: string|null }",
|
|
268
|
+
misses: ["UNRESOLVED_TERM", "NO_SOURCE"], concurrency: CONCURRENT_SAFE,
|
|
269
|
+
purpose: "The exact source span of a symbol. A provider with no working tree returns miss(NO_SOURCE) honestly.",
|
|
270
|
+
},
|
|
271
|
+
context: {
|
|
272
|
+
group: "source", args: { symbol: "string", depth: "min|auto|full?" },
|
|
273
|
+
result: "{ text: string, tier: string } (a sized edit bundle)",
|
|
274
|
+
misses: ["UNRESOLVED_TERM", "NO_SOURCE"], concurrency: CONCURRENT_SAFE,
|
|
275
|
+
purpose: "The composed edit bundle (exemplar, siblings, registration, insertion region). Source-reaching.",
|
|
276
|
+
},
|
|
277
|
+
architecture: {
|
|
278
|
+
group: "aggregate", args: { package: "string?" },
|
|
279
|
+
result: "{ modules: number, packages: Array<[dir, count]>, hubs: Array<{ id, label, importers }> }",
|
|
280
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
281
|
+
purpose: "Package/module shape + the most-imported hub modules. Optional package prefix scopes it.",
|
|
282
|
+
},
|
|
283
|
+
untested: {
|
|
284
|
+
group: "aggregate", args: {},
|
|
285
|
+
result: `{ modules: ${IND}[] }`,
|
|
286
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
287
|
+
purpose: "Modules with no covering test module (via the tests relation).",
|
|
288
|
+
},
|
|
289
|
+
stats: {
|
|
290
|
+
group: "aggregate", args: {},
|
|
291
|
+
result: "{ total: number, classes: Array<{ class: string, count: number }>, truncated: boolean }",
|
|
292
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
293
|
+
purpose: "Per-tmct:class individual counts, read straight from the payload.",
|
|
294
|
+
},
|
|
295
|
+
history: {
|
|
296
|
+
group: "temporal", args: { id: "string" },
|
|
297
|
+
result: "{ commits: Array<{ id, label, author, date, message }> }",
|
|
298
|
+
misses: ["UNRESOLVED_TERM"], concurrency: CONCURRENT_SAFE,
|
|
299
|
+
purpose: "The commits that touched an individual (tmct:commit individuals via tmct:touches).",
|
|
300
|
+
},
|
|
301
|
+
search: {
|
|
302
|
+
group: "search", args: { query: "string", kind: "string?", name: "string?", decorator: "string?" },
|
|
303
|
+
result: `{ results: ${IND}[] }`,
|
|
304
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
305
|
+
purpose: "Lexical, provider-local locate. An empty result set is honest, not a miss.",
|
|
306
|
+
},
|
|
307
|
+
ask: {
|
|
308
|
+
group: "search", args: { query: "string" },
|
|
309
|
+
result: "{ content: string, tmct_ask: object } (the composed NL round-trip envelope)",
|
|
310
|
+
misses: [], concurrency: CONCURRENT_SAFE,
|
|
311
|
+
purpose: "The mechanical, zero-model NL query over the graph — tmct's whole reason to exist.",
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
/** Freeze-deep helper is unnecessary; REPOSITORY_INTERFACE is treated as read-only
|
|
317
|
+
* data. Exported so the contract suite and docs generator share one source. */
|
|
318
|
+
export default REPOSITORY_INTERFACE;
|