kibi-mcp 0.22.0 → 2.0.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 (47) hide show
  1. package/bin/kibi-mcp +12 -11
  2. package/dist/diagnostics.js +68 -8
  3. package/dist/env.js +5 -1
  4. package/dist/runtime/mcp-runtime.js +39 -3
  5. package/dist/server/docs.js +31 -42
  6. package/dist/server/icons.js +27 -0
  7. package/dist/server/jobs.js +107 -0
  8. package/dist/server/json-schema-to-zod.js +33 -9
  9. package/dist/server/session.js +129 -133
  10. package/dist/server/tool-registration.js +46 -9
  11. package/dist/server/tools-runtime.js +10 -10
  12. package/dist/server/tools.js +143 -7
  13. package/dist/server.js +16 -1
  14. package/dist/startup-resolution.js +15 -17
  15. package/dist/tools/check-impact.js +1 -1
  16. package/dist/tools/check.js +1 -1
  17. package/dist/tools/core-module.js +19 -8
  18. package/dist/tools/coverage.js +1 -1
  19. package/dist/tools/delete.js +1 -1
  20. package/dist/tools/discovery-adapter.js +5 -0
  21. package/dist/tools/entity-query.js +1 -1
  22. package/dist/tools/find-gaps.js +9 -1
  23. package/dist/tools/graph.js +9 -1
  24. package/dist/tools/model-requirement.js +2 -2
  25. package/dist/tools/mutation-context.js +15 -9
  26. package/dist/tools/plan-bootstrap.js +21 -0
  27. package/dist/tools/prolog-list.js +1 -1
  28. package/dist/tools/query.js +1 -1
  29. package/dist/tools/relationship-validation.js +1 -1
  30. package/dist/tools/search.js +1 -1
  31. package/dist/tools/semantic-advisor.js +1 -1
  32. package/dist/tools/skills.js +2 -2
  33. package/dist/tools/sparql.js +1 -1
  34. package/dist/tools/status.js +1 -1
  35. package/dist/tools/suggest-predicates.js +1 -1
  36. package/dist/tools/symbols.js +310 -68
  37. package/dist/tools/upsert.js +3 -3
  38. package/dist/tools/validate-upsert.js +1 -1
  39. package/dist/tools-config.js +77 -3
  40. package/dist/workspace.js +10 -8
  41. package/package.json +15 -9
  42. package/dist/tools/autopilot-candidates.js +0 -554
  43. package/dist/tools/autopilot-discovery.js +0 -898
  44. package/dist/tools/autopilot-generate.js +0 -47
  45. package/dist/tools/autopilot-test-compat.js +0 -88
  46. package/dist/tools/autopilot-test-parsers.js +0 -120
  47. package/dist/tools/check-config.js +0 -67
package/dist/workspace.js CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import fs from "node:fs";
19
19
  import path from "node:path";
20
+ import { branchStorePath } from "kibi-runtime";
20
21
  const WORKSPACE_ENV_KEYS = [
21
22
  "KIBI_WORKSPACE",
22
23
  "KIBI_PROJECT_ROOT",
@@ -47,9 +48,9 @@ export function resolveKbPath(workspaceRoot, branch) {
47
48
  if (isBranchPath(resolved)) {
48
49
  return resolved;
49
50
  }
50
- return path.join(resolved, "branches", branch);
51
+ return branchStorePath(resolved, branch);
51
52
  }
52
- return path.join(workspaceRoot, ".kb", "branches", branch);
53
+ return branchStorePath(workspaceRoot, branch);
53
54
  }
54
55
  // implements REQ-002
55
56
  export function resolveEnvFilePath(envFileName, workspaceRoot) {
@@ -67,19 +68,20 @@ function readFirstEnv(keys) {
67
68
  }
68
69
  return null;
69
70
  }
