@polycode-projects/the-mechanical-code-talker 0.3.0 → 0.5.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/src/prose-nlp.mjs CHANGED
@@ -1,23 +1,23 @@
1
1
  // prose-nlp.mjs — the OPTIONAL wink-nlp lemma loader behind prose.mjs's LEMMA layer.
2
2
  //
3
- // Deliberately a SEPARATE loader from ask-nlp.mjs (same createRequire pattern, same
4
- // deps) rather than an import of it: ask-nlp.mjs belongs to the ask-engine surface
5
- // and is under active concurrent work the prose pre-pass must not couple its
6
- // index-build path to that file's export shape. The ~20 duplicated lines are the
7
- // decoupling fee; the wink model itself is loaded lazily and at most once per
8
- // process either way.
3
+ // Kept SEPARATE from ask-nlp.mjs (its own `proseLemma` export shape) rather than an
4
+ // import of that ask-engine surface but both now share the neutral leaf loader
5
+ // src/wink-model.mjs, so the wink model is resolved in ONE place. The former ~20
6
+ // duplicated createRequire lines are gone; the coupling this file avoids is to
7
+ // ask-nlp.mjs's export shape, not to a leaf model loader.
9
8
  //
10
- // BOUNDARY (same as ask-nlp.mjs, hard): Node-only, never inlined into the viewer
11
- // bundle. prose.mjs is itself never inlined by viz.mjs's askSource(), so nothing
12
- // browser-side can reach this module. wink-nlp + wink-eng-lite-web-model are CJS
13
- // loaded via createRequire, lazily, with failure cached as null: a checkout without
14
- // the optional deps simply builds no lemma layer (honestly absent), it never throws.
9
+ // BOUNDARY (same as ask-nlp.mjs, hard): Node-only path, never inlined into the
10
+ // viewer bundle. prose.mjs is itself never inlined by viz.mjs's askSource(), so
11
+ // nothing browser-side can reach this module. The wink pair is loaded lazily (Node
12
+ // createRequire fallback, or the browser registration seam), failure cached as null:
13
+ // a checkout without the optional deps simply builds no lemma layer (honestly
14
+ // absent), it never throws.
15
15
  //
16
16
  // Determinism: wink's lemmatiser is a fixed trained model with no sampling — the
17
17
  // same token always yields the same lemma across runs and processes, which is what
18
18
  // lets the lemma layer meet the "byte-identical proseIndex across builds" contract.
19
19
 
20
- import { createRequire } from "node:module";
20
+ import { winkInstance } from "./wink-model.mjs";
21
21
 
22
22
  let cached; // undefined = not tried yet; null = unavailable (tried once, honestly off)
23
23
 
