@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.
Files changed (53) hide show
  1. package/README.md +77 -3
  2. package/ROADMAP.md +416 -3
  3. package/bin/tmct.mjs +308 -12
  4. package/corpus/README.md +52 -0
  5. package/corpus/conceptnet/LICENSE-NOTICE +37 -0
  6. package/corpus/conceptnet/README.md +103 -0
  7. package/corpus/conceptnet/fetch-slice.mjs +136 -0
  8. package/corpus/conceptnet/filter-dump.mjs +89 -0
  9. package/corpus/conceptnet/slice.jsonl +14258 -0
  10. package/data/phrasebook/software-phrases.txt +231 -0
  11. package/data/templates/grammar-rules.toml +89 -0
  12. package/data/templates/responses.jsonl +68 -0
  13. package/package.json +40 -3
  14. package/src/ask-nlp.mjs +22 -10
  15. package/src/ask-vocab.mjs +35 -1
  16. package/src/ask.mjs +171 -494
  17. package/src/chat.mjs +709 -81
  18. package/src/corpus/conceptnet-map.toml +251 -0
  19. package/src/corpus/conceptnet.mjs +167 -0
  20. package/src/corpus/templates.mjs +188 -0
  21. package/src/finish.mjs +443 -0
  22. package/src/grammar/ace.mjs +341 -0
  23. package/src/grammar/assert.mjs +40 -0
  24. package/src/grammar/lexicon-core.json +287 -0
  25. package/src/grammar/lexicon.mjs +202 -0
  26. package/src/hash.mjs +32 -0
  27. package/src/index.mjs +21 -5
  28. package/src/init.mjs +264 -0
  29. package/src/interpret/fuzzy.mjs +89 -0
  30. package/src/interpret/merge.mjs +148 -0
  31. package/src/interpret/normalize.mjs +151 -0
  32. package/src/interpret/pipeline.mjs +112 -0
  33. package/src/interpret/strategies/grammar.mjs +137 -0
  34. package/src/interpret/strategies/keywords.mjs +241 -0
  35. package/src/interpret/strategies/noise-strip.mjs +114 -0
  36. package/src/memory/blocks.mjs +221 -0
  37. package/src/memory/core.mjs +533 -0
  38. package/src/memory/fold.mjs +0 -0
  39. package/src/memory/inspect.mjs +141 -0
  40. package/src/memory/trust.mjs +113 -0
  41. package/src/prose-nlp.mjs +14 -16
  42. package/src/providers/bootstrap.mjs +24 -0
  43. package/src/providers/fixture.mjs +118 -0
  44. package/src/providers/graph-service.mjs +312 -0
  45. package/src/repository-interface.mjs +318 -0
  46. package/src/server.mjs +44 -28
  47. package/src/sessions.mjs +137 -4
  48. package/src/source.mjs +44 -5
  49. package/src/syllogise.mjs +0 -0
  50. package/src/toml-config.mjs +14 -0
  51. package/src/tui/app.mjs +173 -0
  52. package/src/wink-model.mjs +74 -0
  53. 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;