@kb-labs/policy-core 0.5.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/README.md +120 -0
- package/dist/cli/commands/check.d.ts +9 -0
- package/dist/cli/commands/check.js +646 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/commands/detect.d.ts +9 -0
- package/dist/cli/commands/detect.js +103 -0
- package/dist/cli/commands/detect.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +8 -0
- package/dist/cli/commands/rules.js +49 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/snapshot.d.ts +8 -0
- package/dist/cli/commands/snapshot.js +167 -0
- package/dist/cli/commands/snapshot.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +652 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.v3.d.ts +53 -0
- package/dist/manifest.v3.js +111 -0
- package/dist/manifest.v3.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path4 from 'path';
|
|
3
|
+
import { spawnSync } from 'child_process';
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
import { combinePermissions, defineCommandFlags } from '@kb-labs/sdk';
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/core/category-resolver.ts
|
|
18
|
+
var category_resolver_exports = {};
|
|
19
|
+
__export(category_resolver_exports, {
|
|
20
|
+
detectCategories: () => detectCategories,
|
|
21
|
+
detectCategory: () => detectCategory
|
|
22
|
+
});
|
|
23
|
+
function matchesPattern(repoPath, pattern) {
|
|
24
|
+
const normalizedPath = repoPath.replace(/\/$/, "");
|
|
25
|
+
const normalizedPattern = pattern.replace(/\/$/, "");
|
|
26
|
+
if (normalizedPattern === normalizedPath) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (normalizedPattern.endsWith("/*")) {
|
|
30
|
+
const prefix = normalizedPattern.slice(0, -2);
|
|
31
|
+
const pathPrefix = normalizedPath.split("/").slice(0, prefix.split("/").length).join("/");
|
|
32
|
+
return pathPrefix === prefix;
|
|
33
|
+
}
|
|
34
|
+
if (normalizedPattern.includes("*")) {
|
|
35
|
+
const regexStr = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]+");
|
|
36
|
+
return new RegExp(`^${regexStr}$`).test(normalizedPath);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function detectCategory(repoPath, config) {
|
|
41
|
+
for (const [categoryName, categoryConfig] of Object.entries(config.categories)) {
|
|
42
|
+
for (const pattern of categoryConfig.paths) {
|
|
43
|
+
if (matchesPattern(repoPath, pattern)) {
|
|
44
|
+
return {
|
|
45
|
+
path: repoPath,
|
|
46
|
+
category: categoryName,
|
|
47
|
+
rules: categoryConfig.rules
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
path: repoPath,
|
|
54
|
+
category: null,
|
|
55
|
+
rules: ["boundary-check"]
|
|
56
|
+
// default fallback rule
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function detectCategories(repoPaths, config) {
|
|
60
|
+
return repoPaths.map((p) => detectCategory(p, config));
|
|
61
|
+
}
|
|
62
|
+
var init_category_resolver = __esm({
|
|
63
|
+
"src/core/category-resolver.ts"() {
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// src/core/index.ts
|
|
68
|
+
init_category_resolver();
|
|
69
|
+
|
|
70
|
+
// src/core/workspace-scanner.ts
|
|
71
|
+
init_category_resolver();
|
|
72
|
+
function buildPackageMap(workspaceRoot, config) {
|
|
73
|
+
const packages = scanAllPackages(workspaceRoot, config);
|
|
74
|
+
const map = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const pkg of packages) {
|
|
76
|
+
map.set(pkg.name, pkg.category);
|
|
77
|
+
}
|
|
78
|
+
return map;
|
|
79
|
+
}
|
|
80
|
+
function getRepoPackageNames(workspaceRoot, repoPath) {
|
|
81
|
+
const absRepoPath = path4.resolve(workspaceRoot, repoPath);
|
|
82
|
+
const names = [];
|
|
83
|
+
for (const subdir of ["packages", "apps"]) {
|
|
84
|
+
const subdirPath = path4.join(absRepoPath, subdir);
|
|
85
|
+
if (!fs.existsSync(subdirPath)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for (const entry of fs.readdirSync(subdirPath)) {
|
|
89
|
+
const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
|
|
90
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
95
|
+
if (json.name) {
|
|
96
|
+
names.push(json.name);
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return names;
|
|
103
|
+
}
|
|
104
|
+
function scanAllPackages(workspaceRoot, config) {
|
|
105
|
+
const results = [];
|
|
106
|
+
const topLevelDirs = ["platform", "plugins", "infra", "templates", "installer", "sites"];
|
|
107
|
+
for (const topDir of topLevelDirs) {
|
|
108
|
+
const topDirPath = path4.join(workspaceRoot, topDir);
|
|
109
|
+
if (!fs.existsSync(topDirPath)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
for (const repoEntry of fs.readdirSync(topDirPath)) {
|
|
113
|
+
const repoPath = path4.join(topDirPath, repoEntry);
|
|
114
|
+
const repoRelPath = `${topDir}/${repoEntry}`;
|
|
115
|
+
let isDir;
|
|
116
|
+
try {
|
|
117
|
+
isDir = fs.statSync(repoPath).isDirectory();
|
|
118
|
+
} catch {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!isDir) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const categoryResult = detectCategory(repoRelPath, config);
|
|
125
|
+
for (const subdir of ["packages", "apps"]) {
|
|
126
|
+
const subdirPath = path4.join(repoPath, subdir);
|
|
127
|
+
if (!fs.existsSync(subdirPath)) {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
for (const pkgEntry of fs.readdirSync(subdirPath)) {
|
|
131
|
+
const pkgJsonPath = path4.join(subdirPath, pkgEntry, "package.json");
|
|
132
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
137
|
+
if (json.name) {
|
|
138
|
+
results.push({
|
|
139
|
+
name: json.name,
|
|
140
|
+
repoPath: repoRelPath,
|
|
141
|
+
category: categoryResult.category,
|
|
142
|
+
packageJsonPath: pkgJsonPath
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
async function checkSdkOnlyDeps(repoPath, _config, workspaceRoot) {
|
|
154
|
+
const violations = [];
|
|
155
|
+
const absRepoPath = path4.resolve(workspaceRoot, repoPath);
|
|
156
|
+
const internalPackageNames = new Set(getRepoPackageNames(workspaceRoot, repoPath));
|
|
157
|
+
const packageJsonPaths = findPackageJsonPaths(absRepoPath);
|
|
158
|
+
for (const pkgJsonPath of packageJsonPaths) {
|
|
159
|
+
let json;
|
|
160
|
+
try {
|
|
161
|
+
json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
162
|
+
} catch {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const deps = Object.keys(json.dependencies ?? {});
|
|
166
|
+
const packageName = json.name ?? path4.basename(path4.dirname(pkgJsonPath));
|
|
167
|
+
const relPath = path4.relative(workspaceRoot, pkgJsonPath);
|
|
168
|
+
for (const dep of deps) {
|
|
169
|
+
if (!dep.startsWith("@kb-labs/")) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (dep === "@kb-labs/sdk") {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (internalPackageNames.has(dep)) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
violations.push({
|
|
179
|
+
rule: "sdk-only-deps",
|
|
180
|
+
severity: "error",
|
|
181
|
+
message: `${packageName} imports ${dep} directly`,
|
|
182
|
+
package: packageName,
|
|
183
|
+
detail: `Plugins must depend only on @kb-labs/sdk. Move needed types to SDK or use SDK re-exports.`,
|
|
184
|
+
file: relPath
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return violations;
|
|
189
|
+
}
|
|
190
|
+
function findPackageJsonPaths(absRepoPath) {
|
|
191
|
+
const results = [];
|
|
192
|
+
for (const subdir of ["packages", "apps"]) {
|
|
193
|
+
const subdirPath = path4.join(absRepoPath, subdir);
|
|
194
|
+
if (!fs.existsSync(subdirPath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const entry of fs.readdirSync(subdirPath)) {
|
|
198
|
+
const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
|
|
199
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
200
|
+
results.push(pkgJsonPath);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return results;
|
|
205
|
+
}
|
|
206
|
+
async function checkBoundary(repoPath, config, workspaceRoot) {
|
|
207
|
+
const violations = [];
|
|
208
|
+
const { detectCategory: detectCategory2 } = await Promise.resolve().then(() => (init_category_resolver(), category_resolver_exports));
|
|
209
|
+
const categoryResult = detectCategory2(repoPath, config);
|
|
210
|
+
const category = categoryResult.category;
|
|
211
|
+
if (!category) {
|
|
212
|
+
return violations;
|
|
213
|
+
}
|
|
214
|
+
const boundaryRuleConfig = config.rules["boundary-check"]?.config;
|
|
215
|
+
const allowedCategories = boundaryRuleConfig?.allowed?.[category] ?? [];
|
|
216
|
+
if (allowedCategories.includes("sdk-only")) {
|
|
217
|
+
return checkSdkOnlyDeps(repoPath, config, workspaceRoot);
|
|
218
|
+
}
|
|
219
|
+
const packageMap = buildPackageMap(workspaceRoot, config);
|
|
220
|
+
const internalPackageNames = new Set(getRepoPackageNames(workspaceRoot, repoPath));
|
|
221
|
+
const absRepoPath = path4.resolve(workspaceRoot, repoPath);
|
|
222
|
+
for (const pkgJsonPath of findPackageJsonPaths2(absRepoPath)) {
|
|
223
|
+
let json;
|
|
224
|
+
try {
|
|
225
|
+
json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
226
|
+
} catch {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
const deps = Object.keys(json.dependencies ?? {});
|
|
230
|
+
const packageName = json.name ?? path4.basename(path4.dirname(pkgJsonPath));
|
|
231
|
+
const relPath = path4.relative(workspaceRoot, pkgJsonPath);
|
|
232
|
+
for (const dep of deps) {
|
|
233
|
+
if (!dep.startsWith("@kb-labs/")) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (internalPackageNames.has(dep)) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const depCategory = packageMap.get(dep);
|
|
240
|
+
if (depCategory === void 0 || depCategory === null) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!allowedCategories.includes(depCategory)) {
|
|
244
|
+
violations.push({
|
|
245
|
+
rule: "boundary-check",
|
|
246
|
+
severity: "error",
|
|
247
|
+
message: `${packageName} depends on ${dep} (category: ${depCategory ?? "unknown"})`,
|
|
248
|
+
package: packageName,
|
|
249
|
+
detail: `Category "${category}" may only depend on: ${allowedCategories.join(", ")}`,
|
|
250
|
+
file: relPath
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return violations;
|
|
256
|
+
}
|
|
257
|
+
function findPackageJsonPaths2(absRepoPath) {
|
|
258
|
+
const results = [];
|
|
259
|
+
for (const subdir of ["packages", "apps"]) {
|
|
260
|
+
const subdirPath = path4.join(absRepoPath, subdir);
|
|
261
|
+
if (!fs.existsSync(subdirPath)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
for (const entry of fs.readdirSync(subdirPath)) {
|
|
265
|
+
const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
|
|
266
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
267
|
+
results.push(pkgJsonPath);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/checks/symbol-extractor.ts
|
|
275
|
+
var RegexSymbolExtractor = class {
|
|
276
|
+
extract(dtsContent) {
|
|
277
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
278
|
+
for (const m of dtsContent.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)/gm)) {
|
|
279
|
+
symbols.add(m[1]);
|
|
280
|
+
}
|
|
281
|
+
for (const m of dtsContent.matchAll(/^export\s+(?:abstract\s+)?class\s+(\w+)/gm)) {
|
|
282
|
+
symbols.add(m[1]);
|
|
283
|
+
}
|
|
284
|
+
for (const m of dtsContent.matchAll(/^export\s+(?:type\s+|interface\s+)(\w+)/gm)) {
|
|
285
|
+
symbols.add(m[1]);
|
|
286
|
+
}
|
|
287
|
+
for (const m of dtsContent.matchAll(/^export\s+(?:const|let|var)\s+(\w+)/gm)) {
|
|
288
|
+
symbols.add(m[1]);
|
|
289
|
+
}
|
|
290
|
+
for (const m of dtsContent.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)/gm)) {
|
|
291
|
+
symbols.add(m[1]);
|
|
292
|
+
}
|
|
293
|
+
for (const m of dtsContent.matchAll(/^export\s+\{([^}]+)\}/gm)) {
|
|
294
|
+
const entries = m[1].split(",").map((s) => s.trim());
|
|
295
|
+
for (const entry of entries) {
|
|
296
|
+
const asMatch = entry.match(/\w+\s+as\s+(\w+)/);
|
|
297
|
+
if (asMatch) {
|
|
298
|
+
symbols.add(asMatch[1]);
|
|
299
|
+
} else {
|
|
300
|
+
const nameMatch = entry.match(/^(\w+)/);
|
|
301
|
+
if (nameMatch) {
|
|
302
|
+
symbols.add(nameMatch[1]);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (/^export\s+default\s+/m.test(dtsContent)) {
|
|
308
|
+
symbols.add("default");
|
|
309
|
+
}
|
|
310
|
+
return symbols;
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
var defaultSymbolExtractor = new RegexSymbolExtractor();
|
|
314
|
+
|
|
315
|
+
// src/checks/api-compat-check.ts
|
|
316
|
+
var SNAPSHOTS_DIR = ".kb/api-snapshots";
|
|
317
|
+
async function checkApiCompat(repoPath, _config, workspaceRoot) {
|
|
318
|
+
const violations = [];
|
|
319
|
+
const absRepoPath = path4.resolve(workspaceRoot, repoPath);
|
|
320
|
+
const packageJsonPaths = findPackageJsonPaths3(absRepoPath);
|
|
321
|
+
for (const pkgJsonPath of packageJsonPaths) {
|
|
322
|
+
let json;
|
|
323
|
+
try {
|
|
324
|
+
json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
325
|
+
} catch {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (!json.name || !json.version) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const pkgName = json.name;
|
|
332
|
+
const currentVersion = json.version;
|
|
333
|
+
const snapshotPath = getSnapshotPath(workspaceRoot, pkgName);
|
|
334
|
+
const pkgDir = path4.dirname(pkgJsonPath);
|
|
335
|
+
const currentSymbols = extractSymbolsFromDist(pkgDir);
|
|
336
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
337
|
+
console.warn(
|
|
338
|
+
`[policy] api-compat: No snapshot for ${pkgName} \u2014 run 'policy update-snapshots' to create one.`
|
|
339
|
+
);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
let snapshot;
|
|
343
|
+
try {
|
|
344
|
+
snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf-8"));
|
|
345
|
+
} catch {
|
|
346
|
+
console.warn(`[policy] api-compat: Could not read snapshot for ${pkgName}, skipping.`);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const snapshotSymbols = new Set(snapshot.symbols);
|
|
350
|
+
const relPath = path4.relative(workspaceRoot, pkgJsonPath);
|
|
351
|
+
const removed = [];
|
|
352
|
+
for (const sym of snapshotSymbols) {
|
|
353
|
+
if (!currentSymbols.has(sym)) {
|
|
354
|
+
removed.push(sym);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (removed.length > 0) {
|
|
358
|
+
const currentMajor = parseMajor(currentVersion);
|
|
359
|
+
const snapshotMajor = parseMajor(snapshot.version);
|
|
360
|
+
if (currentMajor <= snapshotMajor) {
|
|
361
|
+
violations.push({
|
|
362
|
+
rule: "api-compat-check",
|
|
363
|
+
severity: "error",
|
|
364
|
+
message: `${pkgName} removed exported symbols without major version bump`,
|
|
365
|
+
package: pkgName,
|
|
366
|
+
detail: `Removed: ${removed.join(", ")}. Bump major version (current: ${currentVersion}, snapshot: ${snapshot.version}) or restore symbols.`,
|
|
367
|
+
file: relPath
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return violations;
|
|
373
|
+
}
|
|
374
|
+
function extractSymbolsFromDist(pkgDir) {
|
|
375
|
+
const distDir = path4.join(pkgDir, "dist");
|
|
376
|
+
const symbols = /* @__PURE__ */ new Set();
|
|
377
|
+
if (!fs.existsSync(distDir)) {
|
|
378
|
+
return symbols;
|
|
379
|
+
}
|
|
380
|
+
const dtsFiles = findDtsFiles(distDir);
|
|
381
|
+
for (const dtsFile of dtsFiles) {
|
|
382
|
+
const content = fs.readFileSync(dtsFile, "utf-8");
|
|
383
|
+
for (const sym of defaultSymbolExtractor.extract(content)) {
|
|
384
|
+
symbols.add(sym);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return symbols;
|
|
388
|
+
}
|
|
389
|
+
function findDtsFiles(dir) {
|
|
390
|
+
const results = [];
|
|
391
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
392
|
+
const fullPath = path4.join(dir, entry.name);
|
|
393
|
+
if (entry.isDirectory()) {
|
|
394
|
+
results.push(...findDtsFiles(fullPath));
|
|
395
|
+
} else if (entry.name.endsWith(".d.ts")) {
|
|
396
|
+
results.push(fullPath);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return results;
|
|
400
|
+
}
|
|
401
|
+
function getSnapshotPath(workspaceRoot, packageName) {
|
|
402
|
+
const safeName = packageName.replace(/\//g, "__").replace(/@/g, "");
|
|
403
|
+
const snapshotsDir = path4.join(workspaceRoot, SNAPSHOTS_DIR);
|
|
404
|
+
if (!fs.existsSync(snapshotsDir)) {
|
|
405
|
+
fs.mkdirSync(snapshotsDir, { recursive: true });
|
|
406
|
+
}
|
|
407
|
+
return path4.join(snapshotsDir, `${safeName}.json`);
|
|
408
|
+
}
|
|
409
|
+
function parseMajor(version) {
|
|
410
|
+
return parseInt(version.split(".")[0] ?? "0", 10);
|
|
411
|
+
}
|
|
412
|
+
function findPackageJsonPaths3(absRepoPath) {
|
|
413
|
+
const results = [];
|
|
414
|
+
for (const subdir of ["packages", "apps"]) {
|
|
415
|
+
const subdirPath = path4.join(absRepoPath, subdir);
|
|
416
|
+
if (!fs.existsSync(subdirPath)) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
for (const entry of fs.readdirSync(subdirPath)) {
|
|
420
|
+
const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
|
|
421
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
422
|
+
results.push(pkgJsonPath);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return results;
|
|
427
|
+
}
|
|
428
|
+
async function checkNoRollback(repoPath, _config, workspaceRoot) {
|
|
429
|
+
const violations = [];
|
|
430
|
+
const absRepoPath = path4.resolve(workspaceRoot, repoPath);
|
|
431
|
+
const packageJsonPaths = findPackageJsonPaths4(absRepoPath);
|
|
432
|
+
for (const pkgJsonPath of packageJsonPaths) {
|
|
433
|
+
let json;
|
|
434
|
+
try {
|
|
435
|
+
json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
436
|
+
} catch {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
if (!json.name || !json.version) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (json.private) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const packageName = json.name;
|
|
446
|
+
const currentVersion = json.version;
|
|
447
|
+
const relPath = path4.relative(workspaceRoot, pkgJsonPath);
|
|
448
|
+
const publishedVersion = getPublishedVersion(packageName);
|
|
449
|
+
if (publishedVersion === null) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (!semver.gte(currentVersion, publishedVersion)) {
|
|
453
|
+
violations.push({
|
|
454
|
+
rule: "no-rollback",
|
|
455
|
+
severity: "error",
|
|
456
|
+
message: `${packageName} version ${currentVersion} is less than published ${publishedVersion}`,
|
|
457
|
+
package: packageName,
|
|
458
|
+
detail: `Cannot decrease version once published to npm. Restore to ${publishedVersion} or higher.`,
|
|
459
|
+
file: relPath
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return violations;
|
|
464
|
+
}
|
|
465
|
+
function getPublishedVersion(packageName) {
|
|
466
|
+
const result = spawnSync("npm", ["show", packageName, "version", "--silent"], {
|
|
467
|
+
timeout: 1e4,
|
|
468
|
+
encoding: "utf-8",
|
|
469
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
470
|
+
});
|
|
471
|
+
if (result.error || result.status !== 0) {
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
return semver.valid(result.stdout.trim()) ?? null;
|
|
475
|
+
}
|
|
476
|
+
function findPackageJsonPaths4(absRepoPath) {
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const subdir of ["packages", "apps"]) {
|
|
479
|
+
const subdirPath = path4.join(absRepoPath, subdir);
|
|
480
|
+
if (!fs.existsSync(subdirPath)) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
for (const entry of fs.readdirSync(subdirPath)) {
|
|
484
|
+
const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
|
|
485
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
486
|
+
results.push(pkgJsonPath);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return results;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/core/policy-runner.ts
|
|
494
|
+
var RULE_CHECKS = {
|
|
495
|
+
"sdk-only-deps": checkSdkOnlyDeps,
|
|
496
|
+
"boundary-check": checkBoundary,
|
|
497
|
+
"no-breaking-without-major": checkApiCompat,
|
|
498
|
+
"no-rollback": checkNoRollback,
|
|
499
|
+
"api-compat-check": checkApiCompat
|
|
500
|
+
};
|
|
501
|
+
async function runChecks(repos, config, workspaceRoot) {
|
|
502
|
+
const repoResults = [];
|
|
503
|
+
for (const repo of repos) {
|
|
504
|
+
const violations = [];
|
|
505
|
+
const passed = [];
|
|
506
|
+
const rulesToRun = repo.rules;
|
|
507
|
+
for (const rule of rulesToRun) {
|
|
508
|
+
const checkFn = RULE_CHECKS[rule];
|
|
509
|
+
if (!checkFn) {
|
|
510
|
+
console.warn(`[policy] Unknown rule: ${rule} (skipping)`);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
let ruleViolations;
|
|
514
|
+
try {
|
|
515
|
+
ruleViolations = await checkFn(repo.path, config, workspaceRoot);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
console.warn(`[policy] Rule '${rule}' threw an unexpected error (skipping): ${err.message}`);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (ruleViolations.length === 0) {
|
|
521
|
+
passed.push(rule);
|
|
522
|
+
} else {
|
|
523
|
+
violations.push(...ruleViolations.map((v) => ({ ...v, rule })));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
repoResults.push({
|
|
527
|
+
path: repo.path,
|
|
528
|
+
category: repo.category,
|
|
529
|
+
violations,
|
|
530
|
+
passed
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const totalViolations = repoResults.reduce((sum, r) => sum + r.violations.length, 0);
|
|
534
|
+
const failedRepos = repoResults.filter((r) => r.violations.length > 0).length;
|
|
535
|
+
return {
|
|
536
|
+
passed: totalViolations === 0,
|
|
537
|
+
repos: repoResults,
|
|
538
|
+
summary: {
|
|
539
|
+
total: repos.length,
|
|
540
|
+
passed: repos.length - failedRepos,
|
|
541
|
+
failed: failedRepos,
|
|
542
|
+
violations: totalViolations
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
var pluginPermissions = combinePermissions().withFs({
|
|
547
|
+
mode: "read",
|
|
548
|
+
allow: [".", ".kb/api-snapshots/**"]
|
|
549
|
+
}).withQuotas({
|
|
550
|
+
timeoutMs: 6e4,
|
|
551
|
+
memoryMb: 256
|
|
552
|
+
}).build();
|
|
553
|
+
var detectPermissions = combinePermissions().withFs({ mode: "read", allow: ["."] }).withQuotas({ timeoutMs: 15e3, memoryMb: 128 }).build();
|
|
554
|
+
var checkPermissions = combinePermissions().withFs({
|
|
555
|
+
mode: "read",
|
|
556
|
+
allow: [".", ".kb/api-snapshots/**"]
|
|
557
|
+
}).withFs({
|
|
558
|
+
mode: "readWrite",
|
|
559
|
+
allow: [".kb/api-snapshots/**"]
|
|
560
|
+
}).withQuotas({ timeoutMs: 6e4, memoryMb: 256 }).build();
|
|
561
|
+
var snapshotPermissions = combinePermissions().withFs({ mode: "read", allow: ["."] }).withFs({ mode: "readWrite", allow: [".kb/api-snapshots/**"] }).withQuotas({ timeoutMs: 3e4, memoryMb: 128 }).build();
|
|
562
|
+
var manifest = {
|
|
563
|
+
schema: "kb.plugin/3",
|
|
564
|
+
id: "@kb-labs/policy",
|
|
565
|
+
version: "0.1.0",
|
|
566
|
+
configSection: "policy",
|
|
567
|
+
display: {
|
|
568
|
+
name: "Policy Enforcer",
|
|
569
|
+
description: "Enforces workspace-level development policies per category. Detects repo categories, resolves applicable rules, and validates compliance.",
|
|
570
|
+
tags: ["policy", "governance", "boundaries", "sdk-only", "api-compat"]
|
|
571
|
+
},
|
|
572
|
+
cli: {
|
|
573
|
+
commands: [
|
|
574
|
+
{
|
|
575
|
+
id: "policy:detect",
|
|
576
|
+
group: "policy",
|
|
577
|
+
describe: "Detect policy category for changed or specified repos",
|
|
578
|
+
longDescription: "Determines the category for repos based on git changes or a specified path, then resolves applicable policy rules.",
|
|
579
|
+
handler: "./cli/commands/detect.js#default",
|
|
580
|
+
handlerPath: "./cli/commands/detect.js",
|
|
581
|
+
flags: defineCommandFlags({
|
|
582
|
+
path: {
|
|
583
|
+
type: "string",
|
|
584
|
+
description: "Repo path to check (relative to workspace root). Defaults to git diff."
|
|
585
|
+
},
|
|
586
|
+
json: {
|
|
587
|
+
type: "boolean",
|
|
588
|
+
description: "Output as JSON",
|
|
589
|
+
default: false
|
|
590
|
+
}
|
|
591
|
+
}),
|
|
592
|
+
permissions: detectPermissions
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
id: "policy:check",
|
|
596
|
+
group: "policy",
|
|
597
|
+
describe: "Run policy checks for changed repos or a specific path",
|
|
598
|
+
longDescription: "Runs all applicable policy rules for detected repos. Exits with code 1 on violations. Use in CI or pnpm done pipeline.",
|
|
599
|
+
handler: "./cli/commands/check.js#default",
|
|
600
|
+
handlerPath: "./cli/commands/check.js",
|
|
601
|
+
flags: defineCommandFlags({
|
|
602
|
+
path: {
|
|
603
|
+
type: "string",
|
|
604
|
+
description: "Repo path to check (relative to workspace root). Defaults to git diff."
|
|
605
|
+
},
|
|
606
|
+
json: {
|
|
607
|
+
type: "boolean",
|
|
608
|
+
description: "Output as JSON",
|
|
609
|
+
default: false
|
|
610
|
+
}
|
|
611
|
+
}),
|
|
612
|
+
permissions: checkPermissions
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
id: "policy:rules",
|
|
616
|
+
group: "policy",
|
|
617
|
+
describe: "Show all configured policy rules and their categories",
|
|
618
|
+
handler: "./cli/commands/rules.js#default",
|
|
619
|
+
handlerPath: "./cli/commands/rules.js",
|
|
620
|
+
flags: defineCommandFlags({
|
|
621
|
+
json: {
|
|
622
|
+
type: "boolean",
|
|
623
|
+
description: "Output as JSON",
|
|
624
|
+
default: false
|
|
625
|
+
}
|
|
626
|
+
}),
|
|
627
|
+
permissions: detectPermissions
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
id: "policy:snapshot",
|
|
631
|
+
group: "policy",
|
|
632
|
+
describe: "Create or update API snapshot for a repo (run after npm publish)",
|
|
633
|
+
longDescription: "Extracts exported symbols from dist/*.d.ts files and saves them to .kb/api-snapshots/. Used by api-compat-check to detect breaking changes.",
|
|
634
|
+
handler: "./cli/commands/snapshot.js#default",
|
|
635
|
+
handlerPath: "./cli/commands/snapshot.js",
|
|
636
|
+
flags: defineCommandFlags({
|
|
637
|
+
path: {
|
|
638
|
+
type: "string",
|
|
639
|
+
description: "Repo path (relative to workspace root)",
|
|
640
|
+
required: true
|
|
641
|
+
}
|
|
642
|
+
}),
|
|
643
|
+
permissions: snapshotPermissions
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
},
|
|
647
|
+
permissions: pluginPermissions
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
export { buildPackageMap, detectCategories, detectCategory, getRepoPackageNames, manifest, runChecks };
|
|
651
|
+
//# sourceMappingURL=index.js.map
|
|
652
|
+
//# sourceMappingURL=index.js.map
|