slopbrick 0.24.0 → 0.25.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.
@@ -37,6 +37,7 @@ module.exports = __toCommonJS(worker_exports);
37
37
  var import_node_worker_threads = require("worker_threads");
38
38
  var import_node_path9 = require("path");
39
39
  var import_node_path10 = require("path");
40
+ var import_minimatch = require("minimatch");
40
41
 
41
42
  // ../engine/dist/index.js
42
43
  var import_promises = require("fs/promises");
@@ -4364,6 +4365,7 @@ function parseSource(source, filePath) {
4364
4365
  case "py":
4365
4366
  case "go":
4366
4367
  case "rs":
4368
+ case "java":
4367
4369
  return parseBlankModule(source);
4368
4370
  default:
4369
4371
  return parseWithSwc(source, filePath);
@@ -37820,6 +37822,38 @@ var DEFAULT_CONFIG3 = {
37820
37822
  */
37821
37823
  testIntelligence: {
37822
37824
  missingEdgeCase: false
37825
+ },
37826
+ /**
37827
+ * v0.25.0: self-scan exclude paths. Defaults cover the three paths
37828
+ * that are always false positives when scanning the slopbrick repo
37829
+ * itself:
37830
+ *
37831
+ * - `src/rules/**` — rule definitions contain example patterns
37832
+ * the rules themselves detect (self-fire). E.g. a
37833
+ * `security/sql-construction` rule's source file has a SQL
37834
+ * concat string literal that fires its own regex.
37835
+ * - `tests/fixtures/**` — test fixtures contain intentional bad
37836
+ * code that the rules must fire on to be useful (each fixture
37837
+ * is a positive test case). Scanning them in a self-scan
37838
+ * produces ~70 false-positive "issues" that are really just
37839
+ * test data.
37840
+ * - `tests/rules/**` — rule test files contain expected-issue
37841
+ * assertions, also meta-code.
37842
+ *
37843
+ * Three patterns, ~70 issues removed per self-scan. Combined with
37844
+ * the v0.25.0 graded security cap, this restores the v9 plan's
37845
+ * "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
37846
+ * self-scan FPs collapsing the score to 0).
37847
+ *
37848
+ * Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
37849
+ * to opt out and scan every file (legacy behavior).
37850
+ */
37851
+ selfScan: {
37852
+ excludePaths: [
37853
+ "src/rules/**",
37854
+ "tests/fixtures/**",
37855
+ "tests/rules/**"
37856
+ ]
37823
37857
  }
37824
37858
  };
37825
37859
 
@@ -45471,7 +45505,23 @@ function applyRuleOverrides(issues, rules) {
45471
45505
  }
45472
45506
  return result;
45473
45507
  }
45508
+ function isExcludedBySelfScan(filePath, cwd, excludePaths) {
45509
+ if (!excludePaths || excludePaths.length === 0) return false;
45510
+ const rel = (0, import_node_path10.relative)(cwd, filePath).split(import_node_path10.sep).join("/");
45511
+ return excludePaths.some((pattern) => (0, import_minimatch.minimatch)(rel, pattern, { dot: true }));
45512
+ }
45474
45513
  async function scanFile(filePath, config, registry, cwd = process.cwd()) {
45514
+ if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
45515
+ return {
45516
+ filePath,
45517
+ componentCount: 0,
45518
+ issues: [],
45519
+ gapValues: [],
45520
+ styleSources: [],
45521
+ elementTags: [],
45522
+ unmatchedStringLiterals: []
45523
+ };
45524
+ }
45475
45525
  const cache = buildParserCacheConfig(cwd);
45476
45526
  const ext = (0, import_node_path9.extname)(filePath).toLowerCase();
45477
45527
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
@@ -45486,7 +45536,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
45486
45536
  ".h",
45487
45537
  ".hpp",
45488
45538
  ".hxx",
45489
- ".java",
45490
45539
  ".rb",
45491
45540
  ".php"
45492
45541
  ]);
@@ -7,7 +7,8 @@ var __export = (target, all) => {
7
7
  // src/engine/worker.ts
8
8
  import { isMainThread, parentPort, workerData } from "worker_threads";
9
9
  import { extname as extname4 } from "path";
10
- import { join as join11 } from "path";
10
+ import { join as join11, relative as relative2, sep } from "path";
11
+ import { minimatch } from "minimatch";
11
12
 
12
13
  // ../engine/dist/index.js
13
14
  import { readFile, writeFile, mkdir, access } from "fs/promises";
@@ -4335,6 +4336,7 @@ function parseSource(source, filePath) {
4335
4336
  case "py":
4336
4337
  case "go":
4337
4338
  case "rs":
4339
+ case "java":
4338
4340
  return parseBlankModule(source);
4339
4341
  default:
4340
4342
  return parseWithSwc(source, filePath);
@@ -37791,6 +37793,38 @@ var DEFAULT_CONFIG3 = {
37791
37793
  */
37792
37794
  testIntelligence: {
37793
37795
  missingEdgeCase: false
37796
+ },
37797
+ /**
37798
+ * v0.25.0: self-scan exclude paths. Defaults cover the three paths
37799
+ * that are always false positives when scanning the slopbrick repo
37800
+ * itself:
37801
+ *
37802
+ * - `src/rules/**` — rule definitions contain example patterns
37803
+ * the rules themselves detect (self-fire). E.g. a
37804
+ * `security/sql-construction` rule's source file has a SQL
37805
+ * concat string literal that fires its own regex.
37806
+ * - `tests/fixtures/**` — test fixtures contain intentional bad
37807
+ * code that the rules must fire on to be useful (each fixture
37808
+ * is a positive test case). Scanning them in a self-scan
37809
+ * produces ~70 false-positive "issues" that are really just
37810
+ * test data.
37811
+ * - `tests/rules/**` — rule test files contain expected-issue
37812
+ * assertions, also meta-code.
37813
+ *
37814
+ * Three patterns, ~70 issues removed per self-scan. Combined with
37815
+ * the v0.25.0 graded security cap, this restores the v9 plan's
37816
+ * "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
37817
+ * self-scan FPs collapsing the score to 0).
37818
+ *
37819
+ * Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
37820
+ * to opt out and scan every file (legacy behavior).
37821
+ */
37822
+ selfScan: {
37823
+ excludePaths: [
37824
+ "src/rules/**",
37825
+ "tests/fixtures/**",
37826
+ "tests/rules/**"
37827
+ ]
37794
37828
  }
37795
37829
  };
37796
37830
 
@@ -45442,7 +45476,23 @@ function applyRuleOverrides(issues, rules) {
45442
45476
  }
45443
45477
  return result;
45444
45478
  }
45479
+ function isExcludedBySelfScan(filePath, cwd, excludePaths) {
45480
+ if (!excludePaths || excludePaths.length === 0) return false;
45481
+ const rel = relative2(cwd, filePath).split(sep).join("/");
45482
+ return excludePaths.some((pattern) => minimatch(rel, pattern, { dot: true }));
45483
+ }
45445
45484
  async function scanFile(filePath, config, registry, cwd = process.cwd()) {
45485
+ if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
45486
+ return {
45487
+ filePath,
45488
+ componentCount: 0,
45489
+ issues: [],
45490
+ gapValues: [],
45491
+ styleSources: [],
45492
+ elementTags: [],
45493
+ unmatchedStringLiterals: []
45494
+ };
45495
+ }
45446
45496
  const cache = buildParserCacheConfig(cwd);
45447
45497
  const ext = extname4(filePath).toLowerCase();
45448
45498
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
@@ -45457,7 +45507,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
45457
45507
  ".h",
45458
45508
  ".hpp",
45459
45509
  ".hxx",
45460
- ".java",
45461
45510
  ".rb",
45462
45511
  ".php"
45463
45512
  ]);