71
+ export function nextAncestorDirectory(current) {
72
+ const parent = path.dirname(current);
73
+ return parent === current ? undefined : parent;
74
+ }
70
75
  function findUpwards(startDir, marker) {
71
76
  let current = path.resolve(startDir);
72
- while (true) {
77
+ while (current !== undefined) {
73
78
  const candidate = path.join(current, marker);
74
79
  if (fs.existsSync(candidate)) {
75
80
  return current;
76
81
  }
77
- const parent = path.dirname(current);
78
- if (parent === current) {
79
- return null;
80
- }
81
- current = parent;
82
+ current = nextAncestorDirectory(current);
82
83
  }
84
+ return null;
83
85
  }
84
86
  function isBranchPath(p) {
85
87
  const parent = path.basename(path.dirname(p));
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "kibi-mcp",
3
- "version": "0.22.0",
3
+ "version": "2.0.0",
4
4
  "dependencies": {
5
- "@modelcontextprotocol/sdk": "^1.26.0",
5
+ "@modelcontextprotocol/sdk": "^1.30.0",
6
6
  "ajv": "^8.18.0",
7
7
  "cli-table3": "^0.6.5",
8
8
  "commander": "^11.0.0",
9
9
  "fast-glob": "^3.2.12",
10
10
  "gray-matter": "^4.0.3",
11
11
  "js-yaml": "^4.1.0",
12
- "kibi-cli": "^0.19.0",
13
- "kibi-core": "^0.10.0",
12
+ "kibi-runtime": "^2.0.0",
13
+ "kibi-core": "^0.12.0",
14
14
  "mcpcat": "^0.1.12",
15
15
  "ts-morph": "^23.0.0",
16
16
  "zod": "^4.3.6"
@@ -24,13 +24,17 @@
24
24
  "scripts": {
25
25
  "test": "bun test",
26
26
  "dev": "bun run bin/kibi-mcp",
27
- "build": "tsc -p tsconfig.json",
27
+ "clean": "rm -rf dist",
28
+ "build": "npm run clean && tsc -p tsconfig.json",
28
29
  "typecheck": "tsc -p tsconfig.json --noEmit",
29
- "prepack": "npm run build"
30
+ "prepack": "npm run build && node ../../scripts/verify-package-contract.mjs kibi-mcp"
30
31
  },
31
- "files": ["dist", "bin"],
32
+ "files": [
33
+ "dist",
34
+ "bin"
35
+ ],
32
36
  "engines": {
33
- "node": ">=18"
37
+ "node": ">=22"
34
38
  },
35
39
  "exports": {
36
40
  ".": "./dist/server.js"
@@ -38,7 +42,9 @@
38
42
  "devDependencies": {
39
43
  "@types/bun": "latest",
40
44
  "@types/node": "latest",
41
- "typescript": "^5.7.0"
45
+ "semver": "^7.7.4",
46
+ "typescript": "^5.7.0",
47
+ "kibi-cli": "workspace:*"
42
48
  },
43
49
  "license": "AGPL-3.0-or-later",
44
50
  "author": "Piotr Franczyk",
@@ -1,554 +0,0 @@
1
- // Kibi — autopilot candidate builders
2
- // Implements candidate assembly from public CLI extractors
3
- import { extractFromManifest } from "kibi-cli/extractors/manifest";
4
- import { extractFromMarkdown } from "kibi-cli/extractors/markdown";
5
- import { buildStrictWriteSet, modelRequirementClaims, } from "kibi-cli/public/check-types";
6
- import fs from "node:fs";
7
- import path from "node:path";
8
- import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
9
- import { estimateNormativeSignalConfidence, extractRequirementClaim, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "./model-requirement.js";
10
- function slugify(value, maxLength = 80) {
11
- return value
12
- .toLowerCase()
13
- .replace(/[^a-z0-9]+/g, "-")
14
- .replace(/(^-|-$)/g, "")
15
- .slice(0, maxLength);
16
- }
17
- function sortUniquePaths(paths) {
18
- return Array.from(new Set(paths)).sort();
19
- }
20
- function getEvidenceFilePaths(discoveryResult, kind) {
21
- return sortUniquePaths((discoveryResult.evidence ?? [])
22
- .filter((item) => item.kind === kind)
23
- .map((item) => item.absolutePath ?? "")
24
- .filter((item) => Boolean(item)));
25
- }
26
- function getTypedMarkdownFiles(discoveryResult) {
27
- const evidenceFiles = getEvidenceFilePaths(discoveryResult, "typed_markdown");
28
- if (evidenceFiles.length > 0)
29
- return evidenceFiles;
30
- return discoveryResult.markdownFiles ?? [];
31
- }
32
- function getManifestFiles(discoveryResult) {
33
- const evidenceFiles = getEvidenceFilePaths(discoveryResult, "symbol_manifest");
34
- if (evidenceFiles.length > 0)
35
- return evidenceFiles;
36
- return discoveryResult.manifestFiles ?? [];
37
- }
38
- function getGenericMarkdownFiles(discoveryResult) {
39
- const evidenceFiles = getEvidenceFilePaths(discoveryResult, "generic_markdown");
40
- if (evidenceFiles.length > 0)
41
- return evidenceFiles;
42
- return discoveryResult.markdownFiles ?? [];
43
- }
44
- function hasGenericMarkdownEvidence(discoveryResult) {
45
- return (discoveryResult.evidence ?? []).some((item) => item.kind === "generic_markdown");
46
- }
47
- function getFactEvidence(discoveryResult) {
48
- return (discoveryResult.evidence ?? []).filter((item) => item.kind === "repo_metadata" ||
49
- item.kind === "repo_layout" ||
50
- item.kind === "test_topology" ||
51
- item.kind === "source_symbols");
52
- }
53
- function toConfidenceBand(confidence) {
54
- if (confidence >= 0.9)
55
- return "high";
56
- if (confidence >= 0.8)
57
- return "medium";
58
- return "low";
59
- }
60
- function resolveCandidatePaths(filePath, workspaceRoot) {
61
- const absolutePath = path.isAbsolute(filePath)
62
- ? filePath
63
- : path.resolve(workspaceRoot, filePath);
64
- const relativePath = (path.relative(workspaceRoot, absolutePath) || filePath)
65
- .split(path.sep)
66
- .join("/");
67
- return { absolutePath, relativePath };
68
- }
69
- // Legacy helper removed in favor of the shared ignore policy from kibi-cli/ignore-policy.
70
- // Use createRepoIgnorePolicy(workspaceRoot).isIgnored(relativePath) in builders.
71
- function shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown) {
72
- const base = path.basename(relativePath).toLowerCase();
73
- const inDocsDir = /(^|\/)docs\//.test(relativePath);
74
- if (providerScopedMarkdown)
75
- return true;
76
- return base === "readme.md" || base === "architecture.md" || inDocsDir;
77
- }
78
- function pushSignal(signals, signal, seen) {
79
- const key = `${signal.kind}::${signal.sourcePath}::${signal.title}`;
80
- if (seen.has(key))
81
- return;
82
- seen.add(key);
83
- signals.push(signal);
84
- }
85
- function buildUpsertFromExtraction(er, typeOverride) {
86
- const ent = er.entity;
87
- const type = typeOverride ?? String(ent.type ?? "");
88
- const id = String(ent.id ?? "");
89
- // Copy properties except `type` to avoid delete
90
- const properties = {};
91
- for (const [k, v] of Object.entries(ent)) {
92
- if (k === "type")
93
- continue;
94
- properties[k] = v;
95
- }
96
- return {
97
- type,
98
- id,
99
- properties,
100
- relationships: er.relationships ?? [],
101
- };
102
- }
103
- // implements REQ-mcp-init-kibi-autopilot-v1
104
- export function buildTypedMarkdownCandidates(discoveryResult, existingEntities) {
105
- const candidates = [];
106
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
107
- const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
108
- for (const filePath of getTypedMarkdownFiles(discoveryResult)) {
109
- try {
110
- const extraction = extractFromMarkdown(filePath);
111
- const { entity, relationships } = extraction;
112
- if (existingEntities.ids.has(entity.id))
113
- continue;
114
- const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
115
- if (ignorePolicy.isIgnored(relativePath))
116
- continue;
117
- const candidateId = `md:${relativePath}:${entity.id}`;
118
- const upsert = buildUpsertFromExtraction({ entity, relationships });
119
- candidates.push({
120
- candidateId,
121
- entityType: entity.type,
122
- title: entity.title,
123
- sourceKind: "typed_markdown",
124
- sourcePath: absolutePath,
125
- confidence: 1.0,
126
- confidenceBand: "high",
127
- evidence: [
128
- `extracted_from_markdown:${relativePath}`,
129
- `entity_id:${entity.id}`,
130
- ],
131
- relationships: relationships || [],
132
- applyPlan: [upsert],
133
- });
134
- }
135
- catch (error) {
136
- // skip files that fail extraction
137
- }
138
- }
139
- return candidates;
140
- }
141
- // implements REQ-mcp-init-kibi-autopilot-v1
142
- export function buildSymbolManifestCandidates(discoveryResult, existingEntities) {
143
- const candidates = [];
144
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
145
- for (const filePath of getManifestFiles(discoveryResult)) {
146
- try {
147
- const results = extractFromManifest(filePath);
148
- for (const res of results) {
149
- const entity = res.entity;
150
- const relationships = res.relationships || [];
151
- if (existingEntities.ids.has(entity.id))
152
- continue;
153
- const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
154
- const candidateId = `mf:${relativePath}:${entity.id}`;
155
- const upsert = buildUpsertFromExtraction({ entity, relationships });
156
- candidates.push({
157
- candidateId,
158
- entityType: entity.type,
159
- title: entity.title,
160
- sourceKind: "symbol_manifest",
161
- sourcePath: absolutePath,
162
- confidence: 0.98,
163
- confidenceBand: "high",
164
- evidence: [
165
- `extracted_from_manifest:${relativePath}`,
166
- `entity_id:${entity.id}`,
167
- ],
168
- relationships,
169
- applyPlan: [upsert],
170
- });
171
- }
172
- }
173
- catch (error) {
174
- // skip manifest parse errors
175
- }
176
- }
177
- return candidates;
178
- }
179
- /**
180
- * Conservative generic markdown candidate builder.
181
- * Scans a small, safe set of top-level markdown files and emits only
182
- * ADR/FACT candidates when clear heading heuristics match.
183
- *
184
- * discoveryResult.markdownFiles is expected to be a list of file paths
185
- * (absolute or relative). Files under documentation/**, .kb/**, .git/**,
186
- * node_modules/**, vendor/**, third_party/** are ignored to avoid vendored
187
- * trees and double-counting typed Kibi docs.
188
- */
189
- // implements REQ-mcp-init-kibi-autopilot-v1
190
- export function buildGenericMarkdownCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
191
- const candidates = [];
192
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
193
- const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
194
- const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
195
- const files = getGenericMarkdownFiles(discoveryResult);
196
- for (const rawPath of files) {
197
- try {
198
- const filePath = String(rawPath);
199
- const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
200
- if (ignorePolicy.isIgnored(relativePath))
201
- continue;
202
- // Legacy path-only discovery was conservative. Provider-scoped discovery
203
- // already filters eligible generic docs, so allow broader repo markdown there.
204
- if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown)) {
205
- continue;
206
- }
207
- if (!fs.existsSync(absolutePath))
208
- continue;
209
- const content = fs.readFileSync(absolutePath, "utf8");
210
- const lines = content.split(/\r?\n/);
211
- for (let i = 0; i < lines.length; i++) {
212
- const line = lines[i];
213
- if (line === undefined)
214
- continue;
215
- const headingMatch = line.match(/^\s*#+\s*(.+)$/);
216
- if (!headingMatch)
217
- continue;
218
- const headingRaw = headingMatch[1];
219
- if (!headingRaw)
220
- continue;
221
- const heading = headingRaw.trim();
222
- const headingLower = heading.toLowerCase();
223
- let type = null;
224
- let confidence = 0;
225
- // ADR heuristic: headings that mention ADR or Architectural Decision
226
- if (/\badr\b/i.test(heading) ||
227
- /architectur.*decision/i.test(heading)) {
228
- type = "adr";
229
- confidence = 0.9;
230
- }
231
- // Fact/Observation heuristic
232
- if (!type && /\b(observations?|facts?|notes?)\b/i.test(heading)) {
233
- type = "fact";
234
- confidence = 0.8;
235
- }
236
- if (!type)
237
- continue;
238
- // Suppress below-threshold candidates early
239
- if (confidence < minConfidence)
240
- continue;
241
- // Build a safe ID for the candidate. Use a repo-relative slug so it's
242
- // deterministic across runs and comparable to typed candidates.
243
- const slug = heading
244
- .toLowerCase()
245
- .replace(/[^a-z0-9]+/g, "-")
246
- .replace(/(^-|-$)/g, "")
247
- .slice(0, 60);
248
- const idPrefix = type === "adr" ? "ADR" : "FACT";
249
- const genId = `${idPrefix}-GEN-${slug || path.basename(relativePath).replace(/\.[^.]+$/, "")}`.toUpperCase();
250
- if (existingEntities.ids.has(genId))
251
- continue;
252
- // Entity record for applyPlan; keep properties minimal and safe.
253
- const entity = {
254
- type,
255
- id: genId,
256
- title: heading,
257
- status: type === "fact" ? "active" : type === "adr" ? "proposed" : "open",
258
- source: `autopilot:generic:${relativePath}`,
259
- text_ref: `${relativePath}#L${i + 1}`,
260
- };
261
- if (type === "fact") {
262
- // Generic facts are observations by policy
263
- entity.fact_kind = "observation";
264
- }
265
- const upsert = buildUpsertFromExtraction({
266
- entity: entity,
267
- relationships: [],
268
- }, type);
269
- const candidateId = `gen:${relativePath}:${type}:${slug}`;
270
- const confidenceBand = confidence >= 0.95 ? "high" : "medium";
271
- candidates.push({
272
- candidateId,
273
- entityType: type,
274
- title: heading,
275
- sourceKind: "generic_markdown",
276
- sourcePath: absolutePath,
277
- confidence,
278
- confidenceBand,
279
- evidence: [`generic_heading:${relativePath}#L${i + 1}`],
280
- relationships: [],
281
- applyPlan: [upsert],
282
- });
283
- }
284
- }
285
- catch (error) {
286
- // skip unreadable files silently
287
- }
288
- }
289
- return candidates;
290
- }
291
- // implements REQ-mcp-init-kibi-autopilot-v1
292
- export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntities, minConfidence = 0.8) {
293
- const signals = [];
294
- const seen = new Set();
295
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
296
- const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
297
- const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
298
- for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
299
- try {
300
- const filePath = String(rawPath);
301
- const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
302
- if (ignorePolicy.isIgnored(relativePath))
303
- continue;
304
- if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
305
- continue;
306
- if (!fs.existsSync(absolutePath))
307
- continue;
308
- const content = fs.readFileSync(absolutePath, "utf8");
309
- const lines = content.split(/\r?\n/);
310
- for (let i = 0; i < lines.length; i++) {
311
- const line = lines[i];
312
- if (line === undefined)
313
- continue;
314
- const headingMatch = line.match(/^\s*#+\s*(.+)$/);
315
- if (!headingMatch)
316
- continue;
317
- const headingRaw = headingMatch[1];
318
- if (!headingRaw)
319
- continue;
320
- const heading = headingRaw.trim();
321
- const textRef = `${relativePath}#L${i + 1}`;
322
- if (/\brequirements?\b/i.test(heading) && 0.84 >= minConfidence) {
323
- pushSignal(signals, {
324
- kind: "req",
325
- title: `Author requirements from ${heading}`,
326
- sourcePath: absolutePath,
327
- confidence: 0.84,
328
- evidence: [`generic_heading:${textRef}`],
329
- }, seen);
330
- }
331
- if (/\bscenarios?\b/i.test(heading) && 0.83 >= minConfidence) {
332
- pushSignal(signals, {
333
- kind: "scenario",
334
- title: `Author scenarios from ${heading}`,
335
- sourcePath: absolutePath,
336
- confidence: 0.83,
337
- evidence: [`generic_heading:${textRef}`],
338
- }, seen);
339
- }
340
- if (/\b(tests?|verification)\b/i.test(heading) &&
341
- 0.82 >= minConfidence) {
342
- pushSignal(signals, {
343
- kind: "test",
344
- title: `Author tests from ${heading}`,
345
- sourcePath: absolutePath,
346
- confidence: 0.82,
347
- evidence: [`generic_heading:${textRef}`],
348
- }, seen);
349
- }
350
- }
351
- }
352
- catch {
353
- // ignore unreadable files when deriving authoring signals
354
- }
355
- }
356
- for (const item of discoveryResult.evidence ?? []) {
357
- const confidence = typeof item.data.confidence === "number" ? item.data.confidence : 0;
358
- if (item.kind === "test_topology" && confidence >= minConfidence) {
359
- const sourcePath = item.absolutePath ??
360
- path.resolve(workspaceRoot, item.relativePath ?? item.label);
361
- const relativePath = item.relativePath ?? item.label;
362
- pushSignal(signals, {
363
- kind: "test",
364
- title: `Author TEST coverage for ${relativePath}`,
365
- sourcePath,
366
- confidence,
367
- evidence: Array.isArray(item.data.evidence)
368
- ? item.data.evidence.filter((value) => typeof value === "string")
369
- : [`test_topology:${relativePath}`],
370
- }, seen);
371
- }
372
- }
373
- return signals.sort((left, right) => {
374
- if (right.confidence !== left.confidence)
375
- return right.confidence - left.confidence;
376
- if (left.kind !== right.kind)
377
- return left.kind.localeCompare(right.kind);
378
- return left.sourcePath.localeCompare(right.sourcePath);
379
- });
380
- }
381
- // implements REQ-mcp-init-kibi-autopilot-v1
382
- export function buildNormativeRequirementCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
383
- const candidates = [];
384
- const seeds = [];
385
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
386
- const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
387
- const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
388
- for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
389
- try {
390
- const filePath = String(rawPath);
391
- const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
392
- if (ignorePolicy.isIgnored(relativePath))
393
- continue;
394
- if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
395
- continue;
396
- if (!fs.existsSync(absolutePath))
397
- continue;
398
- const content = fs.readFileSync(absolutePath, "utf8");
399
- const lines = content.split(/\r?\n/);
400
- let activeHeading;
401
- let activeHeadingLine;
402
- let inCodeFence = false;
403
- for (let i = 0; i < lines.length; i++) {
404
- const line = lines[i];
405
- if (line === undefined)
406
- continue;
407
- if (/^\s*(```|~~~)/.test(line)) {
408
- inCodeFence = !inCodeFence;
409
- continue;
410
- }
411
- if (inCodeFence)
412
- continue;
413
- const headingMatch = line.match(/^\s*#+\s*(.+)$/);
414
- if (headingMatch?.[1]) {
415
- activeHeading = headingMatch[1].trim();
416
- activeHeadingLine = i + 1;
417
- continue;
418
- }
419
- const statement = line
420
- .replace(/^\s*[-*+]\s+/, "")
421
- .replace(/^\s*\d+[.)]\s+/, "")
422
- .trim();
423
- if (!statement || !/\b(must|shall|should)\b/i.test(statement))
424
- continue;
425
- const confidence = estimateNormativeSignalConfidence(statement, activeHeading);
426
- if (confidence < minConfidence)
427
- continue;
428
- const extracted = extractRequirementClaim({
429
- text: statement,
430
- source: relativePath,
431
- confidence,
432
- provenance: `${relativePath}#L${i + 1}`,
433
- });
434
- const writeSet = buildStrictWriteSet({
435
- claim: extracted.claim,
436
- statement: extracted.statement,
437
- });
438
- if (!writeSet.isStrict)
439
- continue;
440
- seeds.push({
441
- input: {
442
- claim: extracted.claim,
443
- statement: extracted.statement,
444
- },
445
- writeSet,
446
- sourcePath: absolutePath,
447
- evidence: [
448
- `normative_statement:${relativePath}#L${i + 1}`,
449
- ...(activeHeading && activeHeadingLine
450
- ? [`generic_heading:${relativePath}#L${activeHeadingLine}`]
451
- : []),
452
- ],
453
- });
454
- }
455
- }
456
- catch {
457
- // ignore unreadable files when deriving strict requirement candidates
458
- }
459
- }
460
- const modeledIds = new Set(modelRequirementClaims(seeds.map((seed) => seed.input)).map((writeSet) => writeSetPrimaryEntityId(writeSet)));
461
- const emittedIds = new Set();
462
- for (const seed of seeds) {
463
- const entityId = writeSetPrimaryEntityId(seed.writeSet);
464
- if (!modeledIds.has(entityId) || emittedIds.has(entityId))
465
- continue;
466
- if (existingEntities.ids.has(entityId))
467
- continue;
468
- emittedIds.add(entityId);
469
- candidates.push({
470
- candidateId: `norm:${entityId.toLowerCase()}`,
471
- entityType: "req",
472
- title: seed.input.statement,
473
- sourceKind: "generic_markdown",
474
- sourcePath: seed.sourcePath,
475
- confidence: seed.writeSet.confidence,
476
- confidenceBand: toConfidenceBand(seed.writeSet.confidence),
477
- evidence: seed.evidence,
478
- relationships: seed.writeSet.relationships.map((relationship) => ({
479
- type: relationship.type,
480
- from: relationship.from,
481
- to: relationship.to,
482
- })),
483
- applyPlan: strictWriteSetToApplyPlan(seed.writeSet),
484
- });
485
- }
486
- return candidates;
487
- }
488
- // implements REQ-mcp-init-kibi-autopilot-v1
489
- export function buildProviderEvidenceCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
490
- const candidates = [];
491
- const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
492
- for (const item of getFactEvidence(discoveryResult)) {
493
- const relativePath = item.relativePath ?? item.label;
494
- const absolutePath = item.absolutePath ?? path.resolve(workspaceRoot, relativePath);
495
- const confidence = typeof item.data.confidence === "number" ? item.data.confidence : 0.8;
496
- if (confidence < minConfidence)
497
- continue;
498
- const factKind = typeof item.data.factKind === "string" && item.data.factKind.length > 0
499
- ? item.data.factKind
500
- : item.kind === "repo_metadata"
501
- ? "meta"
502
- : "observation";
503
- const title = typeof item.data.title === "string" && item.data.title.length > 0
504
- ? item.data.title
505
- : `Autopilot evidence from ${relativePath}`;
506
- const slugSource = `${item.kind}-${relativePath}`;
507
- const generatedId = `FACT-GEN-${slugify(slugSource, 64) || "evidence"}`.toUpperCase();
508
- if (existingEntities.ids.has(generatedId))
509
- continue;
510
- const textRef = relativePath.includes("#")
511
- ? relativePath
512
- : `${relativePath}`;
513
- const evidence = Array.isArray(item.data.evidence)
514
- ? item.data.evidence.filter((value) => typeof value === "string")
515
- : [];
516
- candidates.push({
517
- candidateId: `prov:${item.kind}:${slugify(relativePath, 96) || "evidence"}`,
518
- entityType: "fact",
519
- title,
520
- sourceKind: item.kind,
521
- sourcePath: absolutePath,
522
- confidence,
523
- confidenceBand: toConfidenceBand(confidence),
524
- evidence: evidence.length > 0
525
- ? evidence
526
- : [`provider:${item.provider}`, `${item.kind}:${relativePath}`],
527
- relationships: [],
528
- applyPlan: [
529
- {
530
- type: "fact",
531
- id: generatedId,
532
- properties: {
533
- id: generatedId,
534
- title,
535
- status: "active",
536
- fact_kind: factKind,
537
- source: `autopilot:${item.provider}:${relativePath}`,
538
- text_ref: textRef,
539
- },
540
- relationships: [],
541
- },
542
- ],
543
- });
544
- }
545
- return candidates;
546
- }
547
- export default {
548
- buildTypedMarkdownCandidates,
549
- buildSymbolManifestCandidates,
550
- buildGenericMarkdownCandidates,
551
- collectSourceOnlyAuthoringSignals,
552
- buildNormativeRequirementCandidates,
553
- buildProviderEvidenceCandidates,
554
- };