@polycode-projects/the-mechanical-code-talker 0.3.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 +411 -1
- package/bin/tmct.mjs +56 -1
- package/data/templates/grammar-rules.toml +89 -0
- package/data/templates/responses.jsonl +13 -0
- package/package.json +30 -2
- package/src/ask-nlp.mjs +8 -10
- package/src/ask-vocab.mjs +22 -0
- package/src/ask.mjs +80 -2
- package/src/chat.mjs +576 -50
- package/src/corpus/conceptnet.mjs +14 -2
- package/src/corpus/templates.mjs +94 -10
- package/src/finish.mjs +443 -0
- package/src/hash.mjs +32 -0
- package/src/init.mjs +264 -0
- package/src/interpret/normalize.mjs +34 -0
- package/src/interpret/strategies/keywords.mjs +57 -1
- package/src/memory/blocks.mjs +23 -3
- package/src/memory/core.mjs +257 -16
- 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 +13 -2
- package/src/syllogise.mjs +0 -0
- package/src/toml-config.mjs +14 -0
- package/src/wink-model.mjs +74 -0
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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
11
|
-
// bundle. prose.mjs is itself never inlined by viz.mjs's askSource(), so
|
|
12
|
-
// browser-side can reach this module. wink
|
|
13
|
-
//
|
|
14
|
-
// the optional deps simply builds no lemma layer (honestly
|
|
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 {
|
|
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
|
|
31
|
-
|
|
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 };
|