package/dist/index.cjs CHANGED
@@ -36,7 +36,7 @@ var VERSION;
36
36
  var init_header = __esm({
37
37
  "src/types/_header.ts"() {
38
38
  "use strict";
39
- VERSION = "0.24.0";
39
+ VERSION = "0.25.0";
40
40
  }
41
41
  });
42
42
 
@@ -326,6 +326,38 @@ var init_defaults = __esm({
326
326
  */
327
327
  testIntelligence: {
328
328
  missingEdgeCase: false
329
+ },
330
+ /**
331
+ * v0.25.0: self-scan exclude paths. Defaults cover the three paths
332
+ * that are always false positives when scanning the slopbrick repo
333
+ * itself:
334
+ *
335
+ * - `src/rules/**` — rule definitions contain example patterns
336
+ * the rules themselves detect (self-fire). E.g. a
337
+ * `security/sql-construction` rule's source file has a SQL
338
+ * concat string literal that fires its own regex.
339
+ * - `tests/fixtures/**` — test fixtures contain intentional bad
340
+ * code that the rules must fire on to be useful (each fixture
341
+ * is a positive test case). Scanning them in a self-scan
342
+ * produces ~70 false-positive "issues" that are really just
343
+ * test data.
344
+ * - `tests/rules/**` — rule test files contain expected-issue
345
+ * assertions, also meta-code.
346
+ *
347
+ * Three patterns, ~70 issues removed per self-scan. Combined with
348
+ * the v0.25.0 graded security cap, this restores the v9 plan's
349
+ * "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
350
+ * self-scan FPs collapsing the score to 0).
351
+ *
352
+ * Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
353
+ * to opt out and scan every file (legacy behavior).
354
+ */
355
+ selfScan: {
356
+ excludePaths: [
357
+ "src/rules/**",
358
+ "tests/fixtures/**",
359
+ "tests/rules/**"
360
+ ]
329
361
  }
330
362
  };
331
363
  }
@@ -36375,6 +36407,7 @@ function parseSource(source, filePath) {
36375
36407
  case "py":
36376
36408
  case "go":
36377
36409
  case "rs":
36410
+ case "java":
36378
36411
  return parseBlankModule(source);
36379
36412
  default:
36380
36413
  return parseWithSwc(source, filePath);
@@ -51555,7 +51588,23 @@ function applyRuleOverrides(issues, rules) {
51555
51588
  }
51556
51589
  return result;
51557
51590
  }
51591
+ function isExcludedBySelfScan(filePath, cwd, excludePaths) {
51592
+ if (!excludePaths || excludePaths.length === 0) return false;
51593
+ const rel = (0, import_node_path13.relative)(cwd, filePath).split(import_node_path13.sep).join("/");
51594
+ return excludePaths.some((pattern) => (0, import_minimatch2.minimatch)(rel, pattern, { dot: true }));
51595
+ }
51558
51596
  async function scanFile(filePath, config, registry, cwd = process.cwd()) {
51597
+ if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
51598
+ return {
51599
+ filePath,
51600
+ componentCount: 0,
51601
+ issues: [],
51602
+ gapValues: [],
51603
+ styleSources: [],
51604
+ elementTags: [],
51605
+ unmatchedStringLiterals: []
51606
+ };
51607
+ }
51559
51608
  const cache = buildParserCacheConfig(cwd);
51560
51609
  const ext = (0, import_node_path12.extname)(filePath).toLowerCase();
51561
51610
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
@@ -51570,7 +51619,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
51570
51619
  ".h",
51571
51620
  ".hpp",
51572
51621
  ".hxx",
51573
- ".java",
51574
51622
  ".rb",
51575
51623
  ".php"
51576
51624
  ]);
@@ -51673,13 +51721,14 @@ function collectStyleSources(facts) {
51673
51721
  }
51674
51722
  return sources;
51675
51723
  }
51676
- var import_node_worker_threads, import_node_path12, import_node_path13;
51724
+ var import_node_worker_threads, import_node_path12, import_node_path13, import_minimatch2;
51677
51725
  var init_worker = __esm({
51678
51726
  "src/engine/worker.ts"() {
51679
51727
  "use strict";
51680
51728
  import_node_worker_threads = require("worker_threads");
51681
51729
  import_node_path12 = require("path");
51682
51730
  import_node_path13 = require("path");
51731
+ import_minimatch2 = require("minimatch");
51683
51732
  init_dist2();
51684
51733
  init_visitor();
51685
51734
  init_registry();
@@ -51862,45 +51911,6 @@ var init_flywheel = __esm({
51862
51911
  }
51863
51912
  });
51864
51913
 
51865
- // src/engine/ai-security-risk.ts
51866
- function computeAiSecurityRisk(issues) {
51867
- const findings = { critical: 0, high: 0, medium: 0, low: 0 };
51868
- for (const issue of issues) {
51869
- switch (issue.severity) {
51870
- case "high":
51871
- findings.high += 1;
51872
- break;
51873
- case "medium":
51874
- findings.medium += 1;
51875
- break;
51876
- case "low":
51877
- findings.low += 1;
51878
- break;
51879
- default:
51880
- findings.low += 1;
51881
- }
51882
- }
51883
- let risk = "low";
51884
- if (findings.critical >= 1 || findings.high >= 3) {
51885
- risk = "critical";
51886
- } else if (findings.high >= 1 || findings.medium >= 3) {
51887
- risk = "high";
51888
- } else if (findings.medium >= 1) {
51889
- risk = "medium";
51890
- }
51891
- return { risk, findings };
51892
- }
51893
- function formatAiSecurityRiskLine(risk, findings) {
51894
- const total = findings.critical + findings.high + findings.medium + findings.low;
51895
- if (total === 0) return "AI Security Risk: low";
51896
- return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
51897
- }
51898
- var init_ai_security_risk = __esm({
51899
- "src/engine/ai-security-risk.ts"() {
51900
- "use strict";
51901
- }
51902
- });
51903
-
51904
51914
  // src/engine/test-quality.ts
51905
51915
  var test_quality_exports = {};
51906
51916
  __export(test_quality_exports, {
@@ -52076,14 +52086,10 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
52076
52086
  });
52077
52087
  }
52078
52088
  }
