kibi-cli 0.8.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/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/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/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 +13 -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/schema-version.ts +6 -0
package/schema/config.json
CHANGED
|
@@ -186,6 +186,11 @@
|
|
|
186
186
|
"type": "boolean",
|
|
187
187
|
"description": "Detect requirements with incomplete strict subject/property fact pairing for contradiction-safe semantics",
|
|
188
188
|
"default": false
|
|
189
|
+
},
|
|
190
|
+
"strict-readiness": {
|
|
191
|
+
"type": "boolean",
|
|
192
|
+
"description": "Report strict contradiction-readiness levels for requirements that are still prose-only or otherwise not contradiction-ready",
|
|
193
|
+
"default": false
|
|
189
194
|
}
|
|
190
195
|
},
|
|
191
196
|
"additionalProperties": false
|
|
@@ -229,7 +234,9 @@
|
|
|
229
234
|
"required-fields": true,
|
|
230
235
|
"deprecated-adr-no-successor": true,
|
|
231
236
|
"domain-contradictions": true,
|
|
232
|
-
"strict-fact-shape": false
|
|
237
|
+
"strict-fact-shape": false,
|
|
238
|
+
"strict-req-fact-pairing": false,
|
|
239
|
+
"strict-readiness": false
|
|
233
240
|
},
|
|
234
241
|
"symbolTraceability": {
|
|
235
242
|
"requireAdr": false
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Public re-export barrel for shared check types.
|
|
20
|
+
* Public re-export barrel for shared check types and MCP-consumed modeling helpers.
|
|
21
21
|
* Import from "kibi-cli/public/check-types" in MCP or external consumers.
|
|
22
22
|
*/
|
|
23
23
|
export type {
|
|
@@ -26,6 +26,12 @@ export type {
|
|
|
26
26
|
SymbolTraceabilityOptions,
|
|
27
27
|
Violation,
|
|
28
28
|
} from "../utils/rule-registry.js";
|
|
29
|
+
export type {
|
|
30
|
+
SemanticClaim,
|
|
31
|
+
StableRequirementIds,
|
|
32
|
+
StrictModelInput,
|
|
33
|
+
StrictWriteSet,
|
|
34
|
+
} from "../utils/strict-modeling.js";
|
|
29
35
|
|
|
30
36
|
export {
|
|
31
37
|
DEFAULT_CHECKS_CONFIG,
|
|
@@ -35,3 +41,11 @@ export {
|
|
|
35
41
|
mergeChecksConfig,
|
|
36
42
|
validateRuleName,
|
|
37
43
|
} from "../utils/rule-registry.js";
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
buildStableRequirementIds,
|
|
47
|
+
buildStrictWriteSet,
|
|
48
|
+
modelRequirementClaims,
|
|
49
|
+
normalizePropertyKey,
|
|
50
|
+
normalizeSubjectKey,
|
|
51
|
+
} from "../utils/strict-modeling.js";
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import ignore from "ignore";
|
|
4
|
+
|
|
5
|
+
const HARD_DENYLIST = [
|
|
6
|
+
".kb",
|
|
7
|
+
".git",
|
|
8
|
+
"node_modules",
|
|
9
|
+
"vendor",
|
|
10
|
+
"third_party",
|
|
11
|
+
".sisyphus",
|
|
12
|
+
".opencode",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface IgnorePolicy {
|
|
16
|
+
isIgnored(inputPath: string): boolean;
|
|
17
|
+
getFastGlobIgnoreGlobs(): string[];
|
|
18
|
+
explain(inputPath: string): { ignored: boolean; reason?: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readIgnoreFileLines(filePath: string): string[] {
|
|
22
|
+
if (!existsSync(filePath)) return [];
|
|
23
|
+
try {
|
|
24
|
+
const content = readFileSync(filePath, "utf8");
|
|
25
|
+
return content
|
|
26
|
+
.split(/\r?\n/)
|
|
27
|
+
.map((l) => l.trim())
|
|
28
|
+
.filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function toPosix(p: string): string {
|
|
35
|
+
return p.split(path.sep).join("/");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// implements REQ-001
|
|
39
|
+
export function createRepoIgnorePolicy(workspaceRoot: string): IgnorePolicy {
|
|
40
|
+
const root = path.resolve(workspaceRoot);
|
|
41
|
+
|
|
42
|
+
// Load root .gitignore
|
|
43
|
+
const rootGitignorePath = path.join(root, ".gitignore");
|
|
44
|
+
const rootGitPatterns = readIgnoreFileLines(rootGitignorePath);
|
|
45
|
+
|
|
46
|
+
// Load .git/info/exclude
|
|
47
|
+
const gitInfoExcludePath = path.join(root, ".git", "info", "exclude");
|
|
48
|
+
const gitInfoPatterns = readIgnoreFileLines(gitInfoExcludePath);
|
|
49
|
+
|
|
50
|
+
// Find nested .gitignore files (skip scanning inside hard denylist directories)
|
|
51
|
+
const nestedPatterns = new Map<string, string[]>();
|
|
52
|
+
|
|
53
|
+
function walk(dirAbs: string) {
|
|
54
|
+
let entries;
|
|
55
|
+
try {
|
|
56
|
+
entries = readdirSync(dirAbs, { withFileTypes: true });
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
for (const ent of entries) {
|
|
61
|
+
const name = String(ent.name);
|
|
62
|
+
const abs = path.join(dirAbs, name);
|
|
63
|
+
if (ent.isDirectory()) {
|
|
64
|
+
// avoid descending into common heavy or control directories
|
|
65
|
+
if (HARD_DENYLIST.includes(name)) continue;
|
|
66
|
+
// also avoid .git itself to prevent reading internal excludes as nested
|
|
67
|
+
if (name === ".git") continue;
|
|
68
|
+
walk(abs);
|
|
69
|
+
} else if (ent.isFile()) {
|
|
70
|
+
if (name === ".gitignore") {
|
|
71
|
+
// skip root .gitignore (we already loaded it)
|
|
72
|
+
if (path.resolve(dirAbs) === root) continue;
|
|
73
|
+
const patterns = readIgnoreFileLines(abs);
|
|
74
|
+
const relDir = path.relative(root, dirAbs) || ".";
|
|
75
|
+
nestedPatterns.set(toPosix(relDir), patterns);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
walk(root);
|
|
82
|
+
|
|
83
|
+
// Create ignore instances
|
|
84
|
+
const rootIgnore = ignore();
|
|
85
|
+
if (rootGitPatterns.length > 0) rootIgnore.add(rootGitPatterns);
|
|
86
|
+
|
|
87
|
+
const gitInfoIgnore = ignore();
|
|
88
|
+
if (gitInfoPatterns.length > 0) gitInfoIgnore.add(gitInfoPatterns);
|
|
89
|
+
|
|
90
|
+
const nestedIgnoreMap = new Map<string, ReturnType<typeof ignore>>();
|
|
91
|
+
for (const [dirRel, pats] of nestedPatterns.entries()) {
|
|
92
|
+
const ig = ignore();
|
|
93
|
+
if (pats.length > 0) ig.add(pats);
|
|
94
|
+
nestedIgnoreMap.set(dirRel, ig);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Prepare nested directories sorted by specificity (longest first)
|
|
98
|
+
const nestedDirsSorted = Array.from(nestedIgnoreMap.keys()).sort((a, b) => b.length - a.length);
|
|
99
|
+
|
|
100
|
+
function isPathOutsideWorkspace(absPath: string): boolean {
|
|
101
|
+
const rel = path.relative(root, absPath);
|
|
102
|
+
// path.relative returns paths starting with '..' for outside
|
|
103
|
+
return rel === "" ? false : rel.split(path.sep)[0] === "..";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function matchesHardDeny(relPosix: string): boolean {
|
|
107
|
+
const segments = relPosix.split("/").filter(Boolean);
|
|
108
|
+
for (const deny of HARD_DENYLIST) {
|
|
109
|
+
if (segments.includes(deny)) return true;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isIgnoredInternal(inputPath: string): { ignored: boolean; reason?: string } {
|
|
115
|
+
// Resolve to absolute and relative path inside workspace
|
|
116
|
+
const abs = path.isAbsolute(inputPath) ? path.resolve(inputPath) : path.resolve(root, inputPath);
|
|
117
|
+
|
|
118
|
+
if (path.isAbsolute(inputPath) && isPathOutsideWorkspace(abs)) {
|
|
119
|
+
return { ignored: true, reason: "outside_workspace" };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const rel = path.relative(root, abs) || ".";
|
|
123
|
+
const relPosix = toPosix(rel);
|
|
124
|
+
|
|
125
|
+
// Hard denylist always wins
|
|
126
|
+
if (matchesHardDeny(relPosix)) return { ignored: true, reason: "hard_deny" };
|
|
127
|
+
|
|
128
|
+
// Root .gitignore
|
|
129
|
+
try {
|
|
130
|
+
if (rootGitPatterns.length > 0 && rootIgnore.ignores(relPosix)) {
|
|
131
|
+
return { ignored: true, reason: "gitignored" };
|
|
132
|
+
}
|
|
133
|
+
} catch (e) {
|
|
134
|
+
// ignore errors from library usage; continue
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// .git/info/exclude
|
|
138
|
+
try {
|
|
139
|
+
if (gitInfoPatterns.length > 0 && gitInfoIgnore.ignores(relPosix)) {
|
|
140
|
+
return { ignored: true, reason: "git_info_exclude" };
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// noop
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Nested .gitignore (apply relative to their directory)
|
|
147
|
+
for (const dirRel of nestedDirsSorted) {
|
|
148
|
+
// dirRel is '.' for nested at root which we skipped, so dirRel will be like 'docs'
|
|
149
|
+
if (dirRel === ".") continue;
|
|
150
|
+
if (relPosix === dirRel || relPosix.startsWith(dirRel + "/")) {
|
|
151
|
+
const sub = relPosix === dirRel ? "." : relPosix.slice(dirRel.length + 1);
|
|
152
|
+
const ig = nestedIgnoreMap.get(dirRel)!;
|
|
153
|
+
try {
|
|
154
|
+
if (ig && ig.ignores(sub)) return { ignored: true, reason: "gitignored" };
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// noop
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { ignored: false };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getFastGlobIgnoreGlobs(): string[] {
|
|
165
|
+
const globs: string[] = [];
|
|
166
|
+
|
|
167
|
+
// Hard denylist globs
|
|
168
|
+
for (const d of HARD_DENYLIST) {
|
|
169
|
+
// match directory and its contents anywhere
|
|
170
|
+
globs.push(`**/${d}/**`);
|
|
171
|
+
globs.push(`**/${d}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Root .gitignore patterns (convert to simple globs)
|
|
175
|
+
for (const p of rootGitPatterns) {
|
|
176
|
+
if (!p || p.startsWith("#") || p.startsWith("!")) continue;
|
|
177
|
+
let pat = p;
|
|
178
|
+
if (pat.startsWith("/")) pat = pat.slice(1);
|
|
179
|
+
if (pat.includes("/")) {
|
|
180
|
+
// anchored path
|
|
181
|
+
globs.push(`**/${toPosix(pat)}`);
|
|
182
|
+
} else {
|
|
183
|
+
globs.push(`**/${pat}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// .git/info/exclude patterns
|
|
188
|
+
for (const p of gitInfoPatterns) {
|
|
189
|
+
if (!p || p.startsWith("#") || p.startsWith("!")) continue;
|
|
190
|
+
let pat = p;
|
|
191
|
+
if (pat.startsWith("/")) pat = pat.slice(1);
|
|
192
|
+
if (pat.includes("/")) {
|
|
193
|
+
globs.push(`**/${toPosix(pat)}`);
|
|
194
|
+
} else {
|
|
195
|
+
globs.push(`**/${pat}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Nested .gitignore patterns - prefix with directory path
|
|
200
|
+
// Use the raw patterns collected in nestedPatterns so we scope patterns
|
|
201
|
+
// to the nested directory instead of ignoring the entire directory.
|
|
202
|
+
// Debug: print nested patterns and the computed globs to help diagnosing test failures.
|
|
203
|
+
for (const [dirRel, patterns] of nestedPatterns.entries()) {
|
|
204
|
+
for (const p of patterns) {
|
|
205
|
+
if (!p || p.startsWith("#") || p.startsWith("!")) continue;
|
|
206
|
+
let pat = p;
|
|
207
|
+
if (pat.startsWith("/")) pat = pat.slice(1);
|
|
208
|
+
const prefix = dirRel === "." ? "" : `${dirRel}/`;
|
|
209
|
+
if (pat.includes("/")) {
|
|
210
|
+
globs.push(`**/${prefix}${toPosix(pat)}`);
|
|
211
|
+
} else {
|
|
212
|
+
globs.push(`**/${prefix}${pat}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return Array.from(new Set(globs));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
isIgnored(inputPath: string) {
|
|
222
|
+
return isIgnoredInternal(inputPath).ignored;
|
|
223
|
+
},
|
|
224
|
+
getFastGlobIgnoreGlobs,
|
|
225
|
+
explain(inputPath: string) {
|
|
226
|
+
return isIgnoredInternal(inputPath);
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|