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.
@@ -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`: preview text for the user-facing approval prompt",
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: Preview and Approval",
73
+ "## Step 3: Optional Human Review",
74
74
  "",
75
- "Present the `promptBlock` and a summary of `candidates` to the user. **Wait for explicit approval** before proceeding to writes.",
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 Approved Candidates",
77
+ "## Step 4: Apply Candidates",
78
78
  "",
79
- "Apply approved candidates sequentially using `kb_upsert`.",
80
- "1. Execute each approved candidate's `applyPlan` in ascending phase order.",
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
- "- Never apply changes without a user-facing preview and approval.",
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
- "- Only strict domain facts (`fact_kind: subject` + `property_value`) participate in contradiction inference; observation and meta facts are non-blocking notes.",
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. **Create-before-link**: Create endpoint entities with `kb_upsert` before linking them.",
156
- "3. **Validate intent**: If creating links, call `kb_query` for both endpoint IDs first to ensure they exist.",
157
- "4. **Model requirements as facts**: For new/updated reqs, create/reuse fact entities first, then express req semantics with `constrains` + `requires_property`.",
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
  "",
@@ -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
- function isIgnoredGenericMarkdownPath(relativePath) {
67
- const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
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 (isIgnoredGenericMarkdownPath(relativePath))
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 (isIgnoredGenericMarkdownPath(relativePath))
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 { collectSourceOnlyAuthoringSignals, buildGenericMarkdownCandidates, buildProviderEvidenceCandidates, buildTypedMarkdownCandidates, buildSymbolManifestCandidates, } from "./autopilot-candidates.js";
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: entity.source ? String(entity.source).trim().split(path.sep).join("/") : undefined,
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
- let words = promptBlock.split(/\s+/).filter(Boolean);
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(" ") + "\u2026");
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.map((entity) => ({
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
+ }
@@ -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.11.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.7.0",
13
- "kibi-core": "^0.5.2",
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"