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.
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -0
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +25 -1
- package/dist/commands/migrate.d.ts +9 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +183 -0
- package/dist/commands/sync/staging.d.ts +3 -0
- package/dist/commands/sync/staging.d.ts.map +1 -1
- package/dist/commands/sync/staging.js +58 -1
- package/dist/commands/sync.d.ts +18 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +19 -4
- package/dist/public/check-types.d.ts +3 -1
- package/dist/public/check-types.d.ts.map +1 -1
- package/dist/public/check-types.js +1 -0
- package/dist/public/ignore-policy.d.ts +10 -0
- package/dist/public/ignore-policy.d.ts.map +1 -0
- package/dist/public/ignore-policy.js +219 -0
- package/dist/public/operational-artifacts.d.ts +2 -0
- package/dist/public/operational-artifacts.d.ts.map +1 -0
- package/dist/public/operational-artifacts.js +4 -0
- package/dist/public/schema-version.d.ts +3 -0
- package/dist/public/schema-version.d.ts.map +1 -0
- package/dist/public/schema-version.js +1 -0
- package/dist/search-ranking.d.ts.map +1 -1
- package/dist/search-ranking.js +132 -25
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +35 -22
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/dist/utils/rule-registry.js +6 -0
- package/dist/utils/schema-version.d.ts +14 -0
- package/dist/utils/schema-version.d.ts.map +1 -0
- package/dist/utils/schema-version.js +59 -0
- package/dist/utils/strict-modeling.d.ts +64 -0
- package/dist/utils/strict-modeling.d.ts.map +1 -0
- package/dist/utils/strict-modeling.js +371 -0
- package/package.json +17 -3
- package/schema/config.json +8 -1
- package/src/public/check-types.ts +15 -1
- package/src/public/ignore-policy.ts +229 -0
- package/src/public/operational-artifacts.ts +5 -0
- 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 @@
|
|
|
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 @@
|
|
|
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;
|
|
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"}
|
package/dist/search-ranking.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
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 (
|
|
82
|
+
else if (isPhraseSearchMatch(titleForms, queryContext.phrase)) {
|
|
47
83
|
score += 60;
|
|
48
84
|
reasons.push("title phrase match");
|
|
49
85
|
}
|
|
50
|
-
if (
|
|
86
|
+
if (isExactSearchMatch(idForms, queryContext.phrase)) {
|
|
51
87
|
score += 90;
|
|
52
88
|
reasons.push("exact ID match");
|
|
53
89
|
}
|
|
54
|
-
else if (
|
|
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) =>
|
|
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) =>
|
|
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(
|
|
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
|
|
78
|
-
if (
|
|
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,
|
|
118
|
+
snippet = buildSnippet(bodyText, queryContext.phrase);
|
|
82
119
|
}
|
|
83
120
|
else {
|
|
84
|
-
const bodyTokenMatches = countTokenMatches(
|
|
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,
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/dist/utils/config.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|