kibi-mcp 0.7.1 → 0.9.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.
@@ -0,0 +1,271 @@
1
+ import path from "node:path";
2
+ import { buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
3
+ import { classifyActivationState, discoverSources as discoverActivationSources, } from "./autopilot-discovery.js";
4
+ import { loadEntities } from "./entity-query.js";
5
+ import { resolveWorkspaceRoot } from "../workspace.js";
6
+ function extractTextRefFromApplyPlan(applyPlan) {
7
+ if (!Array.isArray(applyPlan) || applyPlan.length === 0)
8
+ return "";
9
+ const first = applyPlan[0];
10
+ if (!first || typeof first !== "object")
11
+ return "";
12
+ const firstRecord = first;
13
+ const properties = firstRecord.properties;
14
+ if (!properties || typeof properties !== "object")
15
+ return "";
16
+ const propsRecord = properties;
17
+ const textRef = propsRecord.text_ref;
18
+ return typeof textRef === "string" ? textRef : "";
19
+ }
20
+ function toSuppressedCandidate(reason, candidate) {
21
+ return {
22
+ candidateId: String(candidate.candidateId ?? ""),
23
+ reason,
24
+ sourcePath: String(candidate.sourcePath ?? ""),
25
+ entityType: String(candidate.entityType ?? ""),
26
+ };
27
+ }
28
+ function activationReasonFor(state) {
29
+ switch (state) {
30
+ case "vendored_only":
31
+ return "Workspace appears to contain vendored Kibi sources only; no local candidates generated.";
32
+ case "root_partial":
33
+ return "Workspace root is partially configured; discovery completed using available sources.";
34
+ case "root_active_seeded":
35
+ return "KB attached and discovery completed for a seeded workspace.";
36
+ case "root_active_thin":
37
+ return "KB attached and discovery completed for a thin workspace.";
38
+ default:
39
+ return "Workspace root is not fully initialized; discovery completed using the resolved workspace root.";
40
+ }
41
+ }
42
+ function splitDiscoveredSources(workspaceRoot, candidates) {
43
+ const markdownFiles = [];
44
+ const manifestFiles = [];
45
+ for (const relativePath of candidates) {
46
+ const absolutePath = path.resolve(workspaceRoot, relativePath);
47
+ if (/symbols\.ya?ml$/i.test(relativePath)) {
48
+ manifestFiles.push(absolutePath);
49
+ continue;
50
+ }
51
+ if (/\.md$/i.test(relativePath)) {
52
+ markdownFiles.push(absolutePath);
53
+ }
54
+ }
55
+ return { markdownFiles, manifestFiles };
56
+ }
57
+ export async function handleKbAutopilotGenerate(// implements REQ-mcp-init-kibi-autopilot-v1
58
+ _prolog, args) {
59
+ const { includeGenericMarkdown = true, minConfidence = 0.8, maxCandidates = 50, entityTypes, } = args;
60
+ // Minimal discovery + candidate assembly implementation
61
+ const prolog = _prolog;
62
+ // Gather existing entity ids to suppress duplicates
63
+ let existingIds = new Set();
64
+ try {
65
+ const entities = await loadEntities(prolog, {});
66
+ for (const e of entities) {
67
+ const id = String(e.id ?? "");
68
+ if (id)
69
+ existingIds.add(id);
70
+ }
71
+ }
72
+ catch (error) {
73
+ // If we can't list entities, proceed with empty set
74
+ existingIds = new Set();
75
+ }
76
+ const workspaceRoot = resolveWorkspaceRoot();
77
+ const activationState = await classifyActivationState(workspaceRoot, prolog);
78
+ const activationDiscovery = discoverActivationSources(workspaceRoot, activationState);
79
+ const discovery = splitDiscoveredSources(workspaceRoot, activationDiscovery.candidates);
80
+ const allowGeneration = activationState === "root_uninitialized" || activationState === "root_partial";
81
+ if (!allowGeneration) {
82
+ return {
83
+ content: [
84
+ {
85
+ type: "text",
86
+ text: "Autopilot generated 0 candidate(s).",
87
+ },
88
+ ],
89
+ structuredContent: {
90
+ activationState,
91
+ activationReason: activationReasonFor(activationState),
92
+ applyBlocked: true,
93
+ discoverySummary: {
94
+ markdownFiles: discovery.markdownFiles.length,
95
+ manifestFiles: discovery.manifestFiles.length,
96
+ vendored: activationDiscovery.summary.vendored ?? [],
97
+ },
98
+ candidates: [],
99
+ suppressedCandidates: [],
100
+ payoffSummary: {
101
+ current: {},
102
+ projectedIfAllApplied: {},
103
+ delta: {},
104
+ },
105
+ },
106
+ };
107
+ }
108
+ const typedMarkdownCandidates = buildTypedMarkdownCandidates(discovery, {
109
+ ids: existingIds,
110
+ workspaceRoot,
111
+ });
112
+ const manifestCandidates = buildSymbolManifestCandidates(discovery, {
113
+ ids: existingIds,
114
+ workspaceRoot,
115
+ });
116
+ // Lazy import to avoid circulars if any
117
+ // buildGenericMarkdownCandidates is added in autopilot-candidates
118
+ let genericCandidates = [];
119
+ if (includeGenericMarkdown) {
120
+ try {
121
+ // Import from same module file
122
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
123
+ const ac = await import("./autopilot-candidates.js");
124
+ if (typeof ac.buildGenericMarkdownCandidates === "function") {
125
+ genericCandidates = ac.buildGenericMarkdownCandidates(discovery, {
126
+ ids: existingIds,
127
+ workspaceRoot,
128
+ }, minConfidence);
129
+ }
130
+ }
131
+ catch (err) {
132
+ // ignore import failures and proceed with typed candidates only
133
+ genericCandidates = [];
134
+ }
135
+ }
136
+ // Merge and filter candidates by requested entityTypes and minConfidence
137
+ let allCandidates = [...typedMarkdownCandidates, ...manifestCandidates, ...genericCandidates];
138
+ if (entityTypes && entityTypes.length > 0) {
139
+ const allowed = new Set(entityTypes);
140
+ allCandidates = allCandidates.filter((c) => allowed.has(c.entityType));
141
+ }
142
+ allCandidates = allCandidates.filter((c) => c.confidence >= minConfidence);
143
+ // Limit and deterministic sort (confidence desc, sourcePath asc)
144
+ allCandidates.sort((a, b) => {
145
+ if (b.confidence !== a.confidence)
146
+ return b.confidence - a.confidence;
147
+ if (a.sourcePath < b.sourcePath)
148
+ return -1;
149
+ if (a.sourcePath > b.sourcePath)
150
+ return 1;
151
+ return 0;
152
+ });
153
+ allCandidates = allCandidates.slice(0, maxCandidates);
154
+ // Dedupe logic
155
+ const seenByKey = new Map();
156
+ const suppressed = [];
157
+ // Helpers
158
+ function normalizeTitle(entityType, title) {
159
+ return `${entityType}::${String(title).trim().toLowerCase().replace(/\s+/g, " ")}`;
160
+ }
161
+ const typedTitleKeys = new Set(typedMarkdownCandidates.map((candidate) => normalizeTitle(String(candidate.entityType || ""), String(candidate.title || ""))));
162
+ for (const c of allCandidates) {
163
+ const record = { ...c };
164
+ const entityType = String(c.entityType || "");
165
+ const title = String(c.title || "");
166
+ const sourceKind = String(c.sourceKind || "");
167
+ const sourcePath = String(c.sourcePath || "");
168
+ const textRef = extractTextRefFromApplyPlan(c.applyPlan);
169
+ const titleKey = normalizeTitle(entityType, title);
170
+ // entity_exists: exact entity ID present in KB
171
+ const upsert = Array.isArray(c.applyPlan) ? c.applyPlan[0] : null;
172
+ let upsertId = "";
173
+ if (upsert && typeof upsert === "object") {
174
+ const upsertRecord = upsert;
175
+ const directId = upsertRecord.id;
176
+ if (typeof directId === "string" && directId.length > 0) {
177
+ upsertId = directId;
178
+ }
179
+ else {
180
+ const properties = upsertRecord.properties;
181
+ if (properties && typeof properties === "object") {
182
+ const nestedId = properties.id;
183
+ if (typeof nestedId === "string" && nestedId.length > 0) {
184
+ upsertId = nestedId;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ if (existingIds.has(upsertId)) {
190
+ suppressed.push(toSuppressedCandidate("entity_exists", record));
191
+ continue;
192
+ }
193
+ if (sourceKind === "generic_markdown" && typedTitleKeys.has(titleKey)) {
194
+ suppressed.push(toSuppressedCandidate("shadowed_by_typed_source", record));
195
+ continue;
196
+ }
197
+ // duplicate_title: same entityType + normalized title
198
+ const existing = seenByKey.get(titleKey);
199
+ if (existing) {
200
+ // keep the higher confidence one
201
+ const existingConf = Number(existing.confidence ?? 0);
202
+ const thisConf = Number(c.confidence ?? 0);
203
+ if (thisConf > existingConf) {
204
+ // move existing to suppressed
205
+ suppressed.push(toSuppressedCandidate("duplicate_title", existing));
206
+ seenByKey.set(titleKey, record);
207
+ }
208
+ else if (thisConf < existingConf) {
209
+ suppressed.push(toSuppressedCandidate("duplicate_title", record));
210
+ }
211
+ else {
212
+ // tie-break by lexicographically smallest sourcePath:textRef
213
+ const existingRef = `${String(existing.sourcePath ?? "")}::${extractTextRefFromApplyPlan(existing.applyPlan)}`;
214
+ const thisRef = `${sourcePath}::${textRef}`;
215
+ if (thisRef < existingRef) {
216
+ suppressed.push(toSuppressedCandidate("duplicate_title", existing));
217
+ seenByKey.set(titleKey, record);
218
+ }
219
+ else {
220
+ suppressed.push(toSuppressedCandidate("duplicate_title", record));
221
+ }
222
+ }
223
+ continue;
224
+ }
225
+ seenByKey.set(titleKey, record);
226
+ }
227
+ const candidateRecords = Array.from(seenByKey.values());
228
+ return {
229
+ content: [
230
+ {
231
+ type: "text",
232
+ text: `Autopilot generated ${allCandidates.length} candidate(s).`,
233
+ },
234
+ ],
235
+ structuredContent: {
236
+ activationState,
237
+ activationReason: activationReasonFor(activationState),
238
+ applyBlocked: activationState === "root_partial",
239
+ discoverySummary: {
240
+ markdownFiles: discovery.markdownFiles.length,
241
+ manifestFiles: discovery.manifestFiles.length,
242
+ vendored: activationDiscovery.summary.vendored ?? [],
243
+ },
244
+ candidates: candidateRecords,
245
+ suppressedCandidates: suppressed,
246
+ payoffSummary: (() => {
247
+ // current counts by type
248
+ const current = {};
249
+ try {
250
+ // compute from existingIds via loadEntities would be expensive; fall back to empty
251
+ }
252
+ catch (e) {
253
+ // noop
254
+ }
255
+ // projected if all applied
256
+ const projected = { ...current };
257
+ for (const r of candidateRecords) {
258
+ const t = String(r.entityType || "unknown");
259
+ projected[t] = (projected[t] || 0) + 1;
260
+ }
261
+ const delta = {};
262
+ for (const k of Object.keys(projected)) {
263
+ const projectedValue = projected[k] ?? 0;
264
+ const currentValue = current[k] ?? 0;
265
+ delta[k] = projectedValue - currentValue;
266
+ }
267
+ return { current, projectedIfAllApplied: projected, delta };
268
+ })(),
269
+ },
270
+ };
271
+ }