@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.
@@ -0,0 +1,646 @@
1
+ import { defineCommand, findRepoRoot, useConfig } from '@kb-labs/sdk';
2
+ import { execSync, spawnSync } from 'child_process';
3
+ import fs from 'fs';
4
+ import path4 from 'path';
5
+ import semver from 'semver';
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/cli/commands/check.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
+
154
+ // src/checks/sdk-only-deps.ts
155
+ async function checkSdkOnlyDeps(repoPath, _config, workspaceRoot) {
156
+ const violations = [];
157
+ const absRepoPath = path4.resolve(workspaceRoot, repoPath);
158
+ const internalPackageNames = new Set(getRepoPackageNames(workspaceRoot, repoPath));
159
+ const packageJsonPaths = findPackageJsonPaths(absRepoPath);
160
+ for (const pkgJsonPath of packageJsonPaths) {
161
+ let json;
162
+ try {
163
+ json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
164
+ } catch {
165
+ continue;
166
+ }
167
+ const deps = Object.keys(json.dependencies ?? {});
168
+ const packageName = json.name ?? path4.basename(path4.dirname(pkgJsonPath));
169
+ const relPath = path4.relative(workspaceRoot, pkgJsonPath);
170
+ for (const dep of deps) {
171
+ if (!dep.startsWith("@kb-labs/")) {
172
+ continue;
173
+ }
174
+ if (dep === "@kb-labs/sdk") {
175
+ continue;
176
+ }
177
+ if (internalPackageNames.has(dep)) {
178
+ continue;
179
+ }
180
+ violations.push({
181
+ rule: "sdk-only-deps",
182
+ severity: "error",
183
+ message: `${packageName} imports ${dep} directly`,
184
+ package: packageName,
185
+ detail: `Plugins must depend only on @kb-labs/sdk. Move needed types to SDK or use SDK re-exports.`,
186
+ file: relPath
187
+ });
188
+ }
189
+ }
190
+ return violations;
191
+ }
192
+ function findPackageJsonPaths(absRepoPath) {
193
+ const results = [];
194
+ for (const subdir of ["packages", "apps"]) {
195
+ const subdirPath = path4.join(absRepoPath, subdir);
196
+ if (!fs.existsSync(subdirPath)) {
197
+ continue;
198
+ }
199
+ for (const entry of fs.readdirSync(subdirPath)) {
200
+ const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
201
+ if (fs.existsSync(pkgJsonPath)) {
202
+ results.push(pkgJsonPath);
203
+ }
204
+ }
205
+ }
206
+ return results;
207
+ }
208
+ async function checkBoundary(repoPath, config, workspaceRoot) {
209
+ const violations = [];
210
+ const { detectCategory: detectCategory2 } = await Promise.resolve().then(() => (init_category_resolver(), category_resolver_exports));
211
+ const categoryResult = detectCategory2(repoPath, config);
212
+ const category = categoryResult.category;
213
+ if (!category) {
214
+ return violations;
215
+ }
216
+ const boundaryRuleConfig = config.rules["boundary-check"]?.config;
217
+ const allowedCategories = boundaryRuleConfig?.allowed?.[category] ?? [];
218
+ if (allowedCategories.includes("sdk-only")) {
219
+ return checkSdkOnlyDeps(repoPath, config, workspaceRoot);
220
+ }
221
+ const packageMap = buildPackageMap(workspaceRoot, config);
222
+ const internalPackageNames = new Set(getRepoPackageNames(workspaceRoot, repoPath));
223
+ const absRepoPath = path4.resolve(workspaceRoot, repoPath);
224
+ for (const pkgJsonPath of findPackageJsonPaths2(absRepoPath)) {
225
+ let json;
226
+ try {
227
+ json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
228
+ } catch {
229
+ continue;
230
+ }
231
+ const deps = Object.keys(json.dependencies ?? {});
232
+ const packageName = json.name ?? path4.basename(path4.dirname(pkgJsonPath));
233
+ const relPath = path4.relative(workspaceRoot, pkgJsonPath);
234
+ for (const dep of deps) {
235
+ if (!dep.startsWith("@kb-labs/")) {
236
+ continue;
237
+ }
238
+ if (internalPackageNames.has(dep)) {
239
+ continue;
240
+ }
241
+ const depCategory = packageMap.get(dep);
242
+ if (depCategory === void 0 || depCategory === null) {
243
+ continue;
244
+ }
245
+ if (!allowedCategories.includes(depCategory)) {
246
+ violations.push({
247
+ rule: "boundary-check",
248
+ severity: "error",
249
+ message: `${packageName} depends on ${dep} (category: ${depCategory ?? "unknown"})`,
250
+ package: packageName,
251
+ detail: `Category "${category}" may only depend on: ${allowedCategories.join(", ")}`,
252
+ file: relPath
253
+ });
254
+ }
255
+ }
256
+ }
257
+ return violations;
258
+ }
259
+ function findPackageJsonPaths2(absRepoPath) {
260
+ const results = [];
261
+ for (const subdir of ["packages", "apps"]) {
262
+ const subdirPath = path4.join(absRepoPath, subdir);
263
+ if (!fs.existsSync(subdirPath)) {
264
+ continue;
265
+ }
266
+ for (const entry of fs.readdirSync(subdirPath)) {
267
+ const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
268
+ if (fs.existsSync(pkgJsonPath)) {
269
+ results.push(pkgJsonPath);
270
+ }
271
+ }
272
+ }
273
+ return results;
274
+ }
275
+
276
+ // src/checks/symbol-extractor.ts
277
+ var RegexSymbolExtractor = class {
278
+ extract(dtsContent) {
279
+ const symbols = /* @__PURE__ */ new Set();
280
+ for (const m of dtsContent.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)/gm)) {
281
+ symbols.add(m[1]);
282
+ }
283
+ for (const m of dtsContent.matchAll(/^export\s+(?:abstract\s+)?class\s+(\w+)/gm)) {
284
+ symbols.add(m[1]);
285
+ }
286
+ for (const m of dtsContent.matchAll(/^export\s+(?:type\s+|interface\s+)(\w+)/gm)) {
287
+ symbols.add(m[1]);
288
+ }
289
+ for (const m of dtsContent.matchAll(/^export\s+(?:const|let|var)\s+(\w+)/gm)) {
290
+ symbols.add(m[1]);
291
+ }
292
+ for (const m of dtsContent.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)/gm)) {
293
+ symbols.add(m[1]);
294
+ }
295
+ for (const m of dtsContent.matchAll(/^export\s+\{([^}]+)\}/gm)) {
296
+ const entries = m[1].split(",").map((s) => s.trim());
297
+ for (const entry of entries) {
298
+ const asMatch = entry.match(/\w+\s+as\s+(\w+)/);
299
+ if (asMatch) {
300
+ symbols.add(asMatch[1]);
301
+ } else {
302
+ const nameMatch = entry.match(/^(\w+)/);
303
+ if (nameMatch) {
304
+ symbols.add(nameMatch[1]);
305
+ }
306
+ }
307
+ }
308
+ }
309
+ if (/^export\s+default\s+/m.test(dtsContent)) {
310
+ symbols.add("default");
311
+ }
312
+ return symbols;
313
+ }
314
+ };
315
+ var defaultSymbolExtractor = new RegexSymbolExtractor();
316
+
317
+ // src/checks/api-compat-check.ts
318
+ var SNAPSHOTS_DIR = ".kb/api-snapshots";
319
+ async function checkApiCompat(repoPath, _config, workspaceRoot) {
320
+ const violations = [];
321
+ const absRepoPath = path4.resolve(workspaceRoot, repoPath);
322
+ const packageJsonPaths = findPackageJsonPaths3(absRepoPath);
323
+ for (const pkgJsonPath of packageJsonPaths) {
324
+ let json;
325
+ try {
326
+ json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
327
+ } catch {
328
+ continue;
329
+ }
330
+ if (!json.name || !json.version) {
331
+ continue;
332
+ }
333
+ const pkgName = json.name;
334
+ const currentVersion = json.version;
335
+ const snapshotPath = getSnapshotPath(workspaceRoot, pkgName);
336
+ const pkgDir = path4.dirname(pkgJsonPath);
337
+ const currentSymbols = extractSymbolsFromDist(pkgDir);
338
+ if (!fs.existsSync(snapshotPath)) {
339
+ console.warn(
340
+ `[policy] api-compat: No snapshot for ${pkgName} \u2014 run 'policy update-snapshots' to create one.`
341
+ );
342
+ continue;
343
+ }
344
+ let snapshot;
345
+ try {
346
+ snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf-8"));
347
+ } catch {
348
+ console.warn(`[policy] api-compat: Could not read snapshot for ${pkgName}, skipping.`);
349
+ continue;
350
+ }
351
+ const snapshotSymbols = new Set(snapshot.symbols);
352
+ const relPath = path4.relative(workspaceRoot, pkgJsonPath);
353
+ const removed = [];
354
+ for (const sym of snapshotSymbols) {
355
+ if (!currentSymbols.has(sym)) {
356
+ removed.push(sym);
357
+ }
358
+ }
359
+ if (removed.length > 0) {
360
+ const currentMajor = parseMajor(currentVersion);
361
+ const snapshotMajor = parseMajor(snapshot.version);
362
+ if (currentMajor <= snapshotMajor) {
363
+ violations.push({
364
+ rule: "api-compat-check",
365
+ severity: "error",
366
+ message: `${pkgName} removed exported symbols without major version bump`,
367
+ package: pkgName,
368
+ detail: `Removed: ${removed.join(", ")}. Bump major version (current: ${currentVersion}, snapshot: ${snapshot.version}) or restore symbols.`,
369
+ file: relPath
370
+ });
371
+ }
372
+ }
373
+ }
374
+ return violations;
375
+ }
376
+ function extractSymbolsFromDist(pkgDir) {
377
+ const distDir = path4.join(pkgDir, "dist");
378
+ const symbols = /* @__PURE__ */ new Set();
379
+ if (!fs.existsSync(distDir)) {
380
+ return symbols;
381
+ }
382
+ const dtsFiles = findDtsFiles(distDir);
383
+ for (const dtsFile of dtsFiles) {
384
+ const content = fs.readFileSync(dtsFile, "utf-8");
385
+ for (const sym of defaultSymbolExtractor.extract(content)) {
386
+ symbols.add(sym);
387
+ }
388
+ }
389
+ return symbols;
390
+ }
391
+ function findDtsFiles(dir) {
392
+ const results = [];
393
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
394
+ const fullPath = path4.join(dir, entry.name);
395
+ if (entry.isDirectory()) {
396
+ results.push(...findDtsFiles(fullPath));
397
+ } else if (entry.name.endsWith(".d.ts")) {
398
+ results.push(fullPath);
399
+ }
400
+ }
401
+ return results;
402
+ }
403
+ function getSnapshotPath(workspaceRoot, packageName) {
404
+ const safeName = packageName.replace(/\//g, "__").replace(/@/g, "");
405
+ const snapshotsDir = path4.join(workspaceRoot, SNAPSHOTS_DIR);
406
+ if (!fs.existsSync(snapshotsDir)) {
407
+ fs.mkdirSync(snapshotsDir, { recursive: true });
408
+ }
409
+ return path4.join(snapshotsDir, `${safeName}.json`);
410
+ }
411
+ function parseMajor(version) {
412
+ return parseInt(version.split(".")[0] ?? "0", 10);
413
+ }
414
+ function findPackageJsonPaths3(absRepoPath) {
415
+ const results = [];
416
+ for (const subdir of ["packages", "apps"]) {
417
+ const subdirPath = path4.join(absRepoPath, subdir);
418
+ if (!fs.existsSync(subdirPath)) {
419
+ continue;
420
+ }
421
+ for (const entry of fs.readdirSync(subdirPath)) {
422
+ const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
423
+ if (fs.existsSync(pkgJsonPath)) {
424
+ results.push(pkgJsonPath);
425
+ }
426
+ }
427
+ }
428
+ return results;
429
+ }
430
+ async function checkNoRollback(repoPath, _config, workspaceRoot) {
431
+ const violations = [];
432
+ const absRepoPath = path4.resolve(workspaceRoot, repoPath);
433
+ const packageJsonPaths = findPackageJsonPaths4(absRepoPath);
434
+ for (const pkgJsonPath of packageJsonPaths) {
435
+ let json;
436
+ try {
437
+ json = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
438
+ } catch {
439
+ continue;
440
+ }
441
+ if (!json.name || !json.version) {
442
+ continue;
443
+ }
444
+ if (json.private) {
445
+ continue;
446
+ }
447
+ const packageName = json.name;
448
+ const currentVersion = json.version;
449
+ const relPath = path4.relative(workspaceRoot, pkgJsonPath);
450
+ const publishedVersion = getPublishedVersion(packageName);
451
+ if (publishedVersion === null) {
452
+ continue;
453
+ }
454
+ if (!semver.gte(currentVersion, publishedVersion)) {
455
+ violations.push({
456
+ rule: "no-rollback",
457
+ severity: "error",
458
+ message: `${packageName} version ${currentVersion} is less than published ${publishedVersion}`,
459
+ package: packageName,
460
+ detail: `Cannot decrease version once published to npm. Restore to ${publishedVersion} or higher.`,
461
+ file: relPath
462
+ });
463
+ }
464
+ }
465
+ return violations;
466
+ }
467
+ function getPublishedVersion(packageName) {
468
+ const result = spawnSync("npm", ["show", packageName, "version", "--silent"], {
469
+ timeout: 1e4,
470
+ encoding: "utf-8",
471
+ stdio: ["ignore", "pipe", "ignore"]
472
+ });
473
+ if (result.error || result.status !== 0) {
474
+ return null;
475
+ }
476
+ return semver.valid(result.stdout.trim()) ?? null;
477
+ }
478
+ function findPackageJsonPaths4(absRepoPath) {
479
+ const results = [];
480
+ for (const subdir of ["packages", "apps"]) {
481
+ const subdirPath = path4.join(absRepoPath, subdir);
482
+ if (!fs.existsSync(subdirPath)) {
483
+ continue;
484
+ }
485
+ for (const entry of fs.readdirSync(subdirPath)) {
486
+ const pkgJsonPath = path4.join(subdirPath, entry, "package.json");
487
+ if (fs.existsSync(pkgJsonPath)) {
488
+ results.push(pkgJsonPath);
489
+ }
490
+ }
491
+ }
492
+ return results;
493
+ }
494
+
495
+ // src/core/policy-runner.ts
496
+ var RULE_CHECKS = {
497
+ "sdk-only-deps": checkSdkOnlyDeps,
498
+ "boundary-check": checkBoundary,
499
+ "no-breaking-without-major": checkApiCompat,
500
+ "no-rollback": checkNoRollback,
501
+ "api-compat-check": checkApiCompat
502
+ };
503
+ async function runChecks(repos, config, workspaceRoot) {
504
+ const repoResults = [];
505
+ for (const repo of repos) {
506
+ const violations = [];
507
+ const passed = [];
508
+ const rulesToRun = repo.rules;
509
+ for (const rule of rulesToRun) {
510
+ const checkFn = RULE_CHECKS[rule];
511
+ if (!checkFn) {
512
+ console.warn(`[policy] Unknown rule: ${rule} (skipping)`);
513
+ continue;
514
+ }
515
+ let ruleViolations;
516
+ try {
517
+ ruleViolations = await checkFn(repo.path, config, workspaceRoot);
518
+ } catch (err) {
519
+ console.warn(`[policy] Rule '${rule}' threw an unexpected error (skipping): ${err.message}`);
520
+ continue;
521
+ }
522
+ if (ruleViolations.length === 0) {
523
+ passed.push(rule);
524
+ } else {
525
+ violations.push(...ruleViolations.map((v) => ({ ...v, rule })));
526
+ }
527
+ }
528
+ repoResults.push({
529
+ path: repo.path,
530
+ category: repo.category,
531
+ violations,
532
+ passed
533
+ });
534
+ }
535
+ const totalViolations = repoResults.reduce((sum, r) => sum + r.violations.length, 0);
536
+ const failedRepos = repoResults.filter((r) => r.violations.length > 0).length;
537
+ return {
538
+ passed: totalViolations === 0,
539
+ repos: repoResults,
540
+ summary: {
541
+ total: repos.length,
542
+ passed: repos.length - failedRepos,
543
+ failed: failedRepos,
544
+ violations: totalViolations
545
+ }
546
+ };
547
+ }
548
+
549
+ // src/cli/commands/check.ts
550
+ var check_default = defineCommand({
551
+ id: "policy:check",
552
+ description: "Run policy checks for changed repos or a specific path. Exits with code 1 on violations.",
553
+ handler: {
554
+ async execute(ctx, input) {
555
+ const flags = input.flags ?? input;
556
+ const workspaceRoot = await findRepoRoot(ctx.cwd) ?? ctx.cwd;
557
+ const policyConfig = await useConfig();
558
+ if (!policyConfig?.categories) {
559
+ ctx.ui.error('No policies config found in .kb/kb.config.json (expected "policies" key)');
560
+ const emptyReport = {
561
+ passed: false,
562
+ repos: [],
563
+ summary: { total: 0, passed: 0, failed: 0, violations: 0 }
564
+ };
565
+ if (flags.json) {
566
+ ctx.ui.json?.({ passed: false, error: "No policies config" });
567
+ }
568
+ return { exitCode: 1, report: emptyReport };
569
+ }
570
+ const repoPaths = flags.path ? [flags.path] : detectChangedRepos(workspaceRoot);
571
+ const repos = repoPaths.map((p) => detectCategory(p, policyConfig));
572
+ for (const r of repos) {
573
+ if (!r.category) {
574
+ ctx.ui.warn?.(`Repo ${r.path} has no category \u2014 applying default rules: ${r.rules.join(", ")}`);
575
+ }
576
+ }
577
+ const report = await runChecks(repos, policyConfig, workspaceRoot);
578
+ if (flags.json) {
579
+ ctx.ui.json?.(report);
580
+ } else {
581
+ renderHumanReport(ctx, report);
582
+ }
583
+ return { exitCode: report.passed ? 0 : 1, report };
584
+ }
585
+ }
586
+ });
587
+ function renderHumanReport(ctx, report) {
588
+ const lines = [];
589
+ for (const repo of report.repos) {
590
+ const cat = repo.category ?? "(no category)";
591
+ lines.push(`
592
+ ${repo.path} (category: ${cat})`);
593
+ for (const violation of repo.violations) {
594
+ lines.push(` \u274C ${violation.rule}`);
595
+ lines.push(` ${violation.message}`);
596
+ if (violation.detail) {
597
+ lines.push(` \u2192 ${violation.detail}`);
598
+ }
599
+ }
600
+ for (const passed2 of repo.passed) {
601
+ lines.push(` \u2705 ${passed2}`);
602
+ }
603
+ }
604
+ const { total, passed, failed, violations } = report.summary;
605
+ const summaryLine = report.passed ? `\u2705 All ${total} repo(s) passed` : `\u274C ${violations} violation(s) found in ${failed}/${total} repo(s) \u2014 pipeline blocked`;
606
+ if (report.passed) {
607
+ ctx.ui.success?.("Policy Check", {
608
+ sections: [
609
+ { header: "Results", items: lines },
610
+ { header: "Summary", items: [`${passed}/${total} passed`] }
611
+ ]
612
+ });
613
+ } else {
614
+ ctx.ui.error(`Policy Check Failed
615
+ ${lines.join("\n")}
616
+
617
+ ${summaryLine}`);
618
+ }
619
+ }
620
+ function detectChangedRepos(workspaceRoot) {
621
+ try {
622
+ const output = execSync("git diff --name-only HEAD 2>/dev/null || git diff --name-only", {
623
+ cwd: workspaceRoot,
624
+ encoding: "utf-8",
625
+ timeout: 1e4
626
+ });
627
+ const repoSet = /* @__PURE__ */ new Set();
628
+ for (const file of output.trim().split("\n").filter(Boolean)) {
629
+ const parts = file.split("/");
630
+ if (parts.length >= 2) {
631
+ const topDir = parts[0];
632
+ const repoDir = parts[1];
633
+ if (["platform", "plugins", "infra", "templates", "installer", "sites"].includes(topDir)) {
634
+ repoSet.add(`${topDir}/${repoDir}`);
635
+ }
636
+ }
637
+ }
638
+ return Array.from(repoSet);
639
+ } catch {
640
+ return [];
641
+ }
642
+ }
643
+
644
+ export { check_default as default };
645
+ //# sourceMappingURL=check.js.map
646
+ //# sourceMappingURL=check.js.map