kibi-cli 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +9 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +25 -1
  5. package/dist/commands/migrate.d.ts +9 -0
  6. package/dist/commands/migrate.d.ts.map +1 -0
  7. package/dist/commands/migrate.js +183 -0
  8. package/dist/commands/sync/staging.d.ts +3 -0
  9. package/dist/commands/sync/staging.d.ts.map +1 -1
  10. package/dist/commands/sync/staging.js +58 -1
  11. package/dist/commands/sync.d.ts +18 -1
  12. package/dist/commands/sync.d.ts.map +1 -1
  13. package/dist/commands/sync.js +19 -4
  14. package/dist/public/check-types.d.ts +3 -1
  15. package/dist/public/check-types.d.ts.map +1 -1
  16. package/dist/public/check-types.js +1 -0
  17. package/dist/public/ignore-policy.d.ts +10 -0
  18. package/dist/public/ignore-policy.d.ts.map +1 -0
  19. package/dist/public/ignore-policy.js +219 -0
  20. package/dist/public/operational-artifacts.d.ts +2 -0
  21. package/dist/public/operational-artifacts.d.ts.map +1 -0
  22. package/dist/public/operational-artifacts.js +4 -0
  23. package/dist/public/schema-version.d.ts +3 -0
  24. package/dist/public/schema-version.d.ts.map +1 -0
  25. package/dist/public/schema-version.js +1 -0
  26. package/dist/search-ranking.d.ts.map +1 -1
  27. package/dist/search-ranking.js +132 -25
  28. package/dist/utils/config.d.ts +1 -0
  29. package/dist/utils/config.d.ts.map +1 -1
  30. package/dist/utils/config.js +35 -22
  31. package/dist/utils/rule-registry.d.ts.map +1 -1
  32. package/dist/utils/rule-registry.js +6 -0
  33. package/dist/utils/schema-version.d.ts +14 -0
  34. package/dist/utils/schema-version.d.ts.map +1 -0
  35. package/dist/utils/schema-version.js +59 -0
  36. package/dist/utils/strict-modeling.d.ts +64 -0
  37. package/dist/utils/strict-modeling.d.ts.map +1 -0
  38. package/dist/utils/strict-modeling.js +371 -0
  39. package/package.json +17 -3
  40. package/schema/config.json +8 -1
  41. package/src/public/check-types.ts +15 -1
  42. package/src/public/ignore-policy.ts +229 -0
  43. package/src/public/operational-artifacts.ts +5 -0
  44. package/src/public/schema-version.ts +6 -0
