kibi-mcp 0.11.0 → 0.13.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/dist/server/docs.js +15 -11
- package/dist/server/tools.js +6 -0
- package/dist/tools/autopilot-candidates.js +120 -6
- package/dist/tools/autopilot-discovery.js +20 -5
- package/dist/tools/autopilot-generate.js +47 -1
- package/dist/tools/briefing-generate.js +115 -15
- package/dist/tools/model-requirement.js +334 -0
- package/dist/tools-config.js +50 -0
- package/package.json +3 -3
package/dist/server/docs.js
CHANGED
|
@@ -62,7 +62,7 @@ export const PROMPTS = [
|
|
|
62
62
|
"Call `kb_autopilot_generate` with the gathered context to synthesize candidate entities.",
|
|
63
63
|
"",
|
|
64
64
|
"This tool is **read-only**. It returns additive `structuredContent` with:",
|
|
65
|
-
"- `promptBlock`:
|
|
65
|
+
"- `promptBlock`: review text that can be surfaced in optional human-facing briefs",
|
|
66
66
|
"- `recommendedActions`: agent-facing next steps, including any REQ/SCEN/TEST authoring routed for manual handling",
|
|
67
67
|
"- `declaredContext`: the user-provided bootstrap context",
|
|
68
68
|
"- `confidence`: confidence summary for the generated output",
|
|
@@ -70,19 +70,19 @@ export const PROMPTS = [
|
|
|
70
70
|
"- `candidates`: synthesized entities grounded in declared context and source evidence",
|
|
71
71
|
"- `discoverySummary`: source-backed discovery notes",
|
|
72
72
|
"",
|
|
73
|
-
"## Step 3:
|
|
73
|
+
"## Step 3: Optional Human Review",
|
|
74
74
|
"",
|
|
75
|
-
"
|
|
75
|
+
"Surface the `promptBlock` and a summary of `candidates` when optional human review is useful. Human review is post-hoc/optional via VS Code briefs and must not block writes.",
|
|
76
76
|
"",
|
|
77
|
-
"## Step 4: Apply
|
|
77
|
+
"## Step 4: Apply Candidates",
|
|
78
78
|
"",
|
|
79
|
-
"Apply
|
|
80
|
-
"1. Execute each
|
|
79
|
+
"Apply candidates sequentially using `kb_upsert`.",
|
|
80
|
+
"1. Execute each candidate's `applyPlan` in ascending phase order.",
|
|
81
81
|
"2. Confirm success of each `kb_upsert` before moving to the next.",
|
|
82
82
|
"3. Run `kb_check` after the batch to verify KB integrity.",
|
|
83
83
|
"",
|
|
84
84
|
"## Rules",
|
|
85
|
-
"-
|
|
85
|
+
"- Human review is optional and post-hoc via VS Code briefs; do not gate writes on synchronous sign-off.",
|
|
86
86
|
"- `kb_autopilot_generate` is strictly read-only; synthesis is the backend, not the actor.",
|
|
87
87
|
"- Guidance must stay MCP-only; do not suggest `kibi` CLI commands.",
|
|
88
88
|
].join("\n"),
|
|
@@ -135,7 +135,10 @@ export const PROMPTS = [
|
|
|
135
135
|
"Core modeling principles:",
|
|
136
136
|
"- Kibi has eight entity types: common authoring (req, scenario, test, fact) and supporting/system (adr, flag, event, symbol).",
|
|
137
137
|
"- Encode requirements as linked facts: `req --constrains--> fact` plus `req --requires_property--> fact`.",
|
|
138
|
-
"-
|
|
138
|
+
"- High-confidence modeling (>= 0.7) is fully automated; optional human review can happen post-hoc.",
|
|
139
|
+
"- Low-confidence claims (< 0.7) are downgraded to `observation` facts to prevent false-positive contradictions.",
|
|
140
|
+
"- Only strict domain facts participate in contradiction inference; observation and meta facts are non-blocking notes.",
|
|
141
|
+
"- v1 contradictions are limited to exact-value, boolean/enum, numeric range, and polarity conflicts.",
|
|
139
142
|
"- Use `kb_search` first for discovery, then `kb_query` for exact follow-up before any mutation.",
|
|
140
143
|
"- Use `kb_upsert` and `kb_delete` only for intentional, traceable KB changes.",
|
|
141
144
|
"- Run `kb_check` after meaningful mutations to catch integrity issues early.",
|
|
@@ -152,9 +155,10 @@ export const PROMPTS = [
|
|
|
152
155
|
"Follow this sequence for reliable operation:",
|
|
153
156
|
"",
|
|
154
157
|
"1. **Discover first**: Call `kb_search` for exploratory discovery, then `kb_query` to confirm exact current state before mutation.",
|
|
155
|
-
"2. **
|
|
156
|
-
"3. **
|
|
157
|
-
"4. **
|
|
158
|
+
"2. **Check schema status**: Call `kb_status` to see if a schema migration is required for the branch KB.",
|
|
159
|
+
"3. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
|
|
160
|
+
"4. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
|
|
161
|
+
"5. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property` (automated via `kb_model_requirement`).",
|
|
158
162
|
"5. **Mutate**: Call `kb_upsert` for create/update, or `kb_delete` for explicit removals.",
|
|
159
163
|
"6. **Targeted checks**: Run `kb_check` after meaningful mutations; specify only the rules you need.",
|
|
160
164
|
"",
|
package/dist/server/tools.js
CHANGED
|
@@ -11,6 +11,7 @@ import { handleKbQuery } from "../tools/query.js";
|
|
|
11
11
|
import { handleKbSearch } from "../tools/search.js";
|
|
12
12
|
import { handleKbStatus } from "../tools/status.js";
|
|
13
13
|
import { handleKbUpsert } from "../tools/upsert.js";
|
|
14
|
+
import { handleKbModelRequirement, } from "../tools/model-requirement.js";
|
|
14
15
|
import { handleKbAutopilotGenerate, } from "../tools/autopilot-generate.js";
|
|
15
16
|
import { handleKbBriefingGenerate, } from "../tools/briefing-generate.js";
|
|
16
17
|
const defaultToolsServerDeps = {
|
|
@@ -59,6 +60,7 @@ const DEFAULT_TOOLS_RUNTIME = {
|
|
|
59
60
|
handleKbSearch,
|
|
60
61
|
handleKbStatus,
|
|
61
62
|
handleKbUpsert,
|
|
63
|
+
handleKbModelRequirement,
|
|
62
64
|
handleKbAutopilotGenerate,
|
|
63
65
|
handleKbBriefingGenerate,
|
|
64
66
|
};
|
|
@@ -321,6 +323,10 @@ runtime = DEFAULT_TOOLS_RUNTIME) {
|
|
|
321
323
|
const prolog = await runtime.ensureProlog();
|
|
322
324
|
return runtime.handleKbCheck(prolog, args);
|
|
323
325
|
}, runtime);
|
|
326
|
+
addTool(server, "kb_model_requirement", toolDef("kb_model_requirement").description, toolDef("kb_model_requirement").inputSchema, async (args) => {
|
|
327
|
+
const prolog = await runtime.ensureProlog();
|
|
328
|
+
return runtime.handleKbModelRequirement(prolog, args);
|
|
329
|
+
}, runtime);
|
|
324
330
|
addTool(server, "kb_autopilot_generate", toolDef("kb_autopilot_generate").description, toolDef("kb_autopilot_generate").inputSchema, async (args) => {
|
|
325
331
|
const prolog = await runtime.ensureProlog();
|
|
326
332
|
return runtime.handleKbAutopilotGenerate(prolog, args);
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
// Implements candidate assembly from public CLI extractors
|
|
3
3
|
import { extractFromManifest } from "kibi-cli/extractors/manifest";
|
|
4
4
|
import { extractFromMarkdown } from "kibi-cli/extractors/markdown";
|
|
5
|
+
import { buildStrictWriteSet, modelRequirementClaims, } from "kibi-cli/public/check-types";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import fs from "node:fs";
|
|
8
|
+
import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
|
|
9
|
+
import { estimateNormativeSignalConfidence, extractRequirementClaim, strictWriteSetToApplyPlan, writeSetPrimaryEntityId, } from "./model-requirement.js";
|
|
7
10
|
function slugify(value, maxLength = 80) {
|
|
8
11
|
return value
|
|
9
12
|
.toLowerCase()
|
|
@@ -63,10 +66,8 @@ function resolveCandidatePaths(filePath, workspaceRoot) {
|
|
|
63
66
|
.join("/");
|
|
64
67
|
return { absolutePath, relativePath };
|
|
65
68
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return /(^|\/)(documentation|\.kb|\.git|node_modules|vendor|vendors|third_party|third-party|dist|coverage)(\/|$)/.test(normalized);
|
|
69
|
-
}
|
|
69
|
+
// Legacy helper removed in favor of the shared ignore policy from kibi-cli/ignore-policy.
|
|
70
|
+
// Use createRepoIgnorePolicy(workspaceRoot).isIgnored(relativePath) in builders.
|
|
70
71
|
function shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown) {
|
|
71
72
|
const base = path.basename(relativePath).toLowerCase();
|
|
72
73
|
const inDocsDir = /(^|\/)docs\//.test(relativePath);
|
|
@@ -103,6 +104,7 @@ function buildUpsertFromExtraction(er, typeOverride) {
|
|
|
103
104
|
export function buildTypedMarkdownCandidates(discoveryResult, existingEntities) {
|
|
104
105
|
const candidates = [];
|
|
105
106
|
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
107
|
+
const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
|
|
106
108
|
for (const filePath of getTypedMarkdownFiles(discoveryResult)) {
|
|
107
109
|
try {
|
|
108
110
|
const extraction = extractFromMarkdown(filePath);
|
|
@@ -110,6 +112,8 @@ export function buildTypedMarkdownCandidates(discoveryResult, existingEntities)
|
|
|
110
112
|
if (existingEntities.ids.has(entity.id))
|
|
111
113
|
continue;
|
|
112
114
|
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
115
|
+
if (ignorePolicy.isIgnored(relativePath))
|
|
116
|
+
continue;
|
|
113
117
|
const candidateId = `md:${relativePath}:${entity.id}`;
|
|
114
118
|
const upsert = buildUpsertFromExtraction({ entity, relationships });
|
|
115
119
|
candidates.push({
|
|
@@ -186,13 +190,14 @@ export function buildSymbolManifestCandidates(discoveryResult, existingEntities)
|
|
|
186
190
|
export function buildGenericMarkdownCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
|
|
187
191
|
const candidates = [];
|
|
188
192
|
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
193
|
+
const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
|
|
189
194
|
const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
|
|
190
195
|
const files = getGenericMarkdownFiles(discoveryResult);
|
|
191
196
|
for (const rawPath of files) {
|
|
192
197
|
try {
|
|
193
198
|
const filePath = String(rawPath);
|
|
194
199
|
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
195
|
-
if (
|
|
200
|
+
if (ignorePolicy.isIgnored(relativePath))
|
|
196
201
|
continue;
|
|
197
202
|
// Legacy path-only discovery was conservative. Provider-scoped discovery
|
|
198
203
|
// already filters eligible generic docs, so allow broader repo markdown there.
|
|
@@ -287,12 +292,13 @@ export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntit
|
|
|
287
292
|
const signals = [];
|
|
288
293
|
const seen = new Set();
|
|
289
294
|
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
295
|
+
const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
|
|
290
296
|
const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
|
|
291
297
|
for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
|
|
292
298
|
try {
|
|
293
299
|
const filePath = String(rawPath);
|
|
294
300
|
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
295
|
-
if (
|
|
301
|
+
if (ignorePolicy.isIgnored(relativePath))
|
|
296
302
|
continue;
|
|
297
303
|
if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
|
|
298
304
|
continue;
|
|
@@ -370,6 +376,113 @@ export function collectSourceOnlyAuthoringSignals(discoveryResult, existingEntit
|
|
|
370
376
|
});
|
|
371
377
|
}
|
|
372
378
|
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
379
|
+
export function buildNormativeRequirementCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
|
|
380
|
+
const candidates = [];
|
|
381
|
+
const seeds = [];
|
|
382
|
+
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
383
|
+
const providerScopedMarkdown = hasGenericMarkdownEvidence(discoveryResult);
|
|
384
|
+
const ignorePolicy = createRepoIgnorePolicy(workspaceRoot);
|
|
385
|
+
for (const rawPath of getGenericMarkdownFiles(discoveryResult)) {
|
|
386
|
+
try {
|
|
387
|
+
const filePath = String(rawPath);
|
|
388
|
+
const { absolutePath, relativePath } = resolveCandidatePaths(filePath, workspaceRoot);
|
|
389
|
+
if (ignorePolicy.isIgnored(relativePath))
|
|
390
|
+
continue;
|
|
391
|
+
if (!shouldIncludeGenericMarkdown(relativePath, providerScopedMarkdown))
|
|
392
|
+
continue;
|
|
393
|
+
if (!fs.existsSync(absolutePath))
|
|
394
|
+
continue;
|
|
395
|
+
const content = fs.readFileSync(absolutePath, "utf8");
|
|
396
|
+
const lines = content.split(/\r?\n/);
|
|
397
|
+
let activeHeading;
|
|
398
|
+
let activeHeadingLine;
|
|
399
|
+
let inCodeFence = false;
|
|
400
|
+
for (let i = 0; i < lines.length; i++) {
|
|
401
|
+
const line = lines[i];
|
|
402
|
+
if (line === undefined)
|
|
403
|
+
continue;
|
|
404
|
+
if (/^\s*(```|~~~)/.test(line)) {
|
|
405
|
+
inCodeFence = !inCodeFence;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (inCodeFence)
|
|
409
|
+
continue;
|
|
410
|
+
const headingMatch = line.match(/^\s*#+\s*(.+)$/);
|
|
411
|
+
if (headingMatch?.[1]) {
|
|
412
|
+
activeHeading = headingMatch[1].trim();
|
|
413
|
+
activeHeadingLine = i + 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const statement = line
|
|
417
|
+
.replace(/^\s*[-*+]\s+/, "")
|
|
418
|
+
.replace(/^\s*\d+[.)]\s+/, "")
|
|
419
|
+
.trim();
|
|
420
|
+
if (!statement || !/\b(must|shall|should)\b/i.test(statement))
|
|
421
|
+
continue;
|
|
422
|
+
const confidence = estimateNormativeSignalConfidence(statement, activeHeading);
|
|
423
|
+
if (confidence < minConfidence)
|
|
424
|
+
continue;
|
|
425
|
+
const extracted = extractRequirementClaim({
|
|
426
|
+
text: statement,
|
|
427
|
+
source: relativePath,
|
|
428
|
+
confidence,
|
|
429
|
+
provenance: `${relativePath}#L${i + 1}`,
|
|
430
|
+
});
|
|
431
|
+
const writeSet = buildStrictWriteSet({
|
|
432
|
+
claim: extracted.claim,
|
|
433
|
+
statement: extracted.statement,
|
|
434
|
+
});
|
|
435
|
+
if (!writeSet.isStrict)
|
|
436
|
+
continue;
|
|
437
|
+
seeds.push({
|
|
438
|
+
input: {
|
|
439
|
+
claim: extracted.claim,
|
|
440
|
+
statement: extracted.statement,
|
|
441
|
+
},
|
|
442
|
+
writeSet,
|
|
443
|
+
sourcePath: absolutePath,
|
|
444
|
+
evidence: [
|
|
445
|
+
`normative_statement:${relativePath}#L${i + 1}`,
|
|
446
|
+
...(activeHeading && activeHeadingLine
|
|
447
|
+
? [`generic_heading:${relativePath}#L${activeHeadingLine}`]
|
|
448
|
+
: []),
|
|
449
|
+
],
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
// ignore unreadable files when deriving strict requirement candidates
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const modeledIds = new Set(modelRequirementClaims(seeds.map((seed) => seed.input)).map((writeSet) => writeSetPrimaryEntityId(writeSet)));
|
|
458
|
+
const emittedIds = new Set();
|
|
459
|
+
for (const seed of seeds) {
|
|
460
|
+
const entityId = writeSetPrimaryEntityId(seed.writeSet);
|
|
461
|
+
if (!modeledIds.has(entityId) || emittedIds.has(entityId))
|
|
462
|
+
continue;
|
|
463
|
+
if (existingEntities.ids.has(entityId))
|
|
464
|
+
continue;
|
|
465
|
+
emittedIds.add(entityId);
|
|
466
|
+
candidates.push({
|
|
467
|
+
candidateId: `norm:${entityId.toLowerCase()}`,
|
|
468
|
+
entityType: "req",
|
|
469
|
+
title: seed.input.statement,
|
|
470
|
+
sourceKind: "generic_markdown",
|
|
471
|
+
sourcePath: seed.sourcePath,
|
|
472
|
+
confidence: seed.writeSet.confidence,
|
|
473
|
+
confidenceBand: toConfidenceBand(seed.writeSet.confidence),
|
|
474
|
+
evidence: seed.evidence,
|
|
475
|
+
relationships: seed.writeSet.relationships.map((relationship) => ({
|
|
476
|
+
type: relationship.type,
|
|
477
|
+
from: relationship.from,
|
|
478
|
+
to: relationship.to,
|
|
479
|
+
})),
|
|
480
|
+
applyPlan: strictWriteSetToApplyPlan(seed.writeSet),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return candidates;
|
|
484
|
+
}
|
|
485
|
+
// implements REQ-mcp-init-kibi-autopilot-v1
|
|
373
486
|
export function buildProviderEvidenceCandidates(discoveryResult, existingEntities, minConfidence = 0.8) {
|
|
374
487
|
const candidates = [];
|
|
375
488
|
const workspaceRoot = existingEntities.workspaceRoot ?? process.cwd();
|
|
@@ -431,5 +544,6 @@ export default {
|
|
|
431
544
|
buildSymbolManifestCandidates,
|
|
432
545
|
buildGenericMarkdownCandidates,
|
|
433
546
|
collectSourceOnlyAuthoringSignals,
|
|
547
|
+
buildNormativeRequirementCandidates,
|
|
434
548
|
buildProviderEvidenceCandidates,
|
|
435
549
|
};
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import fs from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import fg from "fast-glob";
|
|
10
|
+
import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
|
|
10
11
|
import * as cliSymbolCoordinator from "kibi-cli/extractors/symbols-coordinator";
|
|
11
12
|
import { runJsonModuleQuery } from "./core-module.js";
|
|
12
13
|
// implements REQ-001
|
|
@@ -211,7 +212,7 @@ function normalizeDiscoveryPaths(cwd) {
|
|
|
211
212
|
symbols: readPath("symbols"),
|
|
212
213
|
};
|
|
213
214
|
}
|
|
214
|
-
function buildIgnoredGlobs(vendoredRoots) {
|
|
215
|
+
function buildIgnoredGlobs(vendoredRoots, workspaceRoot) {
|
|
215
216
|
const ignored = new Set();
|
|
216
217
|
for (const dirName of IGNORED_DIRECTORY_NAMES) {
|
|
217
218
|
ignored.add(`**/${dirName}`);
|
|
@@ -226,6 +227,18 @@ function buildIgnoredGlobs(vendoredRoots) {
|
|
|
226
227
|
ignored.add(`**/${normalized}`);
|
|
227
228
|
ignored.add(`**/${normalized}/**`);
|
|
228
229
|
}
|
|
230
|
+
// Use shared ignore policy to include .gitignore, nested ignores, and other rules.
|
|
231
|
+
try {
|
|
232
|
+
const policy = createRepoIgnorePolicy(workspaceRoot);
|
|
233
|
+
const globs = policy.getFastGlobIgnoreGlobs();
|
|
234
|
+
for (const glob of globs) {
|
|
235
|
+
if (glob)
|
|
236
|
+
ignored.add(glob);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// best-effort only
|
|
241
|
+
}
|
|
229
242
|
return Array.from(ignored);
|
|
230
243
|
}
|
|
231
244
|
function detectLanguagesFromPaths(paths) {
|
|
@@ -266,6 +279,7 @@ function runTypedKibiDocsProvider(workspaceRoot) {
|
|
|
266
279
|
onlyFiles: true,
|
|
267
280
|
unique: true,
|
|
268
281
|
suppressErrors: true,
|
|
282
|
+
ignore: buildIgnoredGlobs([], workspaceRoot),
|
|
269
283
|
});
|
|
270
284
|
const manifestFiles = discoveryPaths.symbols
|
|
271
285
|
? fg.sync(discoveryPaths.symbols, {
|
|
@@ -274,6 +288,7 @@ function runTypedKibiDocsProvider(workspaceRoot) {
|
|
|
274
288
|
onlyFiles: true,
|
|
275
289
|
unique: true,
|
|
276
290
|
suppressErrors: true,
|
|
291
|
+
ignore: buildIgnoredGlobs([], workspaceRoot),
|
|
277
292
|
})
|
|
278
293
|
: [];
|
|
279
294
|
const evidence = [
|
|
@@ -292,7 +307,7 @@ function runGenericRepoDocsProvider(workspaceRoot, vendoredRoots, typedFilePaths
|
|
|
292
307
|
onlyFiles: true,
|
|
293
308
|
unique: true,
|
|
294
309
|
suppressErrors: true,
|
|
295
|
-
ignore: buildIgnoredGlobs(vendoredRoots),
|
|
310
|
+
ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
|
|
296
311
|
});
|
|
297
312
|
const evidence = sortUnique(markdownFiles)
|
|
298
313
|
.map((absolutePath) => createFileEvidence("generic_repo_docs", "generic_markdown", workspaceRoot, absolutePath))
|
|
@@ -435,7 +450,7 @@ function runRepoLayoutProvider(workspaceRoot, vendoredRoots) {
|
|
|
435
450
|
onlyFiles: true,
|
|
436
451
|
unique: true,
|
|
437
452
|
suppressErrors: true,
|
|
438
|
-
ignore: buildIgnoredGlobs(vendoredRoots),
|
|
453
|
+
ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
|
|
439
454
|
});
|
|
440
455
|
return {
|
|
441
456
|
provider: "repo_layout",
|
|
@@ -468,7 +483,7 @@ function runTestTopologyProvider(workspaceRoot, vendoredRoots) {
|
|
|
468
483
|
onlyFiles: true,
|
|
469
484
|
unique: true,
|
|
470
485
|
suppressErrors: true,
|
|
471
|
-
ignore: buildIgnoredGlobs(vendoredRoots),
|
|
486
|
+
ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
|
|
472
487
|
});
|
|
473
488
|
const detectedFrameworks = new Set();
|
|
474
489
|
const detectedLanguages = new Set();
|
|
@@ -525,7 +540,7 @@ function runSourceSymbolsProvider(workspaceRoot, vendoredRoots) {
|
|
|
525
540
|
onlyFiles: true,
|
|
526
541
|
unique: true,
|
|
527
542
|
suppressErrors: true,
|
|
528
|
-
ignore: buildIgnoredGlobs(vendoredRoots),
|
|
543
|
+
ignore: buildIgnoredGlobs(vendoredRoots, workspaceRoot),
|
|
529
544
|
});
|
|
530
545
|
const evidence = [];
|
|
531
546
|
const detectedLanguages = new Set();
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import fg from "fast-glob";
|
|
3
|
+
import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
|
|
4
|
+
import { buildNormativeRequirementCandidates, collectSourceOnlyAuthoringSignals, buildGenericMarkdownCandidates, buildProviderEvidenceCandidates, buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
|
|
5
|
+
import { getWorkspaceMigrationWarning } from "./model-requirement.js";
|
|
3
6
|
import { discoverProviderEvidence, resolveActivationPolicy, } from "./autopilot-discovery.js";
|
|
4
7
|
import { loadEntities } from "./entity-query.js";
|
|
5
8
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
@@ -373,6 +376,7 @@ _prolog, args) {
|
|
|
373
376
|
const activation = await resolveActivationPolicy(workspaceRoot, prolog);
|
|
374
377
|
const activationState = activation.activationState;
|
|
375
378
|
const activationDiscovery = discoverProviderEvidence(workspaceRoot, activation);
|
|
379
|
+
const migrationWarning = await getWorkspaceMigrationWarning(workspaceRoot);
|
|
376
380
|
const declaredContext = normalizeBootstrapContext(bootstrapContext);
|
|
377
381
|
const discoveredCandidatePaths = activationDiscovery.evidence.reduce((acc, item) => {
|
|
378
382
|
const relativePath = item.relativePath;
|
|
@@ -405,6 +409,7 @@ _prolog, args) {
|
|
|
405
409
|
let typedMarkdownCandidates = [];
|
|
406
410
|
let manifestCandidates = [];
|
|
407
411
|
let genericCandidates = [];
|
|
412
|
+
let normativeRequirementCandidates = [];
|
|
408
413
|
let providerEvidenceCandidates = [];
|
|
409
414
|
let allCandidates = [];
|
|
410
415
|
const seenByKey = new Map();
|
|
@@ -427,6 +432,10 @@ _prolog, args) {
|
|
|
427
432
|
ids: existingIds,
|
|
428
433
|
workspaceRoot,
|
|
429
434
|
}, normalizedMinConfidence);
|
|
435
|
+
normativeRequirementCandidates = buildNormativeRequirementCandidates(candidateDiscovery, {
|
|
436
|
+
ids: existingIds,
|
|
437
|
+
workspaceRoot,
|
|
438
|
+
}, normalizedMinConfidence);
|
|
430
439
|
}
|
|
431
440
|
providerEvidenceCandidates = buildProviderEvidenceCandidates(candidateDiscovery, {
|
|
432
441
|
ids: existingIds,
|
|
@@ -436,6 +445,7 @@ _prolog, args) {
|
|
|
436
445
|
...typedMarkdownCandidates,
|
|
437
446
|
...manifestCandidates,
|
|
438
447
|
...genericCandidates,
|
|
448
|
+
...normativeRequirementCandidates,
|
|
439
449
|
...providerEvidenceCandidates,
|
|
440
450
|
];
|
|
441
451
|
if (entityTypes && entityTypes.length > 0) {
|
|
@@ -516,6 +526,40 @@ _prolog, args) {
|
|
|
516
526
|
seenByKey.set(titleKey, record);
|
|
517
527
|
}
|
|
518
528
|
}
|
|
529
|
+
// Detect repository files that would be candidate inputs but are ignored by
|
|
530
|
+
// the repo ignore policy (e.g. .sisyphus drafts, .gitignore entries). Add
|
|
531
|
+
// them to suppressedCandidates with reason `ignored_source` so callers see
|
|
532
|
+
// why those files were omitted from candidate output.
|
|
533
|
+
try {
|
|
534
|
+
const repoIgnore = createRepoIgnorePolicy(workspaceRoot);
|
|
535
|
+
const potentialFiles = fg.sync(["**/*.md", "**/symbols.{yml,yaml}"], {
|
|
536
|
+
cwd: workspaceRoot,
|
|
537
|
+
absolute: true,
|
|
538
|
+
onlyFiles: true,
|
|
539
|
+
unique: true,
|
|
540
|
+
dot: true,
|
|
541
|
+
suppressErrors: true,
|
|
542
|
+
});
|
|
543
|
+
for (const absPath of potentialFiles) {
|
|
544
|
+
const rel = toWorkspaceRelativePath(workspaceRoot, absPath);
|
|
545
|
+
const explain = repoIgnore.explain(rel);
|
|
546
|
+
if (explain.ignored) {
|
|
547
|
+
// avoid duplicating existing suppressed entries for the same source
|
|
548
|
+
if (!suppressed.some((s) => String(s.sourcePath ?? "") === rel && s.reason === "ignored_source")) {
|
|
549
|
+
suppressed.push({
|
|
550
|
+
candidateId: String("") /* no candidate id for ignored source */,
|
|
551
|
+
reason: "ignored_source",
|
|
552
|
+
sourcePath: rel,
|
|
553
|
+
entityType: String("") /* unknown at this stage */,
|
|
554
|
+
detail: explain.reason,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// best-effort only; ignore failures here so generation can continue
|
|
562
|
+
}
|
|
519
563
|
const candidateRecords = Array.from(seenByKey.values());
|
|
520
564
|
const payoffSummary = buildPayoffSummary(candidateRecords);
|
|
521
565
|
const promptBlock = buildPromptBlock(workspaceRoot, activationState, activation.activationMode, activation.reason, activation.applyBlocked, declaredContext, candidateRecords, sourceOnlySignals, activationDiscovery.summary.scanWarnings);
|
|
@@ -533,6 +577,7 @@ _prolog, args) {
|
|
|
533
577
|
bootstrapMode: activation.activationMode,
|
|
534
578
|
activationReason: activation.reason,
|
|
535
579
|
applyBlocked: effectiveApplyBlocked,
|
|
580
|
+
migrationWarning,
|
|
536
581
|
...(activation.handoffMessage
|
|
537
582
|
? { handoffMessage: activation.handoffMessage }
|
|
538
583
|
: {}),
|
|
@@ -554,6 +599,7 @@ _prolog, args) {
|
|
|
554
599
|
},
|
|
555
600
|
],
|
|
556
601
|
structuredContent,
|
|
602
|
+
migrationWarning,
|
|
557
603
|
candidates: candidateRecords,
|
|
558
604
|
suppressedCandidates: suppressed,
|
|
559
605
|
payoffSummary,
|
|
@@ -6,6 +6,9 @@ import { runJsonModuleQuery, toPrologList } from "./core-module.js";
|
|
|
6
6
|
import { loadEntities } from "./entity-query.js";
|
|
7
7
|
import { handleKbStatus } from "./status.js";
|
|
8
8
|
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
9
|
+
import { isOperationalArtifactPath } from "kibi-cli/operational-artifacts";
|
|
10
|
+
import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
|
|
11
|
+
import { getSchemaVersionStatus } from "kibi-cli/schema-version";
|
|
9
12
|
const ALLOWED_TYPES = [
|
|
10
13
|
"req",
|
|
11
14
|
"adr",
|
|
@@ -106,6 +109,7 @@ function normalizeSourceFiles(workspaceRoot, sourceFiles) {
|
|
|
106
109
|
const normalized = [];
|
|
107
110
|
const seen = new Set();
|
|
108
111
|
const normalizedRoot = path.resolve(workspaceRoot);
|
|
112
|
+
const policy = createRepoIgnorePolicy(workspaceRoot);
|
|
109
113
|
for (const sourceFile of sourceFiles ?? []) {
|
|
110
114
|
const trimmed = String(sourceFile ?? "").trim();
|
|
111
115
|
if (!trimmed)
|
|
@@ -122,8 +126,18 @@ function normalizeSourceFiles(workspaceRoot, sourceFiles) {
|
|
|
122
126
|
.join("/")
|
|
123
127
|
.replace(/^\.\//, "")
|
|
124
128
|
.replace(/^\//, "");
|
|
125
|
-
if (!normalizedPath || seen.has(normalizedPath))
|
|
129
|
+
if (!normalizedPath || seen.has(normalizedPath) || isOperationalArtifactPath(normalizedPath))
|
|
126
130
|
continue;
|
|
131
|
+
// Skip paths ignored by repository ignore policy (eg .gitignore, .git/info/exclude, nested .gitignore,
|
|
132
|
+
// and hard denylist such as .kb, .git, node_modules, .sisyphus)
|
|
133
|
+
try {
|
|
134
|
+
if (policy.isIgnored(normalizedPath))
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Be conservative on errors and do not let ignore policy break briefing generation;
|
|
139
|
+
// fall through and allow the path unless other checks exclude it.
|
|
140
|
+
}
|
|
127
141
|
seen.add(normalizedPath);
|
|
128
142
|
normalized.push(normalizedPath);
|
|
129
143
|
}
|
|
@@ -140,17 +154,32 @@ function stripOuterSingleQuotes(value) {
|
|
|
140
154
|
function candidateKey(entity) {
|
|
141
155
|
return `${String(entity.type ?? "")}::${String(entity.id ?? "")}`;
|
|
142
156
|
}
|
|
143
|
-
function normalizeEntity(entity) {
|
|
157
|
+
function normalizeEntity(entity, workspaceRoot) {
|
|
144
158
|
const type = stripOuterSingleQuotes(String(entity.type ?? "").trim());
|
|
145
159
|
if (!isAllowedType(type))
|
|
146
160
|
return null;
|
|
161
|
+
const source = entity.source ? String(entity.source).trim().split(path.sep).join("/") : undefined;
|
|
162
|
+
if (source && isOperationalArtifactPath(source))
|
|
163
|
+
return null;
|
|
164
|
+
// Respect repository ignore policy for entity sources. Prefer an explicit workspaceRoot when available.
|
|
165
|
+
try {
|
|
166
|
+
if (source) {
|
|
167
|
+
const policyRoot = workspaceRoot ?? process.cwd();
|
|
168
|
+
const policy = createRepoIgnorePolicy(policyRoot);
|
|
169
|
+
if (policy.isIgnored(source))
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// Ignore errors from ignore policy to avoid blocking normalization on policy issues.
|
|
175
|
+
}
|
|
147
176
|
return {
|
|
148
177
|
...entity,
|
|
149
178
|
id: String(entity.id ?? "").trim(),
|
|
150
179
|
type,
|
|
151
180
|
title: String(entity.title ?? "").trim(),
|
|
152
181
|
status: String(entity.status ?? "").trim(),
|
|
153
|
-
source
|
|
182
|
+
source,
|
|
154
183
|
textRef: entity.textRef
|
|
155
184
|
? String(entity.textRef).trim()
|
|
156
185
|
: entity.text_ref
|
|
@@ -158,8 +187,8 @@ function normalizeEntity(entity) {
|
|
|
158
187
|
: undefined,
|
|
159
188
|
};
|
|
160
189
|
}
|
|
161
|
-
function addCandidate(candidates, entity, scoreDelta, reason) {
|
|
162
|
-
const normalizedEntity = normalizeEntity(entity);
|
|
190
|
+
function addCandidate(candidates, entity, scoreDelta, reason, workspaceRoot) {
|
|
191
|
+
const normalizedEntity = normalizeEntity(entity, workspaceRoot);
|
|
163
192
|
if (!normalizedEntity)
|
|
164
193
|
return;
|
|
165
194
|
const key = candidateKey(normalizedEntity);
|
|
@@ -324,7 +353,7 @@ function buildPromptBlock(entities) {
|
|
|
324
353
|
}
|
|
325
354
|
const bullets = allBullets.slice(0, 5);
|
|
326
355
|
let promptBlock = bullets.join("\n");
|
|
327
|
-
|
|
356
|
+
const words = promptBlock.split(/\s+/).filter(Boolean);
|
|
328
357
|
if (words.length > 120) {
|
|
329
358
|
// Hard-truncate to 120 words, preserving whole bullets where possible
|
|
330
359
|
const truncated = [];
|
|
@@ -335,7 +364,7 @@ function buildPromptBlock(entities) {
|
|
|
335
364
|
// Take a partial bullet that fits within budget
|
|
336
365
|
const remaining = 120 - wordCount;
|
|
337
366
|
if (remaining > 3) {
|
|
338
|
-
truncated.push(bulletWords.slice(0, remaining).join(" ")
|
|
367
|
+
truncated.push(`${bulletWords.slice(0, remaining).join(" ")}\u2026`);
|
|
339
368
|
}
|
|
340
369
|
break;
|
|
341
370
|
}
|
|
@@ -347,7 +376,15 @@ function buildPromptBlock(entities) {
|
|
|
347
376
|
return promptBlock;
|
|
348
377
|
}
|
|
349
378
|
function buildCitations(entities) {
|
|
350
|
-
return entities
|
|
379
|
+
return entities
|
|
380
|
+
.filter((entity) => {
|
|
381
|
+
if (entity.source && isOperationalArtifactPath(entity.source))
|
|
382
|
+
return false;
|
|
383
|
+
if (entity.textRef && isOperationalArtifactPath(entity.textRef))
|
|
384
|
+
return false;
|
|
385
|
+
return true;
|
|
386
|
+
})
|
|
387
|
+
.map((entity) => ({
|
|
351
388
|
id: entity.id,
|
|
352
389
|
type: entity.type,
|
|
353
390
|
title: entity.title,
|
|
@@ -355,6 +392,46 @@ function buildCitations(entities) {
|
|
|
355
392
|
...(entity.textRef ? { textRef: entity.textRef } : {}),
|
|
356
393
|
}));
|
|
357
394
|
}
|
|
395
|
+
function buildAutomationReviewEntities(entities, confidenceScore) {
|
|
396
|
+
const entityConfidence = typeof confidenceScore === "number" && Number.isFinite(confidenceScore)
|
|
397
|
+
? roundScore(confidenceScore)
|
|
398
|
+
: 1;
|
|
399
|
+
return entities.map((entity) => ({
|
|
400
|
+
id: entity.id,
|
|
401
|
+
type: entity.type,
|
|
402
|
+
title: entity.title,
|
|
403
|
+
confidence: entityConfidence,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
function buildAutomationReview(entities, confidence, migrationWarning, prologNeighbors) {
|
|
407
|
+
if (entities.length === 0) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
const generatedEntities = buildAutomationReviewEntities(entities, confidence.score);
|
|
411
|
+
const evidenceCitationIds = entities.map((entity) => entity.id);
|
|
412
|
+
const strictEntities = entities.filter((entity) => entity.type === "req" || entity.type === "fact");
|
|
413
|
+
const strictReadinessScore = roundScore(strictEntities.length > 0
|
|
414
|
+
? Math.min(1, strictEntities.length / entities.length + 0.5)
|
|
415
|
+
: 0);
|
|
416
|
+
const contradictionRisks = [];
|
|
417
|
+
for (const entity of strictEntities) {
|
|
418
|
+
if (entity.type === "req" && prologNeighbors.has(entity.id)) {
|
|
419
|
+
contradictionRisks.push(`${entity.id} may have contradiction overlap with related requirements.`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const migrationWarnings = [];
|
|
423
|
+
if (migrationWarning) {
|
|
424
|
+
migrationWarnings.push(migrationWarning);
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
generatedEntities,
|
|
428
|
+
strictReadinessScore,
|
|
429
|
+
confidence: confidence.score,
|
|
430
|
+
migrationWarnings,
|
|
431
|
+
contradictionRisks,
|
|
432
|
+
evidenceCitationIds,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
358
435
|
function roundScore(score) {
|
|
359
436
|
return Math.max(0, Math.min(1, Math.round(score * 100) / 100));
|
|
360
437
|
}
|
|
@@ -396,7 +473,7 @@ function buildConfidence(activationState, freshness, entities, missingEvidence,
|
|
|
396
473
|
reasons,
|
|
397
474
|
};
|
|
398
475
|
}
|
|
399
|
-
async function expandGraphNeighbors(prolog, seedIds) {
|
|
476
|
+
async function expandGraphNeighbors(prolog, seedIds, workspaceRoot) {
|
|
400
477
|
if (seedIds.length === 0) {
|
|
401
478
|
return new Map();
|
|
402
479
|
}
|
|
@@ -413,7 +490,7 @@ async function expandGraphNeighbors(prolog, seedIds) {
|
|
|
413
490
|
}
|
|
414
491
|
const neighbors = new Map();
|
|
415
492
|
for (const node of payload.nodes ?? []) {
|
|
416
|
-
const normalized = normalizeEntity(node);
|
|
493
|
+
const normalized = normalizeEntity(node, workspaceRoot);
|
|
417
494
|
if (!normalized)
|
|
418
495
|
continue;
|
|
419
496
|
const nodeId = String(normalized.id ?? "");
|
|
@@ -475,15 +552,16 @@ prolog, args) {
|
|
|
475
552
|
regressionRisks: [],
|
|
476
553
|
missingEvidence: [],
|
|
477
554
|
citations: [],
|
|
555
|
+
automationReview: null,
|
|
478
556
|
},
|
|
479
557
|
};
|
|
480
558
|
}
|
|
481
559
|
const candidates = new Map();
|
|
482
560
|
for (const entity of await loadByIds(prolog, seedIds)) {
|
|
483
|
-
addCandidate(candidates, entity, 100, "seed hit");
|
|
561
|
+
addCandidate(candidates, entity, 100, "seed hit", workspaceRoot);
|
|
484
562
|
}
|
|
485
563
|
for (const entity of await loadBySourceFiles(prolog, sourceFiles)) {
|
|
486
|
-
addCandidate(candidates, entity, 90, "source-file hit");
|
|
564
|
+
addCandidate(candidates, entity, 90, "source-file hit", workspaceRoot);
|
|
487
565
|
}
|
|
488
566
|
const rankedIds = [];
|
|
489
567
|
if (taskText) {
|
|
@@ -491,7 +569,7 @@ prolog, args) {
|
|
|
491
569
|
const matches = await rankEntities(allEntities, taskText, workspaceRoot);
|
|
492
570
|
matches.forEach((match, index) => {
|
|
493
571
|
rankedIds.push(String(match.entity.id ?? ""));
|
|
494
|
-
addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})
|
|
572
|
+
addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})`, workspaceRoot);
|
|
495
573
|
});
|
|
496
574
|
}
|
|
497
575
|
const graphSeeds = Array.from(new Set([
|
|
@@ -500,9 +578,9 @@ prolog, args) {
|
|
|
500
578
|
...Array.from(candidates.values()).map((candidate) => String(candidate.entity.id ?? "")),
|
|
501
579
|
...rankedIds,
|
|
502
580
|
].filter(Boolean)));
|
|
503
|
-
const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds);
|
|
581
|
+
const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds, workspaceRoot);
|
|
504
582
|
for (const neighbor of graphNeighbors.values()) {
|
|
505
|
-
addCandidate(candidates, neighbor, 40, "graph neighbor");
|
|
583
|
+
addCandidate(candidates, neighbor, 40, "graph neighbor", workspaceRoot);
|
|
506
584
|
}
|
|
507
585
|
const entities = sortedEntities(candidates);
|
|
508
586
|
const constraints = buildConstraints(entities);
|
|
@@ -512,6 +590,26 @@ prolog, args) {
|
|
|
512
590
|
const citations = buildCitations(entities);
|
|
513
591
|
const confidence = buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock);
|
|
514
592
|
const briefingState = confidence.score >= 0.55 ? "ready" : "no_briefing";
|
|
593
|
+
// Compute automation review metadata
|
|
594
|
+
let migrationWarning = null;
|
|
595
|
+
try {
|
|
596
|
+
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
597
|
+
let rawConfig;
|
|
598
|
+
try {
|
|
599
|
+
rawConfig = fs.readFileSync(configPath, "utf8");
|
|
600
|
+
const parsed = JSON.parse(rawConfig);
|
|
601
|
+
const schemaStatus = getSchemaVersionStatus(parsed ?? undefined);
|
|
602
|
+
migrationWarning = schemaStatus.warning;
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
migrationWarning = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
migrationWarning = null;
|
|
610
|
+
}
|
|
611
|
+
const graphNeighborIds = new Set(graphNeighbors.keys());
|
|
612
|
+
const automationReview = buildAutomationReview(entities, confidence, migrationWarning, graphNeighborIds);
|
|
515
613
|
if (briefingState === "no_briefing") {
|
|
516
614
|
return {
|
|
517
615
|
content: [{ type: "text", text: "No briefing is available." }],
|
|
@@ -528,6 +626,7 @@ prolog, args) {
|
|
|
528
626
|
regressionRisks: [],
|
|
529
627
|
missingEvidence: [],
|
|
530
628
|
citations: [],
|
|
629
|
+
automationReview: null,
|
|
531
630
|
},
|
|
532
631
|
};
|
|
533
632
|
}
|
|
@@ -546,6 +645,7 @@ prolog, args) {
|
|
|
546
645
|
regressionRisks,
|
|
547
646
|
missingEvidence,
|
|
548
647
|
citations,
|
|
648
|
+
automationReview,
|
|
549
649
|
},
|
|
550
650
|
};
|
|
551
651
|
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { buildStrictWriteSet, } from "kibi-cli/public/check-types";
|
|
4
|
+
import { getSchemaVersionStatus } from "kibi-cli/schema-version";
|
|
5
|
+
import { resolveWorkspaceRoot } from "../workspace.js";
|
|
6
|
+
const STRICT_FALLBACK_CONFIDENCE = 0.69;
|
|
7
|
+
const NORMATIVE_SECTION_PATTERN = /\b(requirements?|polic(?:y|ies)|rules?)\b/i;
|
|
8
|
+
function normalizeText(text) {
|
|
9
|
+
const normalized = String(text ?? "").trim();
|
|
10
|
+
if (!normalized) {
|
|
11
|
+
throw new Error("Requirement modeling failed: text must be a non-empty string");
|
|
12
|
+
}
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
function normalizeOptionalString(value) {
|
|
16
|
+
const normalized = String(value ?? "").trim();
|
|
17
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
18
|
+
}
|
|
19
|
+
function normalizeSourceFiles(sourceFiles) {
|
|
20
|
+
const seen = new Set();
|
|
21
|
+
const normalized = [];
|
|
22
|
+
for (const sourceFile of sourceFiles ?? []) {
|
|
23
|
+
const trimmed = String(sourceFile ?? "").trim();
|
|
24
|
+
if (!trimmed || seen.has(trimmed))
|
|
25
|
+
continue;
|
|
26
|
+
seen.add(trimmed);
|
|
27
|
+
normalized.push(trimmed);
|
|
28
|
+
}
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
function clampConfidence(confidence) {
|
|
32
|
+
const numeric = typeof confidence === "number" && Number.isFinite(confidence) ? confidence : 0.8;
|
|
33
|
+
return Math.round(Math.min(1, Math.max(0, numeric)) * 100) / 100;
|
|
34
|
+
}
|
|
35
|
+
function normalizeClaimValue(value) {
|
|
36
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
37
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
38
|
+
throw new Error("Requirement modeling failed: value must be a finite number");
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
throw new Error("Requirement modeling failed: value must be a string, number, or boolean");
|
|
43
|
+
}
|
|
44
|
+
function stripListPrefix(value) {
|
|
45
|
+
return value
|
|
46
|
+
.replace(/^\s*[-*+]\s+/, "")
|
|
47
|
+
.replace(/^\s*\d+[.)]\s+/, "")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
function trimSentenceTail(value) {
|
|
51
|
+
return value.replace(/[\s.?!:;]+$/g, "").trim();
|
|
52
|
+
}
|
|
53
|
+
function cleanSubject(value) {
|
|
54
|
+
const cleaned = trimSentenceTail(stripListPrefix(value));
|
|
55
|
+
return cleaned.replace(/^(?:the|a|an)\s+/i, "").trim() || cleaned;
|
|
56
|
+
}
|
|
57
|
+
function cleanPredicate(value) {
|
|
58
|
+
return trimSentenceTail(stripListPrefix(value)) || "statement";
|
|
59
|
+
}
|
|
60
|
+
function fallbackSubjectFromSource(source) {
|
|
61
|
+
const basename = path.basename(source, path.extname(source)).replace(/[-_]+/g, " ").trim();
|
|
62
|
+
return basename || "Requirement";
|
|
63
|
+
}
|
|
64
|
+
function hasExplicitClaimFields(args) {
|
|
65
|
+
return (args.subjectKey !== undefined ||
|
|
66
|
+
args.propertyKey !== undefined ||
|
|
67
|
+
args.operator !== undefined ||
|
|
68
|
+
args.value !== undefined);
|
|
69
|
+
}
|
|
70
|
+
function buildFallbackClaim(statement, source, confidence, provenance) {
|
|
71
|
+
return {
|
|
72
|
+
claim: {
|
|
73
|
+
source,
|
|
74
|
+
subjectKey: fallbackSubjectFromSource(source),
|
|
75
|
+
propertyKey: "statement",
|
|
76
|
+
operator: "eq",
|
|
77
|
+
value: statement,
|
|
78
|
+
confidence: Math.min(confidence, STRICT_FALLBACK_CONFIDENCE),
|
|
79
|
+
...(provenance ? { provenance } : {}),
|
|
80
|
+
},
|
|
81
|
+
extractionMode: "fallback",
|
|
82
|
+
extractionWarnings: [
|
|
83
|
+
"Deterministic claim extraction could not infer a strict semantic claim; emitted a review artifact instead.",
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function extractHeuristicClaim(statement, source, confidence, provenance) {
|
|
88
|
+
const normalized = stripListPrefix(statement);
|
|
89
|
+
const retentionMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+retained\s+for\s+(?<value>\d+)\s+(?<unit>day|days|month|months|year|years)\.?$/i);
|
|
90
|
+
if (retentionMatch?.groups) {
|
|
91
|
+
const { unit, subject, value } = retentionMatch.groups;
|
|
92
|
+
if (!unit || !subject || !value)
|
|
93
|
+
return null;
|
|
94
|
+
const normalizedUnit = unit.toLowerCase().startsWith("day")
|
|
95
|
+
? "Days"
|
|
96
|
+
: unit.toLowerCase().startsWith("month")
|
|
97
|
+
? "Months"
|
|
98
|
+
: "Years";
|
|
99
|
+
return {
|
|
100
|
+
claim: {
|
|
101
|
+
source,
|
|
102
|
+
subjectKey: cleanSubject(subject),
|
|
103
|
+
propertyKey: `Retention ${normalizedUnit}`,
|
|
104
|
+
operator: "eq",
|
|
105
|
+
value: Number(value),
|
|
106
|
+
confidence,
|
|
107
|
+
...(provenance ? { provenance } : {}),
|
|
108
|
+
},
|
|
109
|
+
extractionMode: "heuristic",
|
|
110
|
+
extractionWarnings: [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const enabledMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+be\s+(?<state>enabled|disabled)\.?$/i);
|
|
114
|
+
if (enabledMatch?.groups) {
|
|
115
|
+
const { subject, state } = enabledMatch.groups;
|
|
116
|
+
if (!subject || !state)
|
|
117
|
+
return null;
|
|
118
|
+
return {
|
|
119
|
+
claim: {
|
|
120
|
+
source,
|
|
121
|
+
subjectKey: cleanSubject(subject),
|
|
122
|
+
propertyKey: "enabled",
|
|
123
|
+
operator: "bool",
|
|
124
|
+
value: state.toLowerCase() === "enabled",
|
|
125
|
+
confidence,
|
|
126
|
+
...(provenance ? { provenance } : {}),
|
|
127
|
+
},
|
|
128
|
+
extractionMode: "heuristic",
|
|
129
|
+
extractionWarnings: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const forbiddenMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+not\s+(?<predicate>.+?)\.?$/i);
|
|
133
|
+
if (forbiddenMatch?.groups) {
|
|
134
|
+
const { subject, predicate } = forbiddenMatch.groups;
|
|
135
|
+
if (!subject || !predicate)
|
|
136
|
+
return null;
|
|
137
|
+
return {
|
|
138
|
+
claim: {
|
|
139
|
+
source,
|
|
140
|
+
subjectKey: cleanSubject(subject),
|
|
141
|
+
propertyKey: cleanPredicate(predicate),
|
|
142
|
+
operator: "polarity",
|
|
143
|
+
value: "forbid",
|
|
144
|
+
confidence,
|
|
145
|
+
...(provenance ? { provenance } : {}),
|
|
146
|
+
},
|
|
147
|
+
extractionMode: "heuristic",
|
|
148
|
+
extractionWarnings: [],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const requiredMatch = normalized.match(/^(?<subject>.+?)\s+(?:must|shall|should)\s+(?<predicate>.+?)\.?$/i);
|
|
152
|
+
if (requiredMatch?.groups) {
|
|
153
|
+
const { subject, predicate } = requiredMatch.groups;
|
|
154
|
+
if (!subject || !predicate)
|
|
155
|
+
return null;
|
|
156
|
+
return {
|
|
157
|
+
claim: {
|
|
158
|
+
source,
|
|
159
|
+
subjectKey: cleanSubject(subject),
|
|
160
|
+
propertyKey: cleanPredicate(predicate),
|
|
161
|
+
operator: "polarity",
|
|
162
|
+
value: "require",
|
|
163
|
+
confidence,
|
|
164
|
+
...(provenance ? { provenance } : {}),
|
|
165
|
+
},
|
|
166
|
+
extractionMode: "heuristic",
|
|
167
|
+
extractionWarnings: [],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
export function estimateNormativeSignalConfidence(statement, heading) {
|
|
173
|
+
const normalizedStatement = stripListPrefix(statement).toLowerCase();
|
|
174
|
+
if (!/\b(must|shall|should)\b/.test(normalizedStatement)) {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
let confidence = normalizedStatement.includes(" shall ")
|
|
178
|
+
? 0.86
|
|
179
|
+
: normalizedStatement.includes(" must ")
|
|
180
|
+
? 0.84
|
|
181
|
+
: 0.78;
|
|
182
|
+
if (heading && NORMATIVE_SECTION_PATTERN.test(heading)) {
|
|
183
|
+
confidence += 0.08;
|
|
184
|
+
}
|
|
185
|
+
return Math.round(Math.min(0.95, confidence) * 100) / 100;
|
|
186
|
+
}
|
|
187
|
+
export function extractRequirementClaim(args) {
|
|
188
|
+
const statement = normalizeText(args.text);
|
|
189
|
+
const sourceFiles = normalizeSourceFiles(args.sourceFiles);
|
|
190
|
+
const source = normalizeOptionalString(args.source) ?? sourceFiles[0];
|
|
191
|
+
if (!source) {
|
|
192
|
+
throw new Error("Requirement modeling failed: provide source or at least one sourceFiles entry");
|
|
193
|
+
}
|
|
194
|
+
const provenance = normalizeOptionalString(args.provenance);
|
|
195
|
+
const confidence = clampConfidence(args.confidence);
|
|
196
|
+
if (hasExplicitClaimFields(args)) {
|
|
197
|
+
if (!normalizeOptionalString(args.subjectKey) ||
|
|
198
|
+
!normalizeOptionalString(args.propertyKey) ||
|
|
199
|
+
args.operator === undefined ||
|
|
200
|
+
args.value === undefined) {
|
|
201
|
+
throw new Error("Requirement modeling failed: subjectKey, propertyKey, operator, and value must all be provided when any extracted claim field is supplied");
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
statement,
|
|
205
|
+
source,
|
|
206
|
+
sourceFiles,
|
|
207
|
+
claim: {
|
|
208
|
+
source,
|
|
209
|
+
subjectKey: normalizeOptionalString(args.subjectKey),
|
|
210
|
+
propertyKey: normalizeOptionalString(args.propertyKey),
|
|
211
|
+
operator: args.operator,
|
|
212
|
+
value: normalizeClaimValue(args.value),
|
|
213
|
+
confidence,
|
|
214
|
+
...(provenance ? { provenance } : {}),
|
|
215
|
+
},
|
|
216
|
+
extractionMode: "provided",
|
|
217
|
+
extractionWarnings: [],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const heuristic = extractHeuristicClaim(statement, source, confidence, provenance);
|
|
221
|
+
if (heuristic) {
|
|
222
|
+
return {
|
|
223
|
+
statement,
|
|
224
|
+
source,
|
|
225
|
+
sourceFiles,
|
|
226
|
+
...heuristic,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
statement,
|
|
231
|
+
source,
|
|
232
|
+
sourceFiles,
|
|
233
|
+
...buildFallbackClaim(statement, source, confidence, provenance),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function toRelationshipPlanRows(relationships) {
|
|
237
|
+
return relationships.map((relationship) => ({
|
|
238
|
+
type: relationship.type,
|
|
239
|
+
from: relationship.from,
|
|
240
|
+
to: relationship.to,
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
243
|
+
export function strictWriteSetToApplyPlan(writeSet) {
|
|
244
|
+
if (!writeSet.isStrict) {
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
type: writeSet.observationFact.type,
|
|
248
|
+
id: writeSet.observationFact.id,
|
|
249
|
+
properties: writeSet.observationFact.properties,
|
|
250
|
+
relationships: [],
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
return [
|
|
255
|
+
{
|
|
256
|
+
type: writeSet.subjectFact.type,
|
|
257
|
+
id: writeSet.subjectFact.id,
|
|
258
|
+
properties: writeSet.subjectFact.properties,
|
|
259
|
+
relationships: [],
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: writeSet.propertyFact.type,
|
|
263
|
+
id: writeSet.propertyFact.id,
|
|
264
|
+
properties: writeSet.propertyFact.properties,
|
|
265
|
+
relationships: [],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
type: writeSet.req.type,
|
|
269
|
+
id: writeSet.req.id,
|
|
270
|
+
properties: writeSet.req.properties,
|
|
271
|
+
relationships: toRelationshipPlanRows(writeSet.relationships),
|
|
272
|
+
},
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
export function writeSetPrimaryEntityId(writeSet) {
|
|
276
|
+
return writeSet.isStrict ? writeSet.req.id : writeSet.observationFact.id;
|
|
277
|
+
}
|
|
278
|
+
export async function getWorkspaceMigrationWarning(workspaceRoot = resolveWorkspaceRoot()) {
|
|
279
|
+
const configPath = path.join(workspaceRoot, ".kb", "config.json");
|
|
280
|
+
let rawConfig;
|
|
281
|
+
try {
|
|
282
|
+
rawConfig = await readFile(configPath, "utf8");
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
const parsed = JSON.parse(rawConfig);
|
|
289
|
+
const status = getSchemaVersionStatus(parsed ?? undefined);
|
|
290
|
+
return status.warning;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return "KB config schemaVersion could not be read and should be checked before applying automated modeling.";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
export async function handleKbModelRequirement(_prolog, args) {
|
|
297
|
+
const extracted = extractRequirementClaim(args);
|
|
298
|
+
const writeSet = buildStrictWriteSet({
|
|
299
|
+
claim: extracted.claim,
|
|
300
|
+
statement: extracted.statement,
|
|
301
|
+
});
|
|
302
|
+
const applyPlan = strictWriteSetToApplyPlan(writeSet);
|
|
303
|
+
const migrationWarning = await getWorkspaceMigrationWarning();
|
|
304
|
+
const strictSummary = writeSet.isStrict
|
|
305
|
+
? `Modeled strict requirement into ${applyPlan.length} sequential applyPlan step(s).`
|
|
306
|
+
: "Modeled a non-blocking observation review artifact; deterministic claim extraction stayed below the strict threshold.";
|
|
307
|
+
const structuredContent = {
|
|
308
|
+
statement: extracted.statement,
|
|
309
|
+
source: extracted.source,
|
|
310
|
+
sourceFiles: extracted.sourceFiles,
|
|
311
|
+
claim: extracted.claim,
|
|
312
|
+
writeSet,
|
|
313
|
+
applyPlan,
|
|
314
|
+
isStrict: writeSet.isStrict,
|
|
315
|
+
confidence: writeSet.confidence,
|
|
316
|
+
extractionMode: extracted.extractionMode,
|
|
317
|
+
extractionWarnings: extracted.extractionWarnings,
|
|
318
|
+
migrationWarning,
|
|
319
|
+
};
|
|
320
|
+
return {
|
|
321
|
+
content: [
|
|
322
|
+
{
|
|
323
|
+
type: "text",
|
|
324
|
+
text: migrationWarning
|
|
325
|
+
? `${strictSummary} Migration warning included.`
|
|
326
|
+
: strictSummary,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
structuredContent,
|
|
330
|
+
applyPlan,
|
|
331
|
+
writeSet,
|
|
332
|
+
migrationWarning,
|
|
333
|
+
};
|
|
334
|
+
}
|
package/dist/tools-config.js
CHANGED
|
@@ -385,6 +385,56 @@ const BASE_TOOLS = [
|
|
|
385
385
|
},
|
|
386
386
|
},
|
|
387
387
|
},
|
|
388
|
+
{
|
|
389
|
+
name: "kb_model_requirement",
|
|
390
|
+
description: "Convert a prose requirement plus optional extracted claim fields into a deterministic strict-lane write set. Read-only modeling returns a sequential applyPlan for later kb_upsert calls. High-confidence claims emit req+fact strict output; lower-confidence claims emit an observation review artifact. Includes migration warnings when legacy schemaVersion metadata is detected.",
|
|
391
|
+
inputSchema: {
|
|
392
|
+
type: "object",
|
|
393
|
+
required: ["text"],
|
|
394
|
+
properties: {
|
|
395
|
+
text: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Required prose requirement text to model. Example: 'Customer data must be retained for 7 years.'",
|
|
398
|
+
},
|
|
399
|
+
source: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Optional primary source path or provenance root used for stable IDs and text refs. Example: 'documentation/requirements/customer-retention.md'.",
|
|
402
|
+
},
|
|
403
|
+
sourceFiles: {
|
|
404
|
+
type: "array",
|
|
405
|
+
items: { type: "string" },
|
|
406
|
+
description: "Optional related source files. The first value is used as the source fallback when source is omitted.",
|
|
407
|
+
},
|
|
408
|
+
confidence: {
|
|
409
|
+
type: "number",
|
|
410
|
+
default: 0.8,
|
|
411
|
+
minimum: 0,
|
|
412
|
+
maximum: 1,
|
|
413
|
+
description: "Confidence score for the extracted claim. >= 0.70 yields strict-lane output; lower confidence yields observation-only review output.",
|
|
414
|
+
},
|
|
415
|
+
subjectKey: {
|
|
416
|
+
type: "string",
|
|
417
|
+
description: "Optional extracted semantic claim subjectKey. Example: 'Customer.Data'.",
|
|
418
|
+
},
|
|
419
|
+
propertyKey: {
|
|
420
|
+
type: "string",
|
|
421
|
+
description: "Optional extracted semantic claim propertyKey. Example: 'Retention Years'.",
|
|
422
|
+
},
|
|
423
|
+
operator: {
|
|
424
|
+
type: "string",
|
|
425
|
+
enum: ["eq", "gte", "lte", "neq", "bool", "polarity"],
|
|
426
|
+
description: "Optional extracted semantic claim operator. Example: 'eq'.",
|
|
427
|
+
},
|
|
428
|
+
value: {
|
|
429
|
+
description: "Optional extracted semantic claim value. Accepts string, number, or boolean.",
|
|
430
|
+
},
|
|
431
|
+
provenance: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "Optional extracted text reference. Falls back to source when omitted. Example: 'documentation/requirements/customer-retention.md#L1'.",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
388
438
|
{
|
|
389
439
|
name: "kb_autopilot_generate",
|
|
390
440
|
description: "Generate agent-centric bootstrap output for KB population. Read-only analysis that returns activation state, bootstrap guidance, candidate entities with evidence, payoff summary, and exact applyPlan payloads for later kb_upsert calls. No mutation side effects.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kibi-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
6
6
|
"ajv": "^8.18.0",
|
|
@@ -9,8 +9,8 @@
|
|
|
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.
|
|
13
|
-
"kibi-core": "^0.5.
|
|
12
|
+
"kibi-cli": "^0.10.0",
|
|
13
|
+
"kibi-core": "^0.5.3",
|
|
14
14
|
"mcpcat": "^0.1.12",
|
|
15
15
|
"ts-morph": "^23.0.0",
|
|
16
16
|
"zod": "^4.3.6"
|