@@ -27,10 +27,8 @@ let cached; // undefined = not tried yet; null = unavailable (tried once, honest
27
27
  export function proseLemma() {
28
28
  if (cached !== undefined) return cached;
29
29
  try {
30
- const require = createRequire(import.meta.url);
31
- const winkNLP = require("wink-nlp");
32
- const model = require("wink-eng-lite-web-model");
33
- const nlp = winkNLP(model);
30
+ const nlp = winkInstance();
31
+ if (!nlp) { cached = null; return cached; }
34
32
  const its = nlp.its;
35
33
  const memo = new Map();
36
34
  cached = (word) => {
@@ -0,0 +1,24 @@
1
+ // The BOOTSTRAP reference provider — the empty/degenerate graph a fresh repo
2
+ // "contains" before anything is indexed. PLAN_REPOSITORY_INTERFACE.md deliverable
3
+ // 2: "bootstrap returns honest empties".
4
+ //
5
+ // It implements every Repository-Interface service over the empty bootstrap
6
+ // payload (src/source.mjs emptyEntities): every id-taking service returns
7
+ // miss(UNRESOLVED_TERM) — there are no individuals — and every aggregate returns
8
+ // an honest empty (stats.total = 0, untested.modules = [], …). Nothing throws.
9
+ // This is the other end of the compatibility kit: the provider that has no data
10
+ // must still CONFORM.
11
+
12
+ import { parseEntities } from "../codegraph.mjs";
13
+ import { emptyEntities } from "../source.mjs";
14
+ import { createGraphService } from "./graph-service.mjs";
15
+
16
+ /** The parsed empty bootstrap graph. */
17
+ export function bootstrapGraph() {
18
+ return parseEntities(emptyEntities());
19
+ }
20
+
21
+ /** The bootstrap provider: every service over the empty graph — honest empties. */
22
+ export function bootstrapProvider() {
23
+ return createGraphService(bootstrapGraph());
24
+ }
@@ -0,0 +1,118 @@
1
+ // The FIXTURE reference provider — a small, real, self-contained code graph that
2
+ // implements every Repository-Interface service. PLAN_REPOSITORY_INTERFACE.md
3
+ // deliverable 2: "the executable specification an external producer reads first".
4
+ //
5
+ // It is a degenerate provider in the sense that its graph is tiny and its source
6
+ // bodies are absent (snippet/context answer NO_SOURCE) — but every OTHER service
7
+ // returns real graph truth. The contract suite (test/repository-interface.test.mjs)
8
+ // runs the whole compatibility kit against it.
9
+ //
10
+ // The payload is embedded (not read from test/) so this ships as a runnable spec
11
+ // inside the library. Its shape is exactly a parseEntities() input.
12
+
13
+ import { parseEntities } from "../codegraph.mjs";
14
+ import { createGraphService } from "./graph-service.mjs";
15
+
16
+ /** A compact but type-complete entities payload: modules, a class hierarchy
17
+ * (Base ← Widget ← Button), a method with a full signature, an attribute, a
18
+ * module global, and a commit — wired by one edge of every closed kind. */
19
+ export const FIXTURE_ENTITIES = Object.freeze({
20
+ generated_at: "2026-07-05T00:00:00.000Z",
21
+ bootstrap: false,
22
+ prefixes: { seon: "http://se-on.org/ontologies/seon.owl#", mgx: "urn:tmct:mgx#" },
23
+ classes: [
24
+ { name: "Module", count: 5, sample: ["pkg/core/graph.mjs"] },
25
+ { name: "Class", count: 3, sample: ["Base", "Widget", "Button"] },
26
+ { name: "Method", count: 1, sample: ["Widget.render"] },
27
+ { name: "Attribute", count: 1, sample: ["Widget.name"] },
28
+ { name: "Function", count: 1, sample: ["parseNode"] },
29
+ { name: "Commit", count: 1, sample: ["a1b2c3d"] },
30
+ ],
31
+ vocabulary: [],
32
+ objectProperties: [
33
+ { predicate: "imports", prop: "mgx:importsNamespace", count: 3, examples: [
34
+ { subject: "mod:view.mjs", object: "mod:graph.mjs", subjectLabel: "pkg/ui/view.mjs", objectLabel: "pkg/core/graph.mjs" },
35
+ { subject: "mod:widget.mjs", object: "mod:graph.mjs", subjectLabel: "pkg/ui/widget.mjs", objectLabel: "pkg/core/graph.mjs" },
36
+ { subject: "mod:button.mjs", object: "mod:widget.mjs", subjectLabel: "pkg/ui/button.mjs", objectLabel: "pkg/ui/widget.mjs" },
37
+ ] },
38
+ { predicate: "calls", prop: "mgx:callsCoarse", count: 1, examples: [
39
+ { subject: "mod:script.mjs", object: "mod:graph.mjs", subjectLabel: "scripts/build.mjs", objectLabel: "pkg/core/graph.mjs" },
40
+ ] },
41
+ { predicate: "callsSymbol", prop: "mgx:callsSymbol", count: 1, examples: [
42
+ { subject: "m:render", object: "fn:parseNode", subjectLabel: "Widget.render", objectLabel: "parseNode" },
43
+ ] },
44
+ { predicate: "defines", prop: "seon:declaresMethod", count: 3, examples: [
45
+ { subject: "mod:graph.mjs", object: "fn:parseNode", subjectLabel: "pkg/core/graph.mjs", objectLabel: "parseNode" },
46
+ { subject: "mod:widget.mjs", object: "cls:widget", subjectLabel: "pkg/ui/widget.mjs", objectLabel: "Widget" },
47
+ { subject: "mod:widget.mjs", object: "g:register", subjectLabel: "pkg/ui/widget.mjs", objectLabel: "register" },
48
+ ] },
49
+ { predicate: "tests", prop: "mgx:testsCoverage", count: 1, examples: [
50
+ { subject: "mod:widget.test.mjs", object: "mod:widget.mjs", subjectLabel: "pkg/test/widget.test.mjs", objectLabel: "pkg/ui/widget.mjs" },
51
+ ] },
52
+ { predicate: "touches", prop: "mgx:touchedByCommit", count: 1, examples: [
53
+ { subject: "commit:a1b2c3d", object: "mod:widget.mjs", subjectLabel: "a1b2c3d", objectLabel: "pkg/ui/widget.mjs" },
54
+ ] },
55
+ { predicate: "touchesSymbol", prop: "mgx:touchesSymbol", count: 1, examples: [
56
+ { subject: "commit:a1b2c3d", object: "m:render", subjectLabel: "a1b2c3d", objectLabel: "Widget.render" },
57
+ ] },
58
+ { predicate: "contains", prop: "seon:containsCodeEntity", count: 2, examples: [
59
+ { subject: "cls:widget", object: "m:render", subjectLabel: "Widget", objectLabel: "render" },
60
+ { subject: "cls:widget", object: "a:name", subjectLabel: "Widget", objectLabel: "name" },
61
+ ] },
62
+ { predicate: "inherits", prop: "seon:hasSuperType", count: 2, examples: [
63
+ { subject: "cls:widget", object: "cls:base", subjectLabel: "Widget", objectLabel: "Base" },
64
+ { subject: "cls:button", object: "cls:widget", subjectLabel: "Button", objectLabel: "Widget" },
65
+ ] },
66
+ { predicate: "cochange", prop: "mgx:changeCoupledWith", count: 1, examples: [
67
+ { subject: "mod:widget.mjs", object: "mod:graph.mjs", subjectLabel: "pkg/ui/widget.mjs", objectLabel: "pkg/core/graph.mjs", weight: 3 },
68
+ ] },
69
+ { predicate: "reexports", prop: "mgx:reExports", count: 1, examples: [
70
+ { subject: "mod:widget.mjs", object: "cls:widget", subjectLabel: "pkg/ui/widget.mjs", objectLabel: "Widget" },
71
+ ] },
72
+ ],
73
+ individuals: [
74
+ { id: "mod:graph.mjs", label: "pkg/core/graph.mjs", class: "Module", derived_from: ["git:a1b2c3d"], mentions: [] },
75
+ { id: "mod:view.mjs", label: "pkg/ui/view.mjs", class: "Module", derived_from: [], mentions: [] },
76
+ { id: "mod:widget.mjs", label: "pkg/ui/widget.mjs", class: "Module", derived_from: ["git:a1b2c3d"], mentions: [] },
77
+ { id: "mod:button.mjs", label: "pkg/ui/button.mjs", class: "Module", derived_from: [], mentions: [] },
78
+ { id: "mod:script.mjs", label: "scripts/build.mjs", class: "Module", derived_from: [], mentions: [] },
79
+ { id: "mod:widget.test.mjs", label: "pkg/test/widget.test.mjs", class: "Module", derived_from: [], mentions: [] },
80
+ { id: "fn:parseNode", label: "parseNode", class: "Function", derived_from: [], mentions: [], attributes: [
81
+ { prop: "seon:startsAt", key: "site", value: "pkg/core/graph.mjs:10-24" },
82
+ { prop: "seon:hasParameter", key: "params", value: "node, depth=0" },
83
+ { prop: "seon:hasReturnType", key: "returns", value: "Node" },
84
+ ] },
85
+ { id: "cls:base", label: "Base", class: "Class", derived_from: [], mentions: [], attributes: [{ prop: "seon:startsAt", key: "site", value: "pkg/core/graph.mjs:1-6" }] },
86
+ { id: "cls:widget", label: "Widget", class: "Class", derived_from: [], mentions: [], attributes: [{ prop: "seon:startsAt", key: "site", value: "pkg/ui/widget.mjs:1-40" }] },
87
+ { id: "cls:button", label: "Button", class: "Class", derived_from: [], mentions: [], attributes: [{ prop: "seon:startsAt", key: "site", value: "pkg/ui/button.mjs:1-12" }] },
88
+ { id: "m:render", label: "Widget.render", class: "Method", derived_from: [], mentions: [], attributes: [
89
+ { prop: "seon:startsAt", key: "site", value: "pkg/ui/widget.mjs:8-20" },
90
+ { prop: "mgx:decorator", key: "decorators", value: "property" },
91
+ { prop: "seon:hasParameter", key: "params", value: "self, mode='full'" },
92
+ { prop: "seon:hasReturnType", key: "returns", value: "str" },
93
+ { prop: "seon:throwsException", key: "raises", value: "ValueError" },
94
+ { prop: "seon:accessesField", key: "self_fields", value: "name, size" },
95
+ { prop: "seon:hasDoc", key: "doc", value: "Render the widget." },
96
+ ] },
97
+ { id: "a:name", label: "Widget.name", class: "Attribute", derived_from: [], mentions: [], attributes: [{ prop: "seon:startsAt", key: "site", value: "pkg/ui/widget.mjs:2" }] },
98
+ { id: "g:register", label: "register", class: "GlobalVariable", derived_from: [], mentions: [], attributes: [
99
+ { prop: "seon:startsAt", key: "site", value: "pkg/ui/widget.mjs:1" },
100
+ { prop: "mgx:value", key: "value", value: "Library()" },
101
+ ] },
102
+ { id: "commit:a1b2c3d", label: "a1b2c3d", class: "Commit", derived_from: [], mentions: [], attributes: [
103
+ { prop: "mgx:commitAuthor", key: "author", value: "Grace Hopper" },
104
+ { prop: "mgx:commitDate", key: "date", value: "2026-07-01" },
105
+ { prop: "mgx:commitMessage", key: "message", value: "render the widget in full mode" },
106
+ ] },
107
+ ],
108
+ });
109
+
110
+ /** The parsed fixture graph (shared, immutable truth). */
111
+ export function fixtureGraph() {
112
+ return parseEntities(FIXTURE_ENTITIES);
113
+ }
114
+
115
+ /** The fixture provider: a Repository-Interface service over the small real graph. */
116
+ export function fixtureProvider() {
117
+ return createGraphService(fixtureGraph());
118
+ }
@@ -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 };