@@ -0,0 +1,219 @@
1
+ import { readFileSync, existsSync, readdirSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import ignore from "ignore";
4
+ const HARD_DENYLIST = [
5
+ ".kb",
6
+ ".git",
7
+ "node_modules",
8
+ "vendor",
9
+ "third_party",
10
+ ".sisyphus",
11
+ ".opencode",
12
+ ];
13
+ function readIgnoreFileLines(filePath) {
14
+ if (!existsSync(filePath))
15
+ return [];
16
+ try {
17
+ const content = readFileSync(filePath, "utf8");
18
+ return content
19
+ .split(/\r?\n/)
20
+ .map((l) => l.trim())
21
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ }
27
+ function toPosix(p) {
28
+ return p.split(path.sep).join("/");
29
+ }
30
+ // implements REQ-001
31
+ export function createRepoIgnorePolicy(workspaceRoot) {
32
+ const root = path.resolve(workspaceRoot);
33
+ // Load root .gitignore
34
+ const rootGitignorePath = path.join(root, ".gitignore");
35
+ const rootGitPatterns = readIgnoreFileLines(rootGitignorePath);
36
+ // Load .git/info/exclude
37
+ const gitInfoExcludePath = path.join(root, ".git", "info", "exclude");
38
+ const gitInfoPatterns = readIgnoreFileLines(gitInfoExcludePath);
39
+ // Find nested .gitignore files (skip scanning inside hard denylist directories)
40
+ const nestedPatterns = new Map();
41
+ function walk(dirAbs) {
42
+ let entries;
43
+ try {
44
+ entries = readdirSync(dirAbs, { withFileTypes: true });
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ for (const ent of entries) {
50
+ const name = String(ent.name);
51
+ const abs = path.join(dirAbs, name);
52
+ if (ent.isDirectory()) {
53
+ // avoid descending into common heavy or control directories
54
+ if (HARD_DENYLIST.includes(name))
55
+ continue;
56
+ // also avoid .git itself to prevent reading internal excludes as nested
57
+ if (name === ".git")
58
+ continue;
59
+ walk(abs);
60
+ }
61
+ else if (ent.isFile()) {
62
+ if (name === ".gitignore") {
63
+ // skip root .gitignore (we already loaded it)
64
+ if (path.resolve(dirAbs) === root)
65
+ continue;
66
+ const patterns = readIgnoreFileLines(abs);
67
+ const relDir = path.relative(root, dirAbs) || ".";
68
+ nestedPatterns.set(toPosix(relDir), patterns);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ walk(root);
74
+ // Create ignore instances
75
+ const rootIgnore = ignore();
76
+ if (rootGitPatterns.length > 0)
77
+ rootIgnore.add(rootGitPatterns);
78
+ const gitInfoIgnore = ignore();
79
+ if (gitInfoPatterns.length > 0)
80
+ gitInfoIgnore.add(gitInfoPatterns);
81
+ const nestedIgnoreMap = new Map();
82
+ for (const [dirRel, pats] of nestedPatterns.entries()) {
83
+ const ig = ignore();
84
+ if (pats.length > 0)
85
+ ig.add(pats);
86
+ nestedIgnoreMap.set(dirRel, ig);
87
+ }
88
+ // Prepare nested directories sorted by specificity (longest first)
89
+ const nestedDirsSorted = Array.from(nestedIgnoreMap.keys()).sort((a, b) => b.length - a.length);
90
+ function isPathOutsideWorkspace(absPath) {
91
+ const rel = path.relative(root, absPath);
92
+ // path.relative returns paths starting with '..' for outside
93
+ return rel === "" ? false : rel.split(path.sep)[0] === "..";
94
+ }
95
+ function matchesHardDeny(relPosix) {
96
+ const segments = relPosix.split("/").filter(Boolean);
97
+ for (const deny of HARD_DENYLIST) {
98
+ if (segments.includes(deny))
99
+ return true;
100
+ }
101
+ return false;
102
+ }
103
+ function isIgnoredInternal(inputPath) {
104
+ // Resolve to absolute and relative path inside workspace
105
+ const abs = path.isAbsolute(inputPath) ? path.resolve(inputPath) : path.resolve(root, inputPath);
106
+ if (path.isAbsolute(inputPath) && isPathOutsideWorkspace(abs)) {
107
+ return { ignored: true, reason: "outside_workspace" };
108
+ }
109
+ const rel = path.relative(root, abs) || ".";
110
+ const relPosix = toPosix(rel);
111
+ // Hard denylist always wins
112
+ if (matchesHardDeny(relPosix))
113
+ return { ignored: true, reason: "hard_deny" };
114
+ // Root .gitignore
115
+ try {
116
+ if (rootGitPatterns.length > 0 && rootIgnore.ignores(relPosix)) {
117
+ return { ignored: true, reason: "gitignored" };
118
+ }
119
+ }
120
+ catch (e) {
121
+ // ignore errors from library usage; continue
122
+ }
123
+ // .git/info/exclude
124
+ try {
125
+ if (gitInfoPatterns.length > 0 && gitInfoIgnore.ignores(relPosix)) {
126
+ return { ignored: true, reason: "git_info_exclude" };
127
+ }
128
+ }
129
+ catch (e) {
130
+ // noop
131
+ }
132
+ // Nested .gitignore (apply relative to their directory)
133
+ for (const dirRel of nestedDirsSorted) {
134
+ // dirRel is '.' for nested at root which we skipped, so dirRel will be like 'docs'
135
+ if (dirRel === ".")
136
+ continue;
137
+ if (relPosix === dirRel || relPosix.startsWith(dirRel + "/")) {
138
+ const sub = relPosix === dirRel ? "." : relPosix.slice(dirRel.length + 1);
139
+ const ig = nestedIgnoreMap.get(dirRel);
140
+ try {
141
+ if (ig && ig.ignores(sub))
142
+ return { ignored: true, reason: "gitignored" };
143
+ }
144
+ catch (e) {
145
+ // noop
146
+ }
147
+ }
148
+ }
149
+ return { ignored: false };
150
+ }
151
+ function getFastGlobIgnoreGlobs() {
152
+ const globs = [];
153
+ // Hard denylist globs
154
+ for (const d of HARD_DENYLIST) {
155
+ // match directory and its contents anywhere
156
+ globs.push(`**/${d}/**`);
157
+ globs.push(`**/${d}`);
158
+ }
159
+ // Root .gitignore patterns (convert to simple globs)
160
+ for (const p of rootGitPatterns) {
161
+ if (!p || p.startsWith("#") || p.startsWith("!"))
162
+ continue;
163
+ let pat = p;
164
+ if (pat.startsWith("/"))
165
+ pat = pat.slice(1);
166
+ if (pat.includes("/")) {
167
+ // anchored path
168
+ globs.push(`**/${toPosix(pat)}`);
169
+ }
170
+ else {
171
+ globs.push(`**/${pat}`);
172
+ }
173
+ }
174
+ // .git/info/exclude patterns
175
+ for (const p of gitInfoPatterns) {
176
+ if (!p || p.startsWith("#") || p.startsWith("!"))
177
+ continue;
178
+ let pat = p;
179
+ if (pat.startsWith("/"))
180
+ pat = pat.slice(1);
181
+ if (pat.includes("/")) {
182
+ globs.push(`**/${toPosix(pat)}`);
183
+ }
184
+ else {
185
+ globs.push(`**/${pat}`);
186
+ }
187
+ }
188
+ // Nested .gitignore patterns - prefix with directory path
189
+ // Use the raw patterns collected in nestedPatterns so we scope patterns
190
+ // to the nested directory instead of ignoring the entire directory.
191
+ // Debug: print nested patterns and the computed globs to help diagnosing test failures.
192
+ for (const [dirRel, patterns] of nestedPatterns.entries()) {
193
+ for (const p of patterns) {
194
+ if (!p || p.startsWith("#") || p.startsWith("!"))
195
+ continue;
196
+ let pat = p;
197
+ if (pat.startsWith("/"))
198
+ pat = pat.slice(1);
199
+ const prefix = dirRel === "." ? "" : `${dirRel}/`;
200
+ if (pat.includes("/")) {
201
+ globs.push(`**/${prefix}${toPosix(pat)}`);
202
+ }
203
+ else {
204
+ globs.push(`**/${prefix}${pat}`);
205
+ }
206
+ }
207
+ }
208
+ return Array.from(new Set(globs));
209
+ }
210
+ return {
211
+ isIgnored(inputPath) {
212
+ return isIgnoredInternal(inputPath).ignored;
213
+ },
214
+ getFastGlobIgnoreGlobs,
215
+ explain(inputPath) {
216
+ return isIgnoredInternal(inputPath);
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,2 @@
1
+ export declare function isOperationalArtifactPath(pathLike: string): boolean;
2
+ //# sourceMappingURL=operational-artifacts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"operational-artifacts.d.ts","sourceRoot":"","sources":["../../src/public/operational-artifacts.ts"],"names":[],"mappings":"AAAA,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAInE"}
@@ -0,0 +1,4 @@
1
+ export function isOperationalArtifactPath(pathLike) {
2
+ const normalized = pathLike.replaceAll("\\", "/");
3
+ return /(^|\/)\.sisyphus\//.test(normalized);
4
+ }
@@ -0,0 +1,3 @@
1
+ export type { SchemaVersionStatus } from "../utils/schema-version.js";
2
+ export { LATEST_KB_SCHEMA_VERSION, getSchemaVersionStatus, normalizeSchemaVersion, } from "../utils/schema-version.js";
3
+ //# sourceMappingURL=schema-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../../src/public/schema-version.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACtE,OAAO,EACL,wBAAwB,EACxB,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1 @@
1
+ export { LATEST_KB_SCHEMA_VERSION, getSchemaVersionStatus, normalizeSchemaVersion, } from "../utils/schema-version.js";
@@ -1 +1 @@
1
- {"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAGD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CAyBxB;AA+FD,wBAAsB,gBAAgB,CAEpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6BxB"}
1
+ {"version":3,"file":"search-ranking.d.ts","sourceRoot":"","sources":["../src/search-ranking.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAgDD,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACnC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,WAAW,EAAE,CAAC,CA8BxB;AAmGD,wBAAsB,gBAAgB,CAEpC,MAAM,EAAE,MAAM,EACd,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6BxB"}
@@ -1,10 +1,47 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ const SEARCH_STOP_WORDS = new Set([
4
+ "to",
5
+ "in",
6
+ "out",
7
+ "log",
8
+ "logged",
9
+ "unable",
10
+ "the",
11
+ "a",
12
+ "an",
13
+ "is",
14
+ "are",
15
+ "was",
16
+ "were",
17
+ "be",
18
+ "been",
19
+ "being",
20
+ "have",
21
+ "has",
22
+ "had",
23
+ "do",
24
+ "does",
25
+ "did",
26
+ "will",
27
+ "would",
28
+ "could",
29
+ "should",
30
+ "may",
31
+ "might",
32
+ "shall",
33
+ "can",
34
+ "not",
35
+ ]);
3
36
  // implements REQ-mcp-search-discovery, REQ-002, REQ-003
4
37
  export async function rankEntities(entities, query, workspaceRoot) {
38
+ const queryContext = buildSearchQueryContext(query);
39
+ if (!queryContext.rawTrimmedQuery || queryContext.signalTokens.length === 0) {
40
+ return [];
41
+ }
5
42
  const matches = [];
6
43
  for (const entity of entities) {
7
- const match = await rankEntity(entity, query, workspaceRoot);
44
+ const match = await rankEntity(entity, queryContext, workspaceRoot);
8
45
  if (match) {
9
46
  matches.push(match);
10
47
  }
@@ -22,9 +59,8 @@ export async function rankEntities(entities, query, workspaceRoot) {
22
59
  });
23
60
  return matches;
24
61
  }
25
- async function rankEntity(entity, query, workspaceRoot) {
26
- const normalizedQuery = normalize(query);
27
- const tokens = normalizedQuery.split(/\s+/).filter(Boolean);
62
+ // implements REQ-mcp-search-discovery
63
+ async function rankEntity(entity, queryContext, workspaceRoot) {
28
64
  const reasons = [];
29
65
  let score = 0;
30
66
  const id = String(entity.id ?? "");
@@ -37,55 +73,56 @@ async function rankEntity(entity, query, workspaceRoot) {
37
73
  const tags = Array.isArray(entity.tags)
38
74
  ? entity.tags.map((tag) => String(tag))
39
75
  : [];
40
- const normalizedTitle = normalize(title);
41
- const normalizedId = normalize(id);
42
- if (normalizedTitle === normalizedQuery) {
76
+ const titleForms = buildSearchTextForms(title);
77
+ const idForms = buildSearchTextForms(id);
78
+ if (isExactSearchMatch(titleForms, queryContext.phrase)) {
43
79
  score += 100;
44
80
  reasons.push("exact title match");
45
81
  }
46
- else if (normalizedTitle.includes(normalizedQuery)) {
82
+ else if (isPhraseSearchMatch(titleForms, queryContext.phrase)) {
47
83
  score += 60;
48
84
  reasons.push("title phrase match");
49
85
  }
50
- if (normalizedId === normalizedQuery) {
86
+ if (isExactSearchMatch(idForms, queryContext.phrase)) {
51
87
  score += 90;
52
88
  reasons.push("exact ID match");
53
89
  }
54
- else if (normalizedId.includes(normalizedQuery)) {
90
+ else if (isPhraseSearchMatch(idForms, queryContext.phrase)) {
55
91
  score += 55;
56
92
  reasons.push("ID match");
57
93
  }
58
94
  const metadataFields = [type, source, owner, priority, severity];
59
- const metadataMatched = metadataFields.some((field) => normalize(field).includes(normalizedQuery));
95
+ const metadataMatched = metadataFields.some((field) => isPhraseSearchMatch(buildSearchTextForms(field), queryContext.phrase));
60
96
  if (metadataMatched) {
61
97
  score += 20;
62
98
  reasons.push("metadata match");
63
99
  }
64
- const matchingTags = tags.filter((tag) => normalize(tag).includes(normalizedQuery));
100
+ const matchingTags = tags.filter((tag) => isPhraseSearchMatch(buildSearchTextForms(tag), queryContext.phrase));
65
101
  if (matchingTags.length > 0) {
66
102
  score += 30;
67
103
  reasons.push("tag match");
68
104
  }
69
- const titleTokenMatches = countTokenMatches(normalizedTitle, tokens);
105
+ const titleTokenMatches = countTokenMatches(titleForms, queryContext.signalTokens);
70
106
  if (titleTokenMatches > 0) {
71
107
  score += titleTokenMatches * 8;
72
108
  reasons.push("title token coverage");
73
109
  }
74
- const bodyText = await loadMarkdownBody(source, workspaceRoot);
110
+ const bodyText = (await loadMarkdownBody(source, workspaceRoot)) ??
111
+ getInlineBodyText(entity);
75
112
  let snippet;
76
113
  if (bodyText) {
77
- const normalizedBody = normalize(bodyText);
78
- if (normalizedBody.includes(normalizedQuery)) {
114
+ const bodyForms = buildSearchTextForms(bodyText);
115
+ if (isPhraseSearchMatch(bodyForms, queryContext.phrase)) {
79
116
  score += 15;
80
117
  reasons.push("markdown body match");
81
- snippet = buildSnippet(bodyText, query);
118
+ snippet = buildSnippet(bodyText, queryContext.phrase);
82
119
  }
83
120
  else {
84
- const bodyTokenMatches = countTokenMatches(normalizedBody, tokens);
121
+ const bodyTokenMatches = countTokenMatches(bodyForms, queryContext.signalTokens);
85
122
  if (bodyTokenMatches > 0) {
86
123
  score += bodyTokenMatches * 3;
87
124
  reasons.push("markdown body token coverage");
88
- snippet = buildSnippet(bodyText, query);
125
+ snippet = buildSnippet(bodyText, queryContext.phrase);
89
126
  }
90
127
  }
91
128
  }
@@ -126,6 +163,7 @@ source, workspaceRoot) {
126
163
  return null;
127
164
  }
128
165
  }
166
+ // implements REQ-mcp-search-discovery
129
167
  function stripFrontmatter(content) {
130
168
  const trimmedContent = content.trimStart();
131
169
  if (!trimmedContent.startsWith("---")) {
@@ -143,19 +181,88 @@ function stripFrontmatter(content) {
143
181
  }
144
182
  return trimmedContent.slice(match.index + match[0].length);
145
183
  }
146
- function normalize(value) {
147
- return value.trim().toLowerCase();
184
+ // implements REQ-mcp-search-discovery
185
+ function buildSearchQueryContext(query) {
186
+ return {
187
+ phrase: buildSearchTextForms(query),
188
+ signalTokens: tokenizeSignalTerms(query),
189
+ rawTrimmedQuery: query.trim(),
190
+ };
191
+ }
192
+ // implements REQ-mcp-search-discovery
193
+ function buildSearchTextForms(value) {
194
+ const normalized = normalizeSearchText(value);
195
+ return {
196
+ normalized,
197
+ compact: normalized.replace(/\s+/g, ""),
198
+ };
199
+ }
200
+ // implements REQ-mcp-search-discovery
201
+ function normalizeSearchText(value) {
202
+ return value
203
+ .trim()
204
+ .toLowerCase()
205
+ .replace(/[-_]+/g, " ")
206
+ .replace(/[^a-z0-9\s]+/g, " ")
207
+ .trim()
208
+ .split(/\s+/)
209
+ .filter(Boolean)
210
+ .map(singularizeSimplePlural)
211
+ .join(" ");
212
+ }
213
+ // implements REQ-mcp-search-discovery
214
+ function tokenizeSignalTerms(value) {
215
+ return Array.from(new Set(normalizeSearchText(value)
216
+ .split(/\s+/)
217
+ .filter((token) => token && !SEARCH_STOP_WORDS.has(token))));
148
218
  }
219
+ // implements REQ-mcp-search-discovery
220
+ function singularizeSimplePlural(token) {
221
+ if (token.length <= 4 ||
222
+ !token.endsWith("s") ||
223
+ token.endsWith("ss") ||
224
+ token.endsWith("us") ||
225
+ token.endsWith("is")) {
226
+ return token;
227
+ }
228
+ return token.slice(0, -1);
229
+ }
230
+ // implements REQ-mcp-search-discovery
231
+ function isExactSearchMatch(haystack, needle) {
232
+ return (haystack.normalized === needle.normalized ||
233
+ (needle.compact !== "" && haystack.compact === needle.compact));
234
+ }
235
+ // implements REQ-mcp-search-discovery
236
+ function isPhraseSearchMatch(haystack, needle) {
237
+ return (haystack.normalized.includes(needle.normalized) ||
238
+ (needle.compact !== "" && haystack.compact.includes(needle.compact)));
239
+ }
240
+ // implements REQ-mcp-search-discovery
149
241
  function countTokenMatches(haystack, tokens) {
150
- return tokens.filter((token) => haystack.includes(token)).length;
242
+ return tokens.filter((token) => haystack.normalized.includes(token) || haystack.compact.includes(token)).length;
243
+ }
244
+ // implements REQ-mcp-search-discovery
245
+ function getInlineBodyText(entity) {
246
+ const candidates = [
247
+ entity.body,
248
+ entity.markdownBody,
249
+ entity.markdown_body,
250
+ entity.content,
251
+ ];
252
+ for (const candidate of candidates) {
253
+ if (typeof candidate === "string" && candidate.trim() !== "") {
254
+ return candidate;
255
+ }
256
+ }
257
+ return null;
151
258
  }
152
- function buildSnippet(bodyText, query) {
259
+ // implements REQ-mcp-search-discovery
260
+ function buildSnippet(bodyText, queryForms) {
153
261
  const lines = bodyText
154
262
  .split(/\r?\n/)
155
263
  .map((line) => line.trim())
156
264
  .filter(Boolean);
157
- const normalizedQuery = normalize(query);
158
- const matchedLine = lines.find((line) => normalize(line).includes(normalizedQuery)) ?? lines[0];
265
+ const matchedLine = lines.find((line) => isPhraseSearchMatch(buildSearchTextForms(line), queryForms)) ?? lines[0];
159
266
  if (!matchedLine) {
160
267
  return undefined;
161
268
  }
@@ -35,6 +35,7 @@ export interface BriefsConfig {
35
35
  */
36
36
  export interface KbConfig {
37
37
  paths: KbConfigPaths;
38
+ schemaVersion?: number | string;
38
39
  briefs?: BriefsConfig;
39
40
  /**
40
41
  * @deprecated defaultBranch is deprecated. Branch lifecycle now follows git naturally
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,CAAC;IACF,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO,CAAC;QAChB,GAAG,EAAE,OAAO,CAAC;KACd,CAAC;IACF,GAAG,EAAE;QACH,KAAK,EAAE,OAAO,CAAC;QACf,YAAY,EAAE,OAAO,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,CAAC;AAwBxD,eAAO,MAAM,cAAc,EAAE,QAAQ,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAexD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAShC,CAAC;AAqBF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAqChE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAqCpE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/utils/config.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAG5B;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE;QACV,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,UAAU,CAAC,EAAE,OAAO,CAAC;KACtB,CAAC;IACF,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO,CAAC;QAChB,GAAG,EAAE,OAAO,CAAC;KACd,CAAC;IACF,GAAG,EAAE;QACH,KAAK,EAAE,OAAO,CAAC;QACf,YAAY,EAAE,OAAO,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED;;;GAGG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,YAAY,EAAE,YAAY,EAAE,yBAAyB,EAAE,CAAC;AAwBxD,eAAO,MAAM,cAAc,EAAE,QAAQ,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAgBxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAShC,CAAC;AA8CF;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAiChE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAiCpE"}
@@ -18,6 +18,7 @@
18
18
  import { existsSync, readFileSync } from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import { DEFAULT_CHECKS_CONFIG, } from "./rule-registry.js";
21
+ import { LATEST_KB_SCHEMA_VERSION } from "./schema-version.js";
21
22
  /**
22
23
  * Default configuration values for new repositories.
23
24
  */
@@ -41,6 +42,7 @@ const DEFAULT_BRIEFS_CONFIG = {
41
42
  // implements REQ-003
42
43
  export const DEFAULT_CONFIG = {
43
44
  $schema: "https://raw.githubusercontent.com/Looted/kibi/master/packages/cli/schema/config.json",
45
+ schemaVersion: LATEST_KB_SCHEMA_VERSION,
44
46
  paths: {
45
47
  requirements: "documentation/requirements",
46
48
  scenarios: "documentation/scenarios",
@@ -85,6 +87,27 @@ function mergeBriefsConfig(userBriefs) {
85
87
  },
86
88
  };
87
89
  }
90
+ function readUserConfig(configPath) {
91
+ if (!existsSync(configPath)) {
92
+ return {
93
+ userConfig: {},
94
+ useDefaultSchemaVersion: true,
95
+ };
96
+ }
97
+ try {
98
+ const content = readFileSync(configPath, "utf8");
99
+ return {
100
+ userConfig: JSON.parse(content),
101
+ useDefaultSchemaVersion: false,
102
+ };
103
+ }
104
+ catch {
105
+ return {
106
+ userConfig: {},
107
+ useDefaultSchemaVersion: true,
108
+ };
109
+ }
110
+ }
88
111
  /**
89
112
  * Load and parse the Kibi configuration from .kb/config.json.
90
113
  * Falls back to DEFAULT_CONFIG if the file doesn't exist or is invalid.
@@ -95,22 +118,17 @@ function mergeBriefsConfig(userBriefs) {
95
118
  export function loadConfig(cwd = process.cwd()) {
96
119
  // implements REQ-003
97
120
  const configPath = path.join(cwd, ".kb/config.json");
98
- let userConfig = {};
99
- if (existsSync(configPath)) {
100
- try {
101
- const content = readFileSync(configPath, "utf8");
102
- userConfig = JSON.parse(content);
103
- }
104
- catch {
105
- // Invalid config, use defaults
106
- userConfig = {};
107
- }
108
- }
121
+ const { userConfig, useDefaultSchemaVersion } = readUserConfig(configPath);
109
122
  return {
110
123
  paths: {
111
124
  ...DEFAULT_CONFIG.paths,
112
125
  ...userConfig.paths,
113
126
  },
127
+ ...((userConfig.schemaVersion !== undefined || useDefaultSchemaVersion)
128
+ ? {
129
+ schemaVersion: userConfig.schemaVersion ?? DEFAULT_CONFIG.schemaVersion,
130
+ }
131
+ : {}),
114
132
  briefs: mergeBriefsConfig(userConfig.briefs),
115
133
  ...(userConfig.defaultBranch !== undefined
116
134
  ? { defaultBranch: userConfig.defaultBranch }
@@ -140,22 +158,17 @@ export function loadConfig(cwd = process.cwd()) {
140
158
  export function loadSyncConfig(cwd = process.cwd()) {
141
159
  // implements REQ-003
142
160
  const configPath = path.join(cwd, ".kb/config.json");
143
- let userConfig = {};
144
- if (existsSync(configPath)) {
145
- try {
146
- const content = readFileSync(configPath, "utf8");
147
- userConfig = JSON.parse(content);
148
- }
149
- catch {
150
- // Invalid config, use defaults
151
- userConfig = {};
152
- }
153
- }
161
+ const { userConfig, useDefaultSchemaVersion } = readUserConfig(configPath);
154
162
  return {
155
163
  paths: {
156
164
  ...DEFAULT_SYNC_PATHS,
157
165
  ...userConfig.paths,
158
166
  },
167
+ ...((userConfig.schemaVersion !== undefined || useDefaultSchemaVersion)
168
+ ? {
169
+ schemaVersion: userConfig.schemaVersion ?? DEFAULT_CONFIG.schemaVersion,
170
+ }
171
+ : {}),
159
172
  briefs: mergeBriefsConfig(userConfig.briefs),
160
173
  ...(userConfig.defaultBranch !== undefined
161
174
  ? { defaultBranch: userConfig.defaultBranch }
@@ -1 +1 @@
1
- {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EAqEjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CAsBb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
1
+ {"version":3,"file":"rule-registry.d.ts","sourceRoot":"","sources":["../../src/utils/rule-registry.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,QAAQ,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,GAAG,cAAc,CAAC;CACnE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,mCAAmC;AACnC,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,kBAAkB,EAAE,yBAAyB,CAAC;CAC/C;AAED;;;GAGG;AACH,eAAO,MAAM,KAAK,EAAE,SAAS,cAAc,EA4EjC,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,UAAU,aAAoC,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,qBAAqB,EAAE,YAKnC,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,iBAAiB,CAC/B,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,QAAQ,CAAC,EAAE,MAAM,GAChB,GAAG,CAAC,MAAM,CAAC,CAsBb;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK5D;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAC9B,YAAY,CAWd"}
@@ -81,6 +81,12 @@ export const RULES = [
81
81
  defaultEnabled: false,
82
82
  category: "integrity",
83
83
  },
84
+ {
85
+ name: "strict-readiness",
86
+ description: "Report strict contradiction-readiness levels for requirements that are still prose-only or otherwise not contradiction-ready",
87
+ defaultEnabled: false,
88
+ category: "integrity",
89
+ },
84
90
  ];
85
91
  /**
86
92
  * Set of all rule names for quick lookups.
@@ -0,0 +1,14 @@
1
+ import type { KbConfig } from "./config.js";
2
+ export declare const LATEST_KB_SCHEMA_VERSION = 1;
3
+ export interface SchemaVersionStatus {
4
+ status: "missing" | "invalid" | "older" | "current" | "newer";
5
+ currentVersion: number | null;
6
+ latestVersion: number;
7
+ needsMigration: boolean;
8
+ warning: string | null;
9
+ }
10
+ type SchemaVersionConfig = Pick<KbConfig, "schemaVersion"> | null | undefined;
11
+ export declare function normalizeSchemaVersion(schemaVersion: number | string | null | undefined): number | null;
12
+ export declare function getSchemaVersionStatus(config?: SchemaVersionConfig): SchemaVersionStatus;
13
+ export {};
14
+ //# sourceMappingURL=schema-version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-version.d.ts","sourceRoot":"","sources":["../../src/utils/schema-version.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAG5C,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;IAC9D,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,KAAK,mBAAmB,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;AAG9E,wBAAgB,sBAAsB,CACpC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,GAChD,MAAM,GAAG,IAAI,CAgBf;AAGD,wBAAgB,sBAAsB,CACpC,MAAM,CAAC,EAAE,mBAAmB,GAC3B,mBAAmB,CA8CrB"}