52079
- const { risk } = computeAiSecurityRisk(flatIssues);
52080
- const securityFromRisk = {
52081
- low: 100,
52082
- medium: 67,
52083
- high: 33,
52084
- critical: 0
52085
- };
52086
- const security = securityFromRisk[risk];
52089
+ const securityIssueCount = flatIssues.filter(
52090
+ (i) => i.category === "security" || (i.ruleId ?? "").startsWith("security/")
52091
+ ).length;
52092
+ const security = Math.max(0, 100 / (1 + securityIssueCount / 5));
52087
52093
  const engineeringCategoryScores = [
52088
52094
  categoryScores.arch ?? 0,
52089
52095
  categoryScores.logic ?? 0,
@@ -52142,7 +52148,6 @@ var init_metrics = __esm({
52142
52148
  "src/engine/metrics.ts"() {
52143
52149
  "use strict";
52144
52150
  import_node_path15 = require("path");
52145
- init_ai_security_risk();
52146
52151
  init_test_quality();
52147
52152
  SEVERITY_WEIGHTS = {
52148
52153
  low: 1,
@@ -54074,6 +54079,45 @@ var init_business_logic = __esm({
54074
54079
  }
54075
54080
  });
54076
54081
 
54082
+ // src/engine/ai-security-risk.ts
54083
+ function computeAiSecurityRisk(issues) {
54084
+ const findings = { critical: 0, high: 0, medium: 0, low: 0 };
54085
+ for (const issue of issues) {
54086
+ switch (issue.severity) {
54087
+ case "high":
54088
+ findings.high += 1;
54089
+ break;
54090
+ case "medium":
54091
+ findings.medium += 1;
54092
+ break;
54093
+ case "low":
54094
+ findings.low += 1;
54095
+ break;
54096
+ default:
54097
+ findings.low += 1;
54098
+ }
54099
+ }
54100
+ let risk = "low";
54101
+ if (findings.critical >= 1 || findings.high >= 3) {
54102
+ risk = "critical";
54103
+ } else if (findings.high >= 1 || findings.medium >= 3) {
54104
+ risk = "high";
54105
+ } else if (findings.medium >= 1) {
54106
+ risk = "medium";
54107
+ }
54108
+ return { risk, findings };
54109
+ }
54110
+ function formatAiSecurityRiskLine(risk, findings) {
54111
+ const total = findings.critical + findings.high + findings.medium + findings.low;
54112
+ if (total === 0) return "AI Security Risk: low";
54113
+ return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
54114
+ }
54115
+ var init_ai_security_risk = __esm({
54116
+ "src/engine/ai-security-risk.ts"() {
54117
+ "use strict";
54118
+ }
54119
+ });
54120
+
54077
54121
  // src/engine/maintenance-cost.ts
54078
54122
  var maintenance_cost_exports = {};
54079
54123
  __export(maintenance_cost_exports, {
@@ -54813,7 +54857,7 @@ function computeDomainScores(issues) {
54813
54857
  performance2.score = issueCountToScore(performance2.issueCount);
54814
54858
  const security = tally(SECURITY_CATEGORIES);
54815
54859
  security.name = "security";
54816
- security.score = security.issueCount > 0 ? 0 : 100;
54860
+ security.score = Math.max(0, 100 / (1 + security.issueCount / 5));
54817
54861
  return { codeHygiene, accessibility, performance: performance2, security };
54818
54862
  }
54819
54863
  var COHERENCE_WEIGHTS, AI_DEBT_NUMERIC, CODE_HYGIENE_CATEGORIES, ACCESSIBILITY_CATEGORIES, PERFORMANCE_CATEGORIES, SECURITY_CATEGORIES;
@@ -57847,7 +57891,7 @@ async function runScan(options, explicitPaths) {
57847
57891
  let files;
57848
57892
  if (explicitPaths && explicitPaths.length > 0) {
57849
57893
  const { globby: globby4 } = await import("globby");
57850
- const { minimatch: minimatch3 } = await import("minimatch");
57894
+ const { minimatch: minimatch4 } = await import("minimatch");
57851
57895
  const resolved = explicitPaths.map((p) => (0, import_node_path30.resolve)(cwd, p));
57852
57896
  const expanded = [];
57853
57897
  for (const p of resolved) {
@@ -57856,10 +57900,10 @@ async function runScan(options, explicitPaths) {
57856
57900
  for (const f of found) {
57857
57901
  if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path30.extname)(f).toLowerCase())) continue;
57858
57902
  const rel = (0, import_node_path30.relative)(cwd, f).split(import_node_path30.sep).join("/");
57859
- if (config.include.length > 0 && !config.include.some((pattern) => minimatch3(rel, pattern))) {
57903
+ if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
57860
57904
  continue;
57861
57905
  }
57862
- if (config.exclude.some((pattern) => minimatch3(rel, pattern, { dot: true }))) {
57906
+ if (config.exclude.some((pattern) => minimatch4(rel, pattern, { dot: true }))) {
57863
57907
  continue;
57864
57908
  }
57865
57909
  expanded.push(f);
@@ -61313,7 +61357,7 @@ var import_node_fs39 = require("fs");
61313
61357
  var import_node_path52 = require("path");
61314
61358
  var import_node_child_process3 = require("child_process");
61315
61359
  var import_node_util2 = require("util");
61316
- var import_minimatch2 = require("minimatch");
61360
+ var import_minimatch3 = require("minimatch");
61317
61361
  init_git();
61318
61362
  init_worker();
61319
61363
  init_metrics();
@@ -61360,10 +61404,10 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
61360
61404
  const ext = (0, import_node_path52.extname)(abs).toLowerCase();
61361
61405
  if (!PR_EXTENSIONS.has(ext)) continue;
61362
61406
  const rel = (0, import_node_path52.relative)(cwd, abs).split("\\").join("/");
61363
- if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch2.minimatch)(rel, pattern))) {
61407
+ if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
61364
61408
  continue;
61365
61409
  }
61366
- if (config.exclude.some((pattern) => (0, import_minimatch2.minimatch)(rel, pattern))) {
61410
+ if (config.exclude.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
61367
61411
  continue;
61368
61412
  }
61369
61413
  sourceFiles.push(abs);
package/dist/index.d.cts CHANGED
@@ -737,6 +737,27 @@ interface RuleContext {
737
737
  supportsRsc?: boolean;
738
738
  hotspotIssues?: Issue[];
739
739
  }
740
+ /**
741
+ * v0.25.0: self-scan excludes. When `selfScan.excludePaths` is set, the
742
+ * scan worker skips files whose workspace-relative path matches any of
743
+ * the glob patterns. This is for *self-scanning* the slopbrick repo
744
+ * itself, where rule definitions (`src/rules/**`) and test fixtures
745
+ * (`tests/fixtures/**`, `tests/rules/**`) are meta-code — the rules
746
+ * contain examples of the patterns they detect (self-fire), and test
747
+ * fixtures contain intentional bad code that the rules must fire on
748
+ * to be useful. Both are false positives in the self-scan context.
749
+ *
750
+ * Default excludes (in `config/defaults.ts`) cover exactly these three
751
+ * paths. Users who scan a *different* repo can leave `selfScan` unset
752
+ * (or set `excludePaths: []`) to opt out. Empty array disables; absent
753
+ * field uses defaults.
754
+ */
755
+ interface ScanSelfScanConfig {
756
+ /** Glob patterns (minimatch) to exclude from the scan. Matched against
757
+ * the workspace-relative POSIX-style path. Dot files are matched
758
+ * (`{ dot: true }` semantics). */
759
+ excludePaths: string[];
760
+ }
740
761
  interface Rule<Context = unknown> {
741
762
  id: string;
742
763
  category: Category;
@@ -832,6 +853,19 @@ interface ResolvedConfig {
832
853
  /** Walk production AST to find branches without test coverage. */
833
854
  missingEdgeCase?: boolean;
834
855
  };
856
+ /**
857
+ * v0.25.0: self-scan exclude paths. Applied at scan time (in
858
+ * `engine/worker.ts`) so files matching any glob in `excludePaths`
859
+ * short-circuit with empty issues. Defaults (in `config/defaults.ts`)
860
+ * cover the three "always false positive in self-scan" paths:
861
+ * rule definitions (`src/rules/**`), test fixtures
862
+ * (`tests/fixtures/**`), and rule test files (`tests/rules/**`).
863
+ *
864
+ * Empty array `excludePaths: []` disables exclusion entirely
865
+ * (legacy behavior — every file is scanned). Unset field uses
866
+ * defaults.
867
+ */
868
+ selfScan?: ScanSelfScanConfig;
835
869
  }
836
870
 
837
871
  /**
@@ -1578,4 +1612,4 @@ declare function formatBadge(report: ProjectReport): string;
1578
1612
  /** Render an array of values as a Unicode sparkline (▁▂▃▄▅▆▇█). */
1579
1613
  declare function formatSparkline(values: number[]): string;
1580
1614
 
1581
- export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
1615
+ export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
package/dist/index.d.ts CHANGED
@@ -737,6 +737,27 @@ interface RuleContext {
737
737
  supportsRsc?: boolean;
738
738
  hotspotIssues?: Issue[];
739
739
  }
740
+ /**
741
+ * v0.25.0: self-scan excludes. When `selfScan.excludePaths` is set, the
742
+ * scan worker skips files whose workspace-relative path matches any of
743
+ * the glob patterns. This is for *self-scanning* the slopbrick repo
744
+ * itself, where rule definitions (`src/rules/**`) and test fixtures
745
+ * (`tests/fixtures/**`, `tests/rules/**`) are meta-code — the rules
746
+ * contain examples of the patterns they detect (self-fire), and test
747
+ * fixtures contain intentional bad code that the rules must fire on
748
+ * to be useful. Both are false positives in the self-scan context.
749
+ *
750
+ * Default excludes (in `config/defaults.ts`) cover exactly these three
751
+ * paths. Users who scan a *different* repo can leave `selfScan` unset
752
+ * (or set `excludePaths: []`) to opt out. Empty array disables; absent
753
+ * field uses defaults.
754
+ */
755
+ interface ScanSelfScanConfig {
756
+ /** Glob patterns (minimatch) to exclude from the scan. Matched against
757
+ * the workspace-relative POSIX-style path. Dot files are matched
758
+ * (`{ dot: true }` semantics). */
759
+ excludePaths: string[];
760
+ }
740
761
  interface Rule<Context = unknown> {
741
762
  id: string;
742
763
  category: Category;
@@ -832,6 +853,19 @@ interface ResolvedConfig {
832
853
  /** Walk production AST to find branches without test coverage. */
833
854
  missingEdgeCase?: boolean;
834
855
  };
856
+ /**
857
+ * v0.25.0: self-scan exclude paths. Applied at scan time (in
858
+ * `engine/worker.ts`) so files matching any glob in `excludePaths`
859
+ * short-circuit with empty issues. Defaults (in `config/defaults.ts`)
860
+ * cover the three "always false positive in self-scan" paths:
861
+ * rule definitions (`src/rules/**`), test fixtures
862
+ * (`tests/fixtures/**`), and rule test files (`tests/rules/**`).
863
+ *
864
+ * Empty array `excludePaths: []` disables exclusion entirely
865
+ * (legacy behavior — every file is scanned). Unset field uses
866
+ * defaults.
867
+ */
868
+ selfScan?: ScanSelfScanConfig;
835
869
  }
836
870
 
837
871
  /**
@@ -1578,4 +1612,4 @@ declare function formatBadge(report: ProjectReport): string;
1578
1612
  /** Render an array of values as a Unicode sparkline (▁▂▃▄▅▆▇█). */
1579
1613
  declare function formatSparkline(values: number[]): string;
1580
1614
 
1581
- export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
1615
+ export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.24.0";
22
+ VERSION = "0.25.0";
23
23
  }
24
24
  });
25
25
 
@@ -309,6 +309,38 @@ var init_defaults = __esm({
309
309
  */
310
310
  testIntelligence: {
311
311
  missingEdgeCase: false
312
+ },
313
+ /**
314
+ * v0.25.0: self-scan exclude paths. Defaults cover the three paths
315
+ * that are always false positives when scanning the slopbrick repo
316
+ * itself:
317
+ *
318
+ * - `src/rules/**` — rule definitions contain example patterns
319
+ * the rules themselves detect (self-fire). E.g. a
320
+ * `security/sql-construction` rule's source file has a SQL
321
+ * concat string literal that fires its own regex.
322
+ * - `tests/fixtures/**` — test fixtures contain intentional bad
323
+ * code that the rules must fire on to be useful (each fixture
324
+ * is a positive test case). Scanning them in a self-scan
325
+ * produces ~70 false-positive "issues" that are really just
326
+ * test data.
327
+ * - `tests/rules/**` — rule test files contain expected-issue
328
+ * assertions, also meta-code.
329
+ *
330
+ * Three patterns, ~70 issues removed per self-scan. Combined with
331
+ * the v0.25.0 graded security cap, this restores the v9 plan's
332
+ * "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
333
+ * self-scan FPs collapsing the score to 0).
334
+ *
335
+ * Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
336
+ * to opt out and scan every file (legacy behavior).
337
+ */
338
+ selfScan: {
339
+ excludePaths: [
340
+ "src/rules/**",
341
+ "tests/fixtures/**",
342
+ "tests/rules/**"
343
+ ]
312
344
  }
313
345
  };
314
346
  }
@@ -36365,6 +36397,7 @@ function parseSource(source, filePath) {
36365
36397
  case "py":
36366
36398
  case "go":
36367
36399
  case "rs":
36400
+ case "java":
36368
36401
  return parseBlankModule(source);
36369
36402
  default:
36370
36403
  return parseWithSwc(source, filePath);
@@ -51516,7 +51549,8 @@ var init_signal_strength2 = __esm({
51516
51549
  // src/engine/worker.ts
51517
51550
  import { isMainThread, parentPort, workerData } from "worker_threads";
51518
51551
  import { extname as extname5 } from "path";
51519
- import { join as join13 } from "path";
51552
+ import { join as join13, relative as relative5, sep as sep2 } from "path";
51553
+ import { minimatch as minimatch2 } from "minimatch";
51520
51554
  function buildParserCacheConfig(cwd) {
51521
51555
  const envVal = process.env.SLOP_AUDIT_CACHE;
51522
51556
  const enabled = envVal === "1" || envVal === "true";
@@ -51536,7 +51570,23 @@ function applyRuleOverrides(issues, rules) {
51536
51570
  }
51537
51571
  return result;
51538
51572
  }
51573
+ function isExcludedBySelfScan(filePath, cwd, excludePaths) {
51574
+ if (!excludePaths || excludePaths.length === 0) return false;
51575
+ const rel = relative5(cwd, filePath).split(sep2).join("/");
51576
+ return excludePaths.some((pattern) => minimatch2(rel, pattern, { dot: true }));
51577
+ }
51539
51578
  async function scanFile(filePath, config, registry, cwd = process.cwd()) {
51579
+ if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
51580
+ return {
51581
+ filePath,
51582
+ componentCount: 0,
51583
+ issues: [],
51584
+ gapValues: [],
51585
+ styleSources: [],
51586
+ elementTags: [],
51587
+ unmatchedStringLiterals: []
51588
+ };
51589
+ }
51540
51590
  const cache = buildParserCacheConfig(cwd);
51541
51591
  const ext = extname5(filePath).toLowerCase();
51542
51592
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
@@ -51551,7 +51601,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
51551
51601
  ".h",
51552
51602
  ".hpp",
51553
51603
  ".hxx",
51554
- ".java",
51555
51604
  ".rb",
51556
51605
  ".php"
51557
51606
  ]);
@@ -51839,45 +51888,6 @@ var init_flywheel = __esm({
51839
51888
  }
51840
51889
  });
51841
51890
 
51842
- // src/engine/ai-security-risk.ts
51843
- function computeAiSecurityRisk(issues) {
51844
- const findings = { critical: 0, high: 0, medium: 0, low: 0 };
51845
- for (const issue of issues) {
51846
- switch (issue.severity) {
51847
- case "high":
51848
- findings.high += 1;
51849
- break;
51850
- case "medium":
51851
- findings.medium += 1;
51852
- break;
51853
- case "low":
51854
- findings.low += 1;
51855
- break;
51856
- default:
51857
- findings.low += 1;
51858
- }
51859
- }
51860
- let risk = "low";
51861
- if (findings.critical >= 1 || findings.high >= 3) {
51862
- risk = "critical";
51863
- } else if (findings.high >= 1 || findings.medium >= 3) {
51864
- risk = "high";
51865
- } else if (findings.medium >= 1) {
51866
- risk = "medium";
51867
- }
51868
- return { risk, findings };
51869
- }
51870
- function formatAiSecurityRiskLine(risk, findings) {
51871
- const total = findings.critical + findings.high + findings.medium + findings.low;
51872
- if (total === 0) return "AI Security Risk: low";
51873
- return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
51874
- }
51875
- var init_ai_security_risk = __esm({
51876
- "src/engine/ai-security-risk.ts"() {
51877
- "use strict";
51878
- }
51879
- });
51880
-
51881
51891
  // src/engine/test-quality.ts
51882
51892
  var test_quality_exports = {};
51883
51893
  __export(test_quality_exports, {
@@ -51946,7 +51956,7 @@ var init_test_quality = __esm({
51946
51956
  });
51947
51957
 
51948
51958
  // src/engine/metrics.ts
51949
- import { isAbsolute as isAbsolute2, relative as relative5 } from "path";
51959
+ import { isAbsolute as isAbsolute2, relative as relative6 } from "path";
51950
51960
  function bucketFor(ruleId) {
51951
51961
  return RULE_TO_BUCKET[ruleId] ?? "visual";
51952
51962
  }
@@ -51970,7 +51980,7 @@ function scoreFile(result, frameworkMultiplier, config, baseline, cwd) {
51970
51980
  100,
51971
51981
  rawScore * frameworkMultiplier * CONTEXT_DENSITY_MULTIPLIER
51972
51982
  );
51973
- const baselineKey = cwd ? isAbsolute2(result.filePath) ? relative5(cwd, result.filePath) : result.filePath : result.filePath;
51983
+ const baselineKey = cwd ? isAbsolute2(result.filePath) ? relative6(cwd, result.filePath) : result.filePath : result.filePath;
51974
51984
  const baselineScore = baseline?.scores[baselineKey]?.baselineScore ?? 0;
51975
51985
  const adjustedScore = baseline ? Math.max(0, componentScore - baselineScore) : componentScore;
51976
51986
  return {
@@ -52054,14 +52064,10 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
52054
52064
  });
52055
52065
  }
52056
52066
  }
52057
- const { risk } = computeAiSecurityRisk(flatIssues);
52058
- const securityFromRisk = {
52059
- low: 100,
52060
- medium: 67,
52061
- high: 33,
52062
- critical: 0
52063
- };
52064
- const security = securityFromRisk[risk];
52067
+ const securityIssueCount = flatIssues.filter(
52068
+ (i) => i.category === "security" || (i.ruleId ?? "").startsWith("security/")
52069
+ ).length;
52070
+ const security = Math.max(0, 100 / (1 + securityIssueCount / 5));
52065
52071
  const engineeringCategoryScores = [
52066
52072
  categoryScores.arch ?? 0,
52067
52073
  categoryScores.logic ?? 0,
@@ -52119,7 +52125,6 @@ var SEVERITY_WEIGHTS, CONTEXT_DENSITY_MULTIPLIER, COMPOSITE_WEIGHTS, RULE_TO_BUC
52119
52125
  var init_metrics = __esm({
52120
52126
  "src/engine/metrics.ts"() {
52121
52127
  "use strict";
52122
- init_ai_security_risk();
52123
52128
  init_test_quality();
52124
52129
  SEVERITY_WEIGHTS = {
52125
52130
  low: 1,
@@ -54051,6 +54056,45 @@ var init_business_logic = __esm({
54051
54056
  }
54052
54057
  });
54053
54058
 
54059
+ // src/engine/ai-security-risk.ts
54060
+ function computeAiSecurityRisk(issues) {
54061
+ const findings = { critical: 0, high: 0, medium: 0, low: 0 };
54062
+ for (const issue of issues) {
54063
+ switch (issue.severity) {
54064
+ case "high":
54065
+ findings.high += 1;
54066
+ break;
54067
+ case "medium":
54068
+ findings.medium += 1;
54069
+ break;
54070
+ case "low":
54071
+ findings.low += 1;
54072
+ break;
54073
+ default:
54074
+ findings.low += 1;
54075
+ }
54076
+ }
54077
+ let risk = "low";
54078
+ if (findings.critical >= 1 || findings.high >= 3) {
54079
+ risk = "critical";
54080
+ } else if (findings.high >= 1 || findings.medium >= 3) {
54081
+ risk = "high";
54082
+ } else if (findings.medium >= 1) {
54083
+ risk = "medium";
54084
+ }
54085
+ return { risk, findings };
54086
+ }
54087
+ function formatAiSecurityRiskLine(risk, findings) {
54088
+ const total = findings.critical + findings.high + findings.medium + findings.low;
54089
+ if (total === 0) return "AI Security Risk: low";
54090
+ return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
54091
+ }
54092
+ var init_ai_security_risk = __esm({
54093
+ "src/engine/ai-security-risk.ts"() {
54094
+ "use strict";
54095
+ }
54096
+ });
54097
+
54054
54098
  // src/engine/maintenance-cost.ts
54055
54099
  var maintenance_cost_exports = {};
54056
54100
  __export(maintenance_cost_exports, {
@@ -54327,7 +54371,7 @@ __export(db_health_exports, {
54327
54371
  buildDbHealth: () => buildDbHealth
54328
54372
  });
54329
54373
  import { readFileSync as readFileSync17 } from "fs";
54330
- import { relative as relative6 } from "path";
54374
+ import { relative as relative7 } from "path";
54331
54375
  import { globby as globby3 } from "globby";
54332
54376
  import { parse as parseSql6, loadModule as loadSqlModule6 } from "pgsql-parser";
54333
54377
  function ensureSqlModule() {
@@ -54378,7 +54422,7 @@ async function buildDbHealth(cwd, _config, options = {}) {
54378
54422
  for (const abs of sqlFiles.slice(0, maxFiles)) {
54379
54423
  const parsed = await parseSqlFile(abs);
54380
54424
  if (!parsed) continue;
54381
- const relPath = relative6(cwd, abs);
54425
+ const relPath = relative7(cwd, abs);
54382
54426
  parsedFiles.push({ relPath, parsed });
54383
54427
  for (const stmt of parsed.statements) {
54384
54428
  if (stmt.type !== "IndexStmt") continue;
@@ -54433,7 +54477,7 @@ async function buildDbHealth(cwd, _config, options = {}) {
54433
54477
  } catch {
54434
54478
  continue;
54435
54479
  }
54436
- const relPath = relative6(cwd, abs);
54480
+ const relPath = relative7(cwd, abs);
54437
54481
  const context = { config: _config, filePath: relPath, cwd };
54438
54482
  const facts = { filePath: relPath, v2: { _source: source } };
54439
54483
  const ruleContext = sqlConcatRule.create(context);
@@ -54790,7 +54834,7 @@ function computeDomainScores(issues) {
54790
54834
  performance2.score = issueCountToScore(performance2.issueCount);
54791
54835
  const security = tally(SECURITY_CATEGORIES);
54792
54836
  security.name = "security";
54793
- security.score = security.issueCount > 0 ? 0 : 100;
54837
+ security.score = Math.max(0, 100 / (1 + security.issueCount / 5));
54794
54838
  return { codeHygiene, accessibility, performance: performance2, security };
54795
54839
  }
54796
54840
  var COHERENCE_WEIGHTS, AI_DEBT_NUMERIC, CODE_HYGIENE_CATEGORIES, ACCESSIBILITY_CATEGORIES, PERFORMANCE_CATEGORIES, SECURITY_CATEGORIES;
@@ -54818,7 +54862,7 @@ var init_coherence = __esm({
54818
54862
 
54819
54863
  // src/cli/report/enrichReport.ts
54820
54864
  import { readFileSync as readFileSync18 } from "fs";
54821
- import { relative as relative7 } from "path";
54865
+ import { relative as relative8 } from "path";
54822
54866
  function collectBusinessLogicIssues(cwd, filePaths) {
54823
54867
  const issues = [];
54824
54868
  for (const absPath of filePaths) {
@@ -54832,7 +54876,7 @@ function collectBusinessLogicIssues(cwd, filePaths) {
54832
54876
  for (const issue of fileIssues) {
54833
54877
  issues.push({
54834
54878
  ...issue,
54835
- filePath: relative7(cwd, absPath) || absPath
54879
+ filePath: relative8(cwd, absPath) || absPath
54836
54880
  });
54837
54881
  }
54838
54882
  }
@@ -55223,7 +55267,7 @@ import {
55223
55267
  statSync as statSync6
55224
55268
  } from "fs";
55225
55269
  import { createHash as createHash10 } from "crypto";
55226
- import { dirname as dirname11, join as join17, relative as relative8 } from "path";
55270
+ import { dirname as dirname11, join as join17, relative as relative9 } from "path";
55227
55271
  function telemetryPath2(cwd) {
55228
55272
  return join17(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
55229
55273
  }
@@ -55232,7 +55276,7 @@ function hashString(input) {
55232
55276
  }
55233
55277
  function safeRelative(cwd, filePath) {
55234
55278
  try {
55235
- return relative8(cwd, filePath);
55279
+ return relative9(cwd, filePath);
55236
55280
  } catch {
55237
55281
  return filePath;
55238
55282
  }
@@ -55580,7 +55624,7 @@ var init_structure_md = __esm({
55580
55624
 
55581
55625
  // src/cli/report/persistRun.ts
55582
55626
  import { existsSync as existsSync18, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
55583
- import { join as join19, relative as relative9 } from "path";
55627
+ import { join as join19, relative as relative10 } from "path";
55584
55628
  async function persistRun(input) {
55585
55629
  const {
55586
55630
  cwd,
@@ -55642,7 +55686,7 @@ async function persistRun(input) {
55642
55686
  const recentTopHashes = telemetryPayloads.map(
55643
55687
  (payload) => [...payload.files].sort((a, b) => b.score - a.score).slice(0, 10).map((file) => file.hash)
55644
55688
  );
55645
- const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile(relative9(cwd, c.filePath)) }));
55689
+ const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile(relative10(cwd, c.filePath)) }));
55646
55690
  const unmatchedStringLiterals = results.flatMap((r) => r.unmatchedStringLiterals ?? []);
55647
55691
  const flywheelOutput = computeFlywheelOutput(
55648
55692
  runs,
@@ -55847,11 +55891,11 @@ var init_finalizeReport = __esm({
55847
55891
  });
55848
55892
 
55849
55893
  // src/cli/report/baseline-cache.ts
55850
- import { relative as relative10 } from "path";
55894
+ import { relative as relative11 } from "path";
55851
55895
  function buildBaselineCache(report, configHash, gitHead, cwd) {
55852
55896
  const scores = {};
55853
55897
  for (const component of report.components) {
55854
- scores[relative10(cwd, component.filePath)] = {
55898
+ scores[relative11(cwd, component.filePath)] = {
55855
55899
  baselineScore: component.componentScore,
55856
55900
  componentCount: component.componentCount
55857
55901
  };
@@ -55929,14 +55973,14 @@ var init_json = __esm({
55929
55973
  // src/report/sarif.ts
55930
55974
  import { createHash as createHash11 } from "crypto";
55931
55975
  import { readFileSync as readFileSync21 } from "fs";
55932
- import { basename as basename3, isAbsolute as isAbsolute3, relative as relative11, resolve as resolve9 } from "path";
55976
+ import { basename as basename3, isAbsolute as isAbsolute3, relative as relative12, resolve as resolve9 } from "path";
55933
55977
  function buildArtifactUri(filePath, cwd) {
55934
55978
  if (!filePath) {
55935
55979
  return ".";
55936
55980
  }
55937
55981
  if (cwd) {
55938
55982
  const absoluteFilePath = isAbsolute3(filePath) ? filePath : resolve9(cwd, filePath);
55939
- const rel = relative11(cwd, absoluteFilePath);
55983
+ const rel = relative12(cwd, absoluteFilePath);
55940
55984
  if (rel.startsWith("..")) {
55941
55985
  return basename3(filePath);
55942
55986
  }
@@ -57311,7 +57355,7 @@ var init_layout_token = __esm({
57311
57355
 
57312
57356
  // src/report/unified-diff.ts
57313
57357
  import { existsSync as existsSync19, readFileSync as readFileSync23 } from "fs";
57314
- import { relative as relative12 } from "path";
57358
+ import { relative as relative13 } from "path";
57315
57359
  function collectAllFixes(issue) {
57316
57360
  return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
57317
57361
  }
@@ -57397,7 +57441,7 @@ function formatUnifiedDiff(report, cwd) {
57397
57441
  parts.push("");
57398
57442
  hasHunk = true;
57399
57443
  }
57400
- const rel = relative12(cwd, filePath);
57444
+ const rel = relative13(cwd, filePath);
57401
57445
  parts.push(`--- a/${rel}`);
57402
57446
  parts.push(`+++ b/${rel}`);
57403
57447
  parts.push(...formatHunk(original, patched));
@@ -57421,7 +57465,7 @@ __export(heatmap_exports, {
57421
57465
  buildHeatmap: () => buildHeatmap,
57422
57466
  formatHeatmap: () => formatHeatmap
57423
57467
  });
57424
- import { relative as relative13 } from "path";
57468
+ import { relative as relative14 } from "path";
57425
57469
  function isWithinDays(lastModified, reference, days) {
57426
57470
  const msPerDay = 1e3 * 60 * 60 * 24;
57427
57471
  const daysAgo = (reference.getTime() - lastModified.getTime()) / msPerDay;
@@ -57437,7 +57481,7 @@ async function buildHeatmap(report, cwd, helpers = { getFileEditCount, getFileLa
57437
57481
  const reference = new Date(report.generatedAt);
57438
57482
  const entries = await Promise.all(
57439
57483
  report.components.map(async (component) => {
57440
- const relPath = relative13(cwd, component.filePath) || component.filePath;
57484
+ const relPath = relative14(cwd, component.filePath) || component.filePath;
57441
57485
  const [edits, lastModified] = await Promise.all([
57442
57486
  helpers.getFileEditCount(cwd, relPath, RECENCY_DAYS),
57443
57487
  helpers.getFileLastModifiedDate(cwd, relPath)
@@ -57782,7 +57826,7 @@ __export(scan_exports, {
57782
57826
  watchProject: () => watchProject
57783
57827
  });
57784
57828
  import { existsSync as existsSync20, statSync as statSync8 } from "fs";
57785
- import { resolve as resolve12, relative as relative14, extname as extname7, sep as sep2 } from "path";
57829
+ import { resolve as resolve12, relative as relative15, extname as extname7, sep as sep3 } from "path";
57786
57830
  import { randomUUID } from "crypto";
57787
57831
  async function runScan(options, explicitPaths) {
57788
57832
  setLoggerQuiet(!!options.quiet);
@@ -57830,7 +57874,7 @@ async function runScan(options, explicitPaths) {
57830
57874
  let files;
57831
57875
  if (explicitPaths && explicitPaths.length > 0) {
57832
57876
  const { globby: globby4 } = await import("globby");
57833
- const { minimatch: minimatch3 } = await import("minimatch");
57877
+ const { minimatch: minimatch4 } = await import("minimatch");
57834
57878
  const resolved = explicitPaths.map((p) => resolve12(cwd, p));
57835
57879
  const expanded = [];
57836
57880
  for (const p of resolved) {
@@ -57838,11 +57882,11 @@ async function runScan(options, explicitPaths) {
57838
57882
  const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
57839
57883
  for (const f of found) {
57840
57884
  if (!ALL_SOURCE_EXTENSIONS.has(extname7(f).toLowerCase())) continue;
57841
- const rel = relative14(cwd, f).split(sep2).join("/");
57842
- if (config.include.length > 0 && !config.include.some((pattern) => minimatch3(rel, pattern))) {
57885
+ const rel = relative15(cwd, f).split(sep3).join("/");
57886
+ if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
57843
57887
  continue;
57844
57888
  }
57845
- if (config.exclude.some((pattern) => minimatch3(rel, pattern, { dot: true }))) {
57889
+ if (config.exclude.some((pattern) => minimatch4(rel, pattern, { dot: true }))) {
57846
57890
  continue;
57847
57891
  }
57848
57892
  expanded.push(f);
@@ -61076,7 +61120,7 @@ import { resolve as resolve26 } from "path";
61076
61120
  init_discover();
61077
61121
  init_patterns();
61078
61122
  import { readFileSync as readFileSync30 } from "fs";
61079
- import { basename as basename4, relative as relative15 } from "path";
61123
+ import { basename as basename4, relative as relative16 } from "path";
61080
61124
  async function runDrift(cwd, config, options = {}) {
61081
61125
  const maxFiles = options.maxFiles ?? 1e3;
61082
61126
  const allFiles = await discoverFiles(cwd, config);
@@ -61098,7 +61142,7 @@ async function runDrift(cwd, config, options = {}) {
61098
61142
  byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
61099
61143
  byFile.push({
61100
61144
  file: absPath,
61101
- relPath: relative15(cwd, absPath),
61145
+ relPath: relative16(cwd, absPath),
61102
61146
  category: v.category,
61103
61147
  import: v.import,
61104
61148
  declared: v.declared,
@@ -61220,10 +61264,10 @@ init_metrics();
61220
61264
  init_patterns();
61221
61265
  init_discover();
61222
61266
  import { readFileSync as readFileSync31 } from "fs";
61223
- import { extname as extname9, relative as relative16, resolve as resolve27 } from "path";
61267
+ import { extname as extname9, relative as relative17, resolve as resolve27 } from "path";
61224
61268
  import { execFile as execFileCb } from "child_process";
61225
61269
  import { promisify as promisify2 } from "util";
61226
- import { minimatch as minimatch2 } from "minimatch";
61270
+ import { minimatch as minimatch3 } from "minimatch";
61227
61271
  var execFile2 = promisify2(execFileCb);
61228
61272
  var PR_EXTENSIONS = /* @__PURE__ */ new Set([...SOURCE_EXTENSIONS, ".mdx"]);
61229
61273
  async function refExists(cwd, ref) {
@@ -61264,11 +61308,11 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
61264
61308
  const abs = resolve27(cwd, relOrAbs);
61265
61309
  const ext = extname9(abs).toLowerCase();
61266
61310
  if (!PR_EXTENSIONS.has(ext)) continue;
61267
- const rel = relative16(cwd, abs).split("\\").join("/");
61268
- if (config.include.length > 0 && !config.include.some((pattern) => minimatch2(rel, pattern))) {
61311
+ const rel = relative17(cwd, abs).split("\\").join("/");
61312
+ if (config.include.length > 0 && !config.include.some((pattern) => minimatch3(rel, pattern))) {
61269
61313
  continue;
61270
61314
  }
61271
- if (config.exclude.some((pattern) => minimatch2(rel, pattern))) {
61315
+ if (config.exclude.some((pattern) => minimatch3(rel, pattern))) {
61272
61316
  continue;
61273
61317
  }
61274
61318
  sourceFiles.push(abs);
@@ -61335,7 +61379,7 @@ async function runPrScan(cwd, config, options = {}) {
61335
61379
  totalScore += fileScore;
61336
61380
  files.push({
61337
61381
  file: absPath,
61338
- relPath: relative16(cwd, absPath).split("\\").join("/"),
61382
+ relPath: relative17(cwd, absPath).split("\\").join("/"),
61339
61383
  score: fileScore,
61340
61384
  issueCount: issues.length,
61341
61385
  constitutionViolationCount: constitutionViolations.length,
@@ -61712,7 +61756,7 @@ import { resolve as resolve33 } from "path";
61712
61756
  init_discover();
61713
61757
  init_business_logic();
61714
61758
  import { readFileSync as readFileSync32 } from "fs";
61715
- import { relative as relative17 } from "path";
61759
+ import { relative as relative18 } from "path";
61716
61760
  async function runBusinessLogicScan(cwd, config, options = {}) {
61717
61761
  const maxFiles = options.maxFiles ?? 500;
61718
61762
  const allFiles = await discoverFiles(cwd, config);
@@ -61731,7 +61775,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
61731
61775
  for (const issue of fileIssues) {
61732
61776
  issues.push({
61733
61777
  ...issue,
61734
- filePath: relative17(cwd, absPath) || absPath
61778
+ filePath: relative18(cwd, absPath) || absPath
61735
61779
  });
61736
61780
  }
61737
61781
  }
@@ -62293,7 +62337,7 @@ import { resolve as resolve37 } from "path";
62293
62337
  init_patterns();
62294
62338
  init_discover();
62295
62339
  import { readFileSync as readFileSync33 } from "fs";
62296
- import { basename as basename5, relative as relative18 } from "path";
62340
+ import { basename as basename5, relative as relative19 } from "path";
62297
62341
  var PATTERN_CATEGORIES = [
62298
62342
  "modal",
62299
62343
  "button",
@@ -62385,7 +62429,7 @@ function detectApiFromFiles(files, cwd) {
62385
62429
  const out = [];
62386
62430
  for (const f of files) {
62387
62431
  if (API_PATH_RE2.test(f)) {
62388
- out.push(relative18(cwd, f).split("\\").join("/"));
62432
+ out.push(relative19(cwd, f).split("\\").join("/"));
62389
62433
  }
62390
62434
  }
62391
62435
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopbrick",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 95+ rules in 15 categories, computes 4 scores (aiSlopScore: lower=cleaner, engineeringHygiene, security, repositoryHealth composite), and persists the structure for AI agents and CI.",
5
5
  "type": "module",
6
6
  "bin": {