kibi-mcp 0.12.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,
@@ -7,6 +7,8 @@ import { loadEntities } from "./entity-query.js";
7
7
  import { handleKbStatus } from "./status.js";
8
8
  import { resolveWorkspaceRoot } from "../workspace.js";
9
9
  import { isOperationalArtifactPath } from "kibi-cli/operational-artifacts";
10
+ import { createRepoIgnorePolicy } from "kibi-cli/ignore-policy";
11
+ import { getSchemaVersionStatus } from "kibi-cli/schema-version";
10
12
  const ALLOWED_TYPES = [
11
13
  "req",
12
14
  "adr",
@@ -107,6 +109,7 @@ function normalizeSourceFiles(workspaceRoot, sourceFiles) {
107
109
  const normalized = [];
108
110
  const seen = new Set();
109
111
  const normalizedRoot = path.resolve(workspaceRoot);
112
+ const policy = createRepoIgnorePolicy(workspaceRoot);
110
113
  for (const sourceFile of sourceFiles ?? []) {
111
114
  const trimmed = String(sourceFile ?? "").trim();
112
115
  if (!trimmed)
@@ -125,6 +128,16 @@ function normalizeSourceFiles(workspaceRoot, sourceFiles) {
125
128
  .replace(/^\//, "");
126
129
  if (!normalizedPath || seen.has(normalizedPath) || isOperationalArtifactPath(normalizedPath))
127
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
+ }
128
141
  seen.add(normalizedPath);
129
142
  normalized.push(normalizedPath);
130
143
  }
@@ -141,13 +154,25 @@ function stripOuterSingleQuotes(value) {
141
154
  function candidateKey(entity) {
142
155
  return `${String(entity.type ?? "")}::${String(entity.id ?? "")}`;
143
156
  }
144
- function normalizeEntity(entity) {
157
+ function normalizeEntity(entity, workspaceRoot) {
145
158
  const type = stripOuterSingleQuotes(String(entity.type ?? "").trim());
146
159
  if (!isAllowedType(type))
147
160
  return null;
148
161
  const source = entity.source ? String(entity.source).trim().split(path.sep).join("/") : undefined;
149
162
  if (source && isOperationalArtifactPath(source))
150
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
+ }
151
176
  return {
152
177
  ...entity,
153
178
  id: String(entity.id ?? "").trim(),
@@ -162,8 +187,8 @@ function normalizeEntity(entity) {
162
187
  : undefined,
163
188
  };
164
189
  }
165
- function addCandidate(candidates, entity, scoreDelta, reason) {
166
- const normalizedEntity = normalizeEntity(entity);
190
+ function addCandidate(candidates, entity, scoreDelta, reason, workspaceRoot) {
191
+ const normalizedEntity = normalizeEntity(entity, workspaceRoot);
167
192
  if (!normalizedEntity)
168
193
  return;
169
194
  const key = candidateKey(normalizedEntity);
@@ -328,7 +353,7 @@ function buildPromptBlock(entities) {
328
353
  }
329
354
  const bullets = allBullets.slice(0, 5);
330
355
  let promptBlock = bullets.join("\n");
331
- let words = promptBlock.split(/\s+/).filter(Boolean);
356
+ const words = promptBlock.split(/\s+/).filter(Boolean);
332
357
  if (words.length > 120) {
333
358
  // Hard-truncate to 120 words, preserving whole bullets where possible
334
359
  const truncated = [];
@@ -339,7 +364,7 @@ function buildPromptBlock(entities) {
339
364
  // Take a partial bullet that fits within budget
340
365
  const remaining = 120 - wordCount;
341
366
  if (remaining > 3) {
342
- truncated.push(bulletWords.slice(0, remaining).join(" ") + "\u2026");
367
+ truncated.push(`${bulletWords.slice(0, remaining).join(" ")}\u2026`);
343
368
  }
344
369
  break;
345
370
  }
@@ -367,6 +392,46 @@ function buildCitations(entities) {
367
392
  ...(entity.textRef ? { textRef: entity.textRef } : {}),
368
393
  }));
369
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
+ }
370
435
  function roundScore(score) {
371
436
  return Math.max(0, Math.min(1, Math.round(score * 100) / 100));
372
437
  }
@@ -408,7 +473,7 @@ function buildConfidence(activationState, freshness, entities, missingEvidence,
408
473
  reasons,
409
474
  };
410
475
  }
411
- async function expandGraphNeighbors(prolog, seedIds) {
476
+ async function expandGraphNeighbors(prolog, seedIds, workspaceRoot) {
412
477
  if (seedIds.length === 0) {
413
478
  return new Map();
414
479
  }
@@ -425,7 +490,7 @@ async function expandGraphNeighbors(prolog, seedIds) {
425
490
  }
426
491
  const neighbors = new Map();
427
492
  for (const node of payload.nodes ?? []) {
428
- const normalized = normalizeEntity(node);
493
+ const normalized = normalizeEntity(node, workspaceRoot);
429
494
  if (!normalized)
430
495
  continue;
431
496
  const nodeId = String(normalized.id ?? "");
@@ -487,15 +552,16 @@ prolog, args) {
487
552
  regressionRisks: [],
488
553
  missingEvidence: [],
489
554
  citations: [],
555
+ automationReview: null,
490
556
  },
491
557
  };
492
558
  }
493
559
  const candidates = new Map();
494
560
  for (const entity of await loadByIds(prolog, seedIds)) {
495
- addCandidate(candidates, entity, 100, "seed hit");
561
+ addCandidate(candidates, entity, 100, "seed hit", workspaceRoot);
496
562
  }
497
563
  for (const entity of await loadBySourceFiles(prolog, sourceFiles)) {
498
- addCandidate(candidates, entity, 90, "source-file hit");
564
+ addCandidate(candidates, entity, 90, "source-file hit", workspaceRoot);
499
565
  }
500
566
  const rankedIds = [];
501
567
  if (taskText) {
@@ -503,7 +569,7 @@ prolog, args) {
503
569
  const matches = await rankEntities(allEntities, taskText, workspaceRoot);
504
570
  matches.forEach((match, index) => {
505
571
  rankedIds.push(String(match.entity.id ?? ""));
506
- addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})`);
572
+ addCandidate(candidates, match.entity, 70 - index, `text-search hit (#${index + 1})`, workspaceRoot);
507
573
  });
508
574
  }
509
575
  const graphSeeds = Array.from(new Set([
@@ -512,9 +578,9 @@ prolog, args) {
512
578
  ...Array.from(candidates.values()).map((candidate) => String(candidate.entity.id ?? "")),
513
579
  ...rankedIds,
514
580
  ].filter(Boolean)));
515
- const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds);
581
+ const graphNeighbors = await expandGraphNeighbors(prolog, graphSeeds, workspaceRoot);
516
582
  for (const neighbor of graphNeighbors.values()) {
517
- addCandidate(candidates, neighbor, 40, "graph neighbor");
583
+ addCandidate(candidates, neighbor, 40, "graph neighbor", workspaceRoot);
518
584
  }
519
585
  const entities = sortedEntities(candidates);
520
586
  const constraints = buildConstraints(entities);
@@ -524,6 +590,26 @@ prolog, args) {
524
590
  const citations = buildCitations(entities);
525
591
  const confidence = buildConfidence(activationState, freshness, entities, missingEvidence, promptBlock);
526
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);
527
613
  if (briefingState === "no_briefing") {
528
614
  return {
529
615
  content: [{ type: "text", text: "No briefing is available." }],
@@ -540,6 +626,7 @@ prolog, args) {
540
626
  regressionRisks: [],
541
627
  missingEvidence: [],
542
628
  citations: [],
629
+ automationReview: null,
543
630
  },
544
631
  };
545
632
  }
@@ -558,6 +645,7 @@ prolog, args) {
558
645
  regressionRisks,
559
646
  missingEvidence,
560
647
  citations,
648
+ automationReview,
561
649
  },
562
650
  };
563
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.12.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.8.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"