slopbrick 0.34.3 → 0.34.6

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.
@@ -34497,6 +34497,7 @@ var cppMagicNumbersRule = createRule({
34497
34497
  if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
34498
34498
  const lines = source.split("\n");
34499
34499
  const allowSet = /* @__PURE__ */ new Set([
34500
+ // v0.24 originals
34500
34501
  "1024",
34501
34502
  "65535",
34502
34503
  "65536",
@@ -34515,17 +34516,42 @@ var cppMagicNumbersRule = createRule({
34515
34516
  "2",
34516
34517
  "3",
34517
34518
  "4",
34518
- "5"
34519
+ "5",
34520
+ // v0.34.4 additions
34521
+ "-1",
34522
+ // sentinel value for "not found" / "all bits set"
34523
+ "100",
34524
+ // percent literal, very common
34525
+ "0.0",
34526
+ "0.5",
34527
+ "1.0",
34528
+ "2.0",
34529
+ // common probability / ratio
34530
+ "4096",
34531
+ // page size, hash bucket count
34532
+ "2048",
34533
+ "512",
34534
+ // power-of-2 sizes
34535
+ "32",
34536
+ "64",
34537
+ "128",
34538
+ // bit widths, byte sizes
34539
+ "16",
34540
+ "8",
34541
+ // common small constants
34542
+ "50"
34543
+ // percentile literal
34519
34544
  ]);
34520
34545
  for (let i = 0; i < lines.length; i++) {
34521
34546
  const line = lines[i] ?? "";
34522
34547
  if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
34548
+ const codeLine = line.replace(/\/\/.*$/, "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
34523
34549
  let m;
34524
34550
  MAGIC_NUMBER_REGEX.lastIndex = 0;
34525
- while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
34551
+ while ((m = MAGIC_NUMBER_REGEX.exec(codeLine)) !== null) {
34526
34552
  const literal = m[0] ?? "";
34527
34553
  if (allowSet.has(literal)) continue;
34528
- if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
34554
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(codeLine.slice(0, m.index).trimEnd())) continue;
34529
34555
  const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
34530
34556
  if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
34531
34557
  issues.push({
@@ -34536,7 +34562,7 @@ var cppMagicNumbersRule = createRule({
34536
34562
  message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
34537
34563
  line: i + 1,
34538
34564
  column: 1,
34539
- advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
34565
+ advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.34.4 (expanded allowSet + string/comment exclusion).'
34540
34566
  });
34541
34567
  }
34542
34568
  }
@@ -37189,7 +37215,6 @@ var javaSystemOutPrintlnRule = createRule({
37189
37215
  });
37190
37216
 
37191
37217
  // src/rules/java/thread-sleep-in-loop.ts
37192
- var THREAD_SLEEP_REGEX = /\bThread\.sleep\s*\(/g;
37193
37218
  var javaThreadSleepInLoopRule = createRule({
37194
37219
  id: "java/thread-sleep-in-loop",
37195
37220
  category: "perf",
@@ -37205,11 +37230,104 @@ var javaThreadSleepInLoopRule = createRule({
37205
37230
  if (!source) return issues;
37206
37231
  if (!/\.java$/i.test(facts.filePath)) return issues;
37207
37232
  if (!/\bThread\.sleep\s*\(/.test(source)) return issues;
37208
- if (!/\b(?:for|while|do)\b/.test(source)) return issues;
37209
- let m;
37210
- THREAD_SLEEP_REGEX.lastIndex = 0;
37211
- while ((m = THREAD_SLEEP_REGEX.exec(source)) !== null) {
37212
- const line = source.slice(0, m.index).split("\n").length;
37233
+ const braceStack = [];
37234
+ const loopSet = /* @__PURE__ */ new Set();
37235
+ let pendingLoopKeyword = null;
37236
+ let parenDepth = 0;
37237
+ const sleepEvents = [];
37238
+ let inString = false;
37239
+ let inLineComment = false;
37240
+ let inBlockComment = false;
37241
+ for (let i = 0; i < source.length; i++) {
37242
+ const c = source[i] ?? "";
37243
+ const next = source[i + 1] ?? "";
37244
+ const prev = source[i - 1] ?? "";
37245
+ if (inLineComment) {
37246
+ if (c === "\n") inLineComment = false;
37247
+ continue;
37248
+ }
37249
+ if (inBlockComment) {
37250
+ if (c === "*" && next === "/") {
37251
+ inBlockComment = false;
37252
+ i++;
37253
+ }
37254
+ continue;
37255
+ }
37256
+ if (inString) {
37257
+ if (c === "\\") {
37258
+ i++;
37259
+ continue;
37260
+ }
37261
+ if (c === inString) inString = false;
37262
+ continue;
37263
+ }
37264
+ if (c === "/" && next === "/") {
37265
+ inLineComment = true;
37266
+ i++;
37267
+ continue;
37268
+ }
37269
+ if (c === "/" && next === "*") {
37270
+ inBlockComment = true;
37271
+ i++;
37272
+ continue;
37273
+ }
37274
+ if (c === '"' || c === "'") {
37275
+ inString = c;
37276
+ continue;
37277
+ }
37278
+ if (c === "(") {
37279
+ parenDepth++;
37280
+ continue;
37281
+ }
37282
+ if (c === ")") {
37283
+ parenDepth = Math.max(0, parenDepth - 1);
37284
+ continue;
37285
+ }
37286
+ if (/[A-Za-z_]/.test(c) && !/[A-Za-z0-9_]/.test(prev)) {
37287
+ const next3 = source.slice(i, i + 3);
37288
+ const next5 = source.slice(i, i + 5);
37289
+ const next2 = source.slice(i, i + 2);
37290
+ const after3 = source[i + 3] ?? "";
37291
+ const after5 = source[i + 5] ?? "";
37292
+ const after2 = source[i + 2] ?? "";
37293
+ if (next3 === "for" && !/[A-Za-z0-9_]/.test(after3)) {
37294
+ pendingLoopKeyword = { kind: "for", idx: i };
37295
+ i += 2;
37296
+ continue;
37297
+ }
37298
+ if (next5 === "while" && !/[A-Za-z0-9_]/.test(after5)) {
37299
+ pendingLoopKeyword = { kind: "while", idx: i };
37300
+ i += 4;
37301
+ continue;
37302
+ }
37303
+ if (next2 === "do" && !/[A-Za-z0-9_]/.test(after2)) {
37304
+ pendingLoopKeyword = { kind: "do", idx: i };
37305
+ i += 1;
37306
+ continue;
37307
+ }
37308
+ }
37309
+ if (c === "{") {
37310
+ braceStack.push(i);
37311
+ if (pendingLoopKeyword && parenDepth === 0) {
37312
+ loopSet.add(i);
37313
+ pendingLoopKeyword = null;
37314
+ }
37315
+ } else if (c === "}") {
37316
+ const popped = braceStack.pop();
37317
+ if (popped !== void 0 && loopSet.has(popped)) {
37318
+ loopSet.delete(popped);
37319
+ }
37320
+ if (pendingLoopKeyword) {
37321
+ pendingLoopKeyword = null;
37322
+ }
37323
+ } else if (c === "T" && source.slice(i, i + 13) === "Thread.sleep(") {
37324
+ sleepEvents.push({ idx: i, loopDepth: loopSet.size });
37325
+ i += 12;
37326
+ }
37327
+ }
37328
+ for (const ev of sleepEvents) {
37329
+ if (ev.loopDepth === 0) continue;
37330
+ const line = source.slice(0, ev.idx).split("\n").length;
37213
37331
  issues.push({
37214
37332
  ruleId: "java/thread-sleep-in-loop",
37215
37333
  category: "perf",
@@ -37218,7 +37336,7 @@ var javaThreadSleepInLoopRule = createRule({
37218
37336
  message: `Thread.sleep() at line ${line}`,
37219
37337
  line,
37220
37338
  column: 1,
37221
- advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.30.'
37339
+ advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.34.6 (refined to require Thread.sleep inside the loop block, not just in the file).'
37222
37340
  });
37223
37341
  }
37224
37342
  return issues;
@@ -37465,6 +37583,7 @@ var kotlinObjectSingletonMisuseRule = createRule({
37465
37583
  // src/rules/kotlin/println-as-log.ts
37466
37584
  var PRINTLN_REGEX = /\bprintln\s*\(/g;
37467
37585
  var REAL_LOGGING_IMPORT_REGEX2 = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
37586
+ var TEST_FILE_REGEX = /(?:\/src\/test\/|\/test\/|\/Tests\/|\/Test\.kt|\/Tests\.kt|Tests\.kt$|Test\.kt$)/;
37468
37587
  var kotlinPrintlnAsLogRule = createRule({
37469
37588
  id: "kotlin/println-as-log",
37470
37589
  category: "logic",
@@ -37479,7 +37598,7 @@ var kotlinPrintlnAsLogRule = createRule({
37479
37598
  const source = facts.v2?._source;
37480
37599
  if (!source) return issues;
37481
37600
  if (!/\.kts?$/i.test(facts.filePath)) return issues;
37482
- if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
37601
+ if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
37483
37602
  if (REAL_LOGGING_IMPORT_REGEX2.test(source)) return issues;
37484
37603
  let m;
37485
37604
  PRINTLN_REGEX.lastIndex = 0;
@@ -37493,7 +37612,7 @@ var kotlinPrintlnAsLogRule = createRule({
37493
37612
  message: `println() as logger at line ${line}`,
37494
37613
  line,
37495
37614
  column: 1,
37496
- advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
37615
+ advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.34.5 (refined to skip test files for higher precision)."
37497
37616
  });
37498
37617
  }
37499
37618
  return issues;
@@ -40516,7 +40635,7 @@ var swiftImplicitlyUnwrappedOptionalRule = createRule({
40516
40635
  // src/rules/swift/print-debug.ts
40517
40636
  var PRINT_REGEX = /\bprint\s*\(/g;
40518
40637
  var DEFAULT_THRESHOLD2 = 1;
40519
- var TEST_FILE_REGEX = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
40638
+ var TEST_FILE_REGEX2 = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
40520
40639
  var swiftPrintDebugRule = createRule({
40521
40640
  id: "swift/print-debug",
40522
40641
  category: "typo",
@@ -40531,7 +40650,7 @@ var swiftPrintDebugRule = createRule({
40531
40650
  const source = facts.v2?._source;
40532
40651
  if (!source) return issues;
40533
40652
  if (!/\.swift$/i.test(facts.filePath)) return issues;
40534
- if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
40653
+ if (TEST_FILE_REGEX2.test(facts.filePath)) return issues;
40535
40654
  const matches = [];
40536
40655
  let m;
40537
40656
  PRINT_REGEX.lastIndex = 0;
@@ -45467,7 +45586,7 @@ var signal_strength_default = {
45467
45586
  precision: 0.1268,
45468
45587
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45469
45588
  verdict: "OK",
45470
- _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
45589
+ _calibrationNote: "v0.34.5: REFINED \u2014 rule now skips test files (JUnit4/5 + Android: *Tests.kt, *Test.kt, src/test/, test/ dirs). Per-file unique v9 Kotlin calibration (2698 neg, 213 pos): ratio=1.84 (\u22651.5, OK). The refinement mirrors v0.34.2's swift/print-debug fix: the previous exclusion only matched `\\/test\\/` and `.test.kts?$`, missing JUnit5's `FooTests.kt` naming. Expected post-refinement precision: 12.7% \u2192 25%+ by removing the JUnit assertion / debug-output fires that were ~50% of the FPs. Same direction as java/system-out-println (1.73 refined) and the v0.34.2 swift/print-debug pipeline. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
45471
45590
  aiSpecific: false,
45472
45591
  _v9Verdict: "OK",
45473
45592
  _v9Lift: 1.84,
@@ -45531,7 +45650,7 @@ var signal_strength_default = {
45531
45650
  precision: 0.1091,
45532
45651
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45533
45652
  verdict: "DORMANT",
45534
- _calibrationNote: "v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
45653
+ _calibrationNote: "v0.34.6: REFINED \u2014 rule now requires Thread.sleep to be INSIDE the loop block (verified via brace-counting), not just in a file that happens to contain a for/while/do keyword. v0.30 baseline: 2154 total fires, ratio 0.97 (DORMANT). The refinement targets: (1) Thread.sleep in `main()` no longer fires if a different method has a `for` loop (the v0.30 heuristic fired on every Thread.sleep in the file); (2) Thread.sleep before/after a loop block (not inside it) no longer fires; (3) string literals containing `Thread.sleep(...)` are skipped via the string-state-machine. Expected post-refinement ratio: 1.5+ (positive-signal OK verdict) by removing the cross-method over-fires. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
45535
45654
  aiSpecific: false,
45536
45655
  _v9Verdict: "DORMANT",
45537
45656
  _v9Lift: 0.97,
@@ -45786,7 +45905,7 @@ var signal_strength_default = {
45786
45905
  precision: 0.2187,
45787
45906
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45788
45907
  verdict: "DORMANT",
45789
- _calibrationNote: "v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
45908
+ _calibrationNote: "v0.34.4: REFINED \u2014 expanded allowSet with common constants (-1 sentinel, 100 percent literal, 0.5/1.0/2.0 ratios, 4096/2048/512/128/64/32/16/8 power-of-2 sizes, 50 percentile). Added string-literal and `//` comment stripping so substrings like `\"got 42 errors\"` and `// ticket #4242` no longer fire. v0.33 baseline: 220 TP / 786 FP per-file (ratio 0.86, DORMANT). The refinement targets: (1) sentinel `-1` literals are skipped because `1` is allowlisted and the `-` doesn't match MAGIC_NUMBER_REGEX anyway; (2) hex literals (`0xFF`) are skipped because the digit-only regex requires a word-boundary after digits, and `0x...` has none; (3) literals inside string/comment contexts no longer fire. Expected post-refinement ratio: 1.0-1.2 (still DORMANT but better-targeted). Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
45790
45909
  aiSpecific: true,
45791
45910
  _v7Verdict: "DORMANT",
45792
45911
  _v7Lift: 1,
@@ -34468,6 +34468,7 @@ var cppMagicNumbersRule = createRule({
34468
34468
  if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
34469
34469
  const lines = source.split("\n");
34470
34470
  const allowSet = /* @__PURE__ */ new Set([
34471
+ // v0.24 originals
34471
34472
  "1024",
34472
34473
  "65535",
34473
34474
  "65536",
@@ -34486,17 +34487,42 @@ var cppMagicNumbersRule = createRule({
34486
34487
  "2",
34487
34488
  "3",
34488
34489
  "4",
34489
- "5"
34490
+ "5",
34491
+ // v0.34.4 additions
34492
+ "-1",
34493
+ // sentinel value for "not found" / "all bits set"
34494
+ "100",
34495
+ // percent literal, very common
34496
+ "0.0",
34497
+ "0.5",
34498
+ "1.0",
34499
+ "2.0",
34500
+ // common probability / ratio
34501
+ "4096",
34502
+ // page size, hash bucket count
34503
+ "2048",
34504
+ "512",
34505
+ // power-of-2 sizes
34506
+ "32",
34507
+ "64",
34508
+ "128",
34509
+ // bit widths, byte sizes
34510
+ "16",
34511
+ "8",
34512
+ // common small constants
34513
+ "50"
34514
+ // percentile literal
34490
34515
  ]);
34491
34516
  for (let i = 0; i < lines.length; i++) {
34492
34517
  const line = lines[i] ?? "";
34493
34518
  if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
34519
+ const codeLine = line.replace(/\/\/.*$/, "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
34494
34520
  let m;
34495
34521
  MAGIC_NUMBER_REGEX.lastIndex = 0;
34496
- while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
34522
+ while ((m = MAGIC_NUMBER_REGEX.exec(codeLine)) !== null) {
34497
34523
  const literal = m[0] ?? "";
34498
34524
  if (allowSet.has(literal)) continue;
34499
- if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
34525
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(codeLine.slice(0, m.index).trimEnd())) continue;
34500
34526
  const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
34501
34527
  if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
34502
34528
  issues.push({
@@ -34507,7 +34533,7 @@ var cppMagicNumbersRule = createRule({
34507
34533
  message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
34508
34534
  line: i + 1,
34509
34535
  column: 1,
34510
- advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
34536
+ advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.34.4 (expanded allowSet + string/comment exclusion).'
34511
34537
  });
34512
34538
  }
34513
34539
  }
@@ -37160,7 +37186,6 @@ var javaSystemOutPrintlnRule = createRule({
37160
37186
  });
37161
37187
 
37162
37188
  // src/rules/java/thread-sleep-in-loop.ts
37163
- var THREAD_SLEEP_REGEX = /\bThread\.sleep\s*\(/g;
37164
37189
  var javaThreadSleepInLoopRule = createRule({
37165
37190
  id: "java/thread-sleep-in-loop",
37166
37191
  category: "perf",
@@ -37176,11 +37201,104 @@ var javaThreadSleepInLoopRule = createRule({
37176
37201
  if (!source) return issues;
37177
37202
  if (!/\.java$/i.test(facts.filePath)) return issues;
37178
37203
  if (!/\bThread\.sleep\s*\(/.test(source)) return issues;
37179
- if (!/\b(?:for|while|do)\b/.test(source)) return issues;
37180
- let m;
37181
- THREAD_SLEEP_REGEX.lastIndex = 0;
37182
- while ((m = THREAD_SLEEP_REGEX.exec(source)) !== null) {
37183
- const line = source.slice(0, m.index).split("\n").length;
37204
+ const braceStack = [];
37205
+ const loopSet = /* @__PURE__ */ new Set();
37206
+ let pendingLoopKeyword = null;
37207
+ let parenDepth = 0;
37208
+ const sleepEvents = [];
37209
+ let inString = false;
37210
+ let inLineComment = false;
37211
+ let inBlockComment = false;
37212
+ for (let i = 0; i < source.length; i++) {
37213
+ const c = source[i] ?? "";
37214
+ const next = source[i + 1] ?? "";
37215
+ const prev = source[i - 1] ?? "";
37216
+ if (inLineComment) {
37217
+ if (c === "\n") inLineComment = false;
37218
+ continue;
37219
+ }
37220
+ if (inBlockComment) {
37221
+ if (c === "*" && next === "/") {
37222
+ inBlockComment = false;
37223
+ i++;
37224
+ }
37225
+ continue;
37226
+ }
37227
+ if (inString) {
37228
+ if (c === "\\") {
37229
+ i++;
37230
+ continue;
37231
+ }
37232
+ if (c === inString) inString = false;
37233
+ continue;
37234
+ }
37235
+ if (c === "/" && next === "/") {
37236
+ inLineComment = true;
37237
+ i++;
37238
+ continue;
37239
+ }
37240
+ if (c === "/" && next === "*") {
37241
+ inBlockComment = true;
37242
+ i++;
37243
+ continue;
37244
+ }
37245
+ if (c === '"' || c === "'") {
37246
+ inString = c;
37247
+ continue;
37248
+ }
37249
+ if (c === "(") {
37250
+ parenDepth++;
37251
+ continue;
37252
+ }
37253
+ if (c === ")") {
37254
+ parenDepth = Math.max(0, parenDepth - 1);
37255
+ continue;
37256
+ }
37257
+ if (/[A-Za-z_]/.test(c) && !/[A-Za-z0-9_]/.test(prev)) {
37258
+ const next3 = source.slice(i, i + 3);
37259
+ const next5 = source.slice(i, i + 5);
37260
+ const next2 = source.slice(i, i + 2);
37261
+ const after3 = source[i + 3] ?? "";
37262
+ const after5 = source[i + 5] ?? "";
37263
+ const after2 = source[i + 2] ?? "";
37264
+ if (next3 === "for" && !/[A-Za-z0-9_]/.test(after3)) {
37265
+ pendingLoopKeyword = { kind: "for", idx: i };
37266
+ i += 2;
37267
+ continue;
37268
+ }
37269
+ if (next5 === "while" && !/[A-Za-z0-9_]/.test(after5)) {
37270
+ pendingLoopKeyword = { kind: "while", idx: i };
37271
+ i += 4;
37272
+ continue;
37273
+ }
37274
+ if (next2 === "do" && !/[A-Za-z0-9_]/.test(after2)) {
37275
+ pendingLoopKeyword = { kind: "do", idx: i };
37276
+ i += 1;
37277
+ continue;
37278
+ }
37279
+ }
37280
+ if (c === "{") {
37281
+ braceStack.push(i);
37282
+ if (pendingLoopKeyword && parenDepth === 0) {
37283
+ loopSet.add(i);
37284
+ pendingLoopKeyword = null;
37285
+ }
37286
+ } else if (c === "}") {
37287
+ const popped = braceStack.pop();
37288
+ if (popped !== void 0 && loopSet.has(popped)) {
37289
+ loopSet.delete(popped);
37290
+ }
37291
+ if (pendingLoopKeyword) {
37292
+ pendingLoopKeyword = null;
37293
+ }
37294
+ } else if (c === "T" && source.slice(i, i + 13) === "Thread.sleep(") {
37295
+ sleepEvents.push({ idx: i, loopDepth: loopSet.size });
37296
+ i += 12;
37297
+ }
37298
+ }
37299
+ for (const ev of sleepEvents) {
37300
+ if (ev.loopDepth === 0) continue;
37301
+ const line = source.slice(0, ev.idx).split("\n").length;
37184
37302
  issues.push({
37185
37303
  ruleId: "java/thread-sleep-in-loop",
37186
37304
  category: "perf",
@@ -37189,7 +37307,7 @@ var javaThreadSleepInLoopRule = createRule({
37189
37307
  message: `Thread.sleep() at line ${line}`,
37190
37308
  line,
37191
37309
  column: 1,
37192
- advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.30.'
37310
+ advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.34.6 (refined to require Thread.sleep inside the loop block, not just in the file).'
37193
37311
  });
37194
37312
  }
37195
37313
  return issues;
@@ -37436,6 +37554,7 @@ var kotlinObjectSingletonMisuseRule = createRule({
37436
37554
  // src/rules/kotlin/println-as-log.ts
37437
37555
  var PRINTLN_REGEX = /\bprintln\s*\(/g;
37438
37556
  var REAL_LOGGING_IMPORT_REGEX2 = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
37557
+ var TEST_FILE_REGEX = /(?:\/src\/test\/|\/test\/|\/Tests\/|\/Test\.kt|\/Tests\.kt|Tests\.kt$|Test\.kt$)/;
37439
37558
  var kotlinPrintlnAsLogRule = createRule({
37440
37559
  id: "kotlin/println-as-log",
37441
37560
  category: "logic",
@@ -37450,7 +37569,7 @@ var kotlinPrintlnAsLogRule = createRule({
37450
37569
  const source = facts.v2?._source;
37451
37570
  if (!source) return issues;
37452
37571
  if (!/\.kts?$/i.test(facts.filePath)) return issues;
37453
- if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
37572
+ if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
37454
37573
  if (REAL_LOGGING_IMPORT_REGEX2.test(source)) return issues;
37455
37574
  let m;
37456
37575
  PRINTLN_REGEX.lastIndex = 0;
@@ -37464,7 +37583,7 @@ var kotlinPrintlnAsLogRule = createRule({
37464
37583
  message: `println() as logger at line ${line}`,
37465
37584
  line,
37466
37585
  column: 1,
37467
- advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
37586
+ advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.34.5 (refined to skip test files for higher precision)."
37468
37587
  });
37469
37588
  }
37470
37589
  return issues;
@@ -40487,7 +40606,7 @@ var swiftImplicitlyUnwrappedOptionalRule = createRule({
40487
40606
  // src/rules/swift/print-debug.ts
40488
40607
  var PRINT_REGEX = /\bprint\s*\(/g;
40489
40608
  var DEFAULT_THRESHOLD2 = 1;
40490
- var TEST_FILE_REGEX = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
40609
+ var TEST_FILE_REGEX2 = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
40491
40610
  var swiftPrintDebugRule = createRule({
40492
40611
  id: "swift/print-debug",
40493
40612
  category: "typo",
@@ -40502,7 +40621,7 @@ var swiftPrintDebugRule = createRule({
40502
40621
  const source = facts.v2?._source;
40503
40622
  if (!source) return issues;
40504
40623
  if (!/\.swift$/i.test(facts.filePath)) return issues;
40505
- if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
40624
+ if (TEST_FILE_REGEX2.test(facts.filePath)) return issues;
40506
40625
  const matches = [];
40507
40626
  let m;
40508
40627
  PRINT_REGEX.lastIndex = 0;
@@ -45438,7 +45557,7 @@ var signal_strength_default = {
45438
45557
  precision: 0.1268,
45439
45558
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45440
45559
  verdict: "OK",
45441
- _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
45560
+ _calibrationNote: "v0.34.5: REFINED \u2014 rule now skips test files (JUnit4/5 + Android: *Tests.kt, *Test.kt, src/test/, test/ dirs). Per-file unique v9 Kotlin calibration (2698 neg, 213 pos): ratio=1.84 (\u22651.5, OK). The refinement mirrors v0.34.2's swift/print-debug fix: the previous exclusion only matched `\\/test\\/` and `.test.kts?$`, missing JUnit5's `FooTests.kt` naming. Expected post-refinement precision: 12.7% \u2192 25%+ by removing the JUnit assertion / debug-output fires that were ~50% of the FPs. Same direction as java/system-out-println (1.73 refined) and the v0.34.2 swift/print-debug pipeline. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
45442
45561
  aiSpecific: false,
45443
45562
  _v9Verdict: "OK",
45444
45563
  _v9Lift: 1.84,
@@ -45502,7 +45621,7 @@ var signal_strength_default = {
45502
45621
  precision: 0.1091,
45503
45622
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45504
45623
  verdict: "DORMANT",
45505
- _calibrationNote: "v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
45624
+ _calibrationNote: "v0.34.6: REFINED \u2014 rule now requires Thread.sleep to be INSIDE the loop block (verified via brace-counting), not just in a file that happens to contain a for/while/do keyword. v0.30 baseline: 2154 total fires, ratio 0.97 (DORMANT). The refinement targets: (1) Thread.sleep in `main()` no longer fires if a different method has a `for` loop (the v0.30 heuristic fired on every Thread.sleep in the file); (2) Thread.sleep before/after a loop block (not inside it) no longer fires; (3) string literals containing `Thread.sleep(...)` are skipped via the string-state-machine. Expected post-refinement ratio: 1.5+ (positive-signal OK verdict) by removing the cross-method over-fires. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
45506
45625
  aiSpecific: false,
45507
45626
  _v9Verdict: "DORMANT",
45508
45627
  _v9Lift: 0.97,
@@ -45757,7 +45876,7 @@ var signal_strength_default = {
45757
45876
  precision: 0.2187,
45758
45877
  lastCalibratedAt: "2026-07-03T00:00:00Z",
45759
45878
  verdict: "DORMANT",
45760
- _calibrationNote: "v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
45879
+ _calibrationNote: "v0.34.4: REFINED \u2014 expanded allowSet with common constants (-1 sentinel, 100 percent literal, 0.5/1.0/2.0 ratios, 4096/2048/512/128/64/32/16/8 power-of-2 sizes, 50 percentile). Added string-literal and `//` comment stripping so substrings like `\"got 42 errors\"` and `// ticket #4242` no longer fire. v0.33 baseline: 220 TP / 786 FP per-file (ratio 0.86, DORMANT). The refinement targets: (1) sentinel `-1` literals are skipped because `1` is allowlisted and the `-` doesn't match MAGIC_NUMBER_REGEX anyway; (2) hex literals (`0xFF`) are skipped because the digit-only regex requires a word-boundary after digits, and `0x...` has none; (3) literals inside string/comment contexts no longer fire. Expected post-refinement ratio: 1.0-1.2 (still DORMANT but better-targeted). Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
45761
45880
  aiSpecific: true,
45762
45881
  _v7Verdict: "DORMANT",
45763
45882
  _v7Lift: 1,
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.34.3";
39
+ VERSION = "0.34.6";
40
40
  }
41
41
  });
42
42
 
@@ -27734,6 +27734,7 @@ var init_magic_numbers = __esm({
27734
27734
  if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27735
27735
  const lines = source.split("\n");
27736
27736
  const allowSet = /* @__PURE__ */ new Set([
27737
+ // v0.24 originals
27737
27738
  "1024",
27738
27739
  "65535",
27739
27740
  "65536",
@@ -27752,17 +27753,42 @@ var init_magic_numbers = __esm({
27752
27753
  "2",
27753
27754
  "3",
27754
27755
  "4",
27755
- "5"
27756
+ "5",
27757
+ // v0.34.4 additions
27758
+ "-1",
27759
+ // sentinel value for "not found" / "all bits set"
27760
+ "100",
27761
+ // percent literal, very common
27762
+ "0.0",
27763
+ "0.5",
27764
+ "1.0",
27765
+ "2.0",
27766
+ // common probability / ratio
27767
+ "4096",
27768
+ // page size, hash bucket count
27769
+ "2048",
27770
+ "512",
27771
+ // power-of-2 sizes
27772
+ "32",
27773
+ "64",
27774
+ "128",
27775
+ // bit widths, byte sizes
27776
+ "16",
27777
+ "8",
27778
+ // common small constants
27779
+ "50"
27780
+ // percentile literal
27756
27781
  ]);
27757
27782
  for (let i = 0; i < lines.length; i++) {
27758
27783
  const line = lines[i] ?? "";
27759
27784
  if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
27785
+ const codeLine = line.replace(/\/\/.*$/, "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
27760
27786
  let m;
27761
27787
  MAGIC_NUMBER_REGEX.lastIndex = 0;
27762
- while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
27788
+ while ((m = MAGIC_NUMBER_REGEX.exec(codeLine)) !== null) {
27763
27789
  const literal = m[0] ?? "";
27764
27790
  if (allowSet.has(literal)) continue;
27765
- if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
27791
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(codeLine.slice(0, m.index).trimEnd())) continue;
27766
27792
  const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
27767
27793
  if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
27768
27794
  issues.push({
@@ -27773,7 +27799,7 @@ var init_magic_numbers = __esm({
27773
27799
  message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
27774
27800
  line: i + 1,
27775
27801
  column: 1,
27776
- advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
27802
+ advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.34.4 (expanded allowSet + string/comment exclusion).'
27777
27803
  });
27778
27804
  }
27779
27805
  }
@@ -30861,12 +30887,11 @@ var init_system_out_println = __esm({
30861
30887
  });
30862
30888
 
30863
30889
  // src/rules/java/thread-sleep-in-loop.ts
30864
- var THREAD_SLEEP_REGEX, javaThreadSleepInLoopRule;
30890
+ var javaThreadSleepInLoopRule;
30865
30891
  var init_thread_sleep_in_loop = __esm({
30866
30892
  "src/rules/java/thread-sleep-in-loop.ts"() {
30867
30893
  "use strict";
30868
30894
  init_rule();
30869
- THREAD_SLEEP_REGEX = /\bThread\.sleep\s*\(/g;
30870
30895
  javaThreadSleepInLoopRule = createRule({
30871
30896
  id: "java/thread-sleep-in-loop",
30872
30897
  category: "perf",
@@ -30882,11 +30907,104 @@ var init_thread_sleep_in_loop = __esm({
30882
30907
  if (!source) return issues;
30883
30908
  if (!/\.java$/i.test(facts.filePath)) return issues;
30884
30909
  if (!/\bThread\.sleep\s*\(/.test(source)) return issues;
30885
- if (!/\b(?:for|while|do)\b/.test(source)) return issues;
30886
- let m;
30887
- THREAD_SLEEP_REGEX.lastIndex = 0;
30888
- while ((m = THREAD_SLEEP_REGEX.exec(source)) !== null) {
30889
- const line = source.slice(0, m.index).split("\n").length;
30910
+ const braceStack = [];
30911
+ const loopSet = /* @__PURE__ */ new Set();
30912
+ let pendingLoopKeyword = null;
30913
+ let parenDepth = 0;
30914
+ const sleepEvents = [];
30915
+ let inString = false;
30916
+ let inLineComment = false;
30917
+ let inBlockComment = false;
30918
+ for (let i = 0; i < source.length; i++) {
30919
+ const c = source[i] ?? "";
30920
+ const next = source[i + 1] ?? "";
30921
+ const prev = source[i - 1] ?? "";
30922
+ if (inLineComment) {
30923
+ if (c === "\n") inLineComment = false;
30924
+ continue;
30925
+ }
30926
+ if (inBlockComment) {
30927
+ if (c === "*" && next === "/") {
30928
+ inBlockComment = false;
30929
+ i++;
30930
+ }
30931
+ continue;
30932
+ }
30933
+ if (inString) {
30934
+ if (c === "\\") {
30935
+ i++;
30936
+ continue;
30937
+ }
30938
+ if (c === inString) inString = false;
30939
+ continue;
30940
+ }
30941
+ if (c === "/" && next === "/") {
30942
+ inLineComment = true;
30943
+ i++;
30944
+ continue;
30945
+ }
30946
+ if (c === "/" && next === "*") {
30947
+ inBlockComment = true;
30948
+ i++;
30949
+ continue;
30950
+ }
30951
+ if (c === '"' || c === "'") {
30952
+ inString = c;
30953
+ continue;
30954
+ }
30955
+ if (c === "(") {
30956
+ parenDepth++;
30957
+ continue;
30958
+ }
30959
+ if (c === ")") {
30960
+ parenDepth = Math.max(0, parenDepth - 1);
30961
+ continue;
30962
+ }
30963
+ if (/[A-Za-z_]/.test(c) && !/[A-Za-z0-9_]/.test(prev)) {
30964
+ const next3 = source.slice(i, i + 3);
30965
+ const next5 = source.slice(i, i + 5);
30966
+ const next2 = source.slice(i, i + 2);
30967
+ const after3 = source[i + 3] ?? "";
30968
+ const after5 = source[i + 5] ?? "";
30969
+ const after2 = source[i + 2] ?? "";
30970
+ if (next3 === "for" && !/[A-Za-z0-9_]/.test(after3)) {
30971
+ pendingLoopKeyword = { kind: "for", idx: i };
30972
+ i += 2;
30973
+ continue;
30974
+ }
30975
+ if (next5 === "while" && !/[A-Za-z0-9_]/.test(after5)) {
30976
+ pendingLoopKeyword = { kind: "while", idx: i };
30977
+ i += 4;
30978
+ continue;
30979
+ }
30980
+ if (next2 === "do" && !/[A-Za-z0-9_]/.test(after2)) {
30981
+ pendingLoopKeyword = { kind: "do", idx: i };
30982
+ i += 1;
30983
+ continue;
30984
+ }
30985
+ }
30986
+ if (c === "{") {
30987
+ braceStack.push(i);
30988
+ if (pendingLoopKeyword && parenDepth === 0) {
30989
+ loopSet.add(i);
30990
+ pendingLoopKeyword = null;
30991
+ }
30992
+ } else if (c === "}") {
30993
+ const popped = braceStack.pop();
30994
+ if (popped !== void 0 && loopSet.has(popped)) {
30995
+ loopSet.delete(popped);
30996
+ }
30997
+ if (pendingLoopKeyword) {
30998
+ pendingLoopKeyword = null;
30999
+ }
31000
+ } else if (c === "T" && source.slice(i, i + 13) === "Thread.sleep(") {
31001
+ sleepEvents.push({ idx: i, loopDepth: loopSet.size });
31002
+ i += 12;
31003
+ }
31004
+ }
31005
+ for (const ev of sleepEvents) {
31006
+ if (ev.loopDepth === 0) continue;
31007
+ const line = source.slice(0, ev.idx).split("\n").length;
30890
31008
  issues.push({
30891
31009
  ruleId: "java/thread-sleep-in-loop",
30892
31010
  category: "perf",
@@ -30895,7 +31013,7 @@ var init_thread_sleep_in_loop = __esm({
30895
31013
  message: `Thread.sleep() at line ${line}`,
30896
31014
  line,
30897
31015
  column: 1,
30898
- advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.30.'
31016
+ advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.34.6 (refined to require Thread.sleep inside the loop block, not just in the file).'
30899
31017
  });
30900
31018
  }
30901
31019
  return issues;
@@ -31177,13 +31295,14 @@ var init_object_singleton_misuse = __esm({
31177
31295
  });
31178
31296
 
31179
31297
  // src/rules/kotlin/println-as-log.ts
31180
- var PRINTLN_REGEX, REAL_LOGGING_IMPORT_REGEX2, kotlinPrintlnAsLogRule;
31298
+ var PRINTLN_REGEX, REAL_LOGGING_IMPORT_REGEX2, TEST_FILE_REGEX, kotlinPrintlnAsLogRule;
31181
31299
  var init_println_as_log = __esm({
31182
31300
  "src/rules/kotlin/println-as-log.ts"() {
31183
31301
  "use strict";
31184
31302
  init_rule();
31185
31303
  PRINTLN_REGEX = /\bprintln\s*\(/g;
31186
31304
  REAL_LOGGING_IMPORT_REGEX2 = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
31305
+ TEST_FILE_REGEX = /(?:\/src\/test\/|\/test\/|\/Tests\/|\/Test\.kt|\/Tests\.kt|Tests\.kt$|Test\.kt$)/;
31187
31306
  kotlinPrintlnAsLogRule = createRule({
31188
31307
  id: "kotlin/println-as-log",
31189
31308
  category: "logic",
@@ -31198,7 +31317,7 @@ var init_println_as_log = __esm({
31198
31317
  const source = facts.v2?._source;
31199
31318
  if (!source) return issues;
31200
31319
  if (!/\.kts?$/i.test(facts.filePath)) return issues;
31201
- if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
31320
+ if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
31202
31321
  if (REAL_LOGGING_IMPORT_REGEX2.test(source)) return issues;
31203
31322
  let m;
31204
31323
  PRINTLN_REGEX.lastIndex = 0;
@@ -31212,7 +31331,7 @@ var init_println_as_log = __esm({
31212
31331
  message: `println() as logger at line ${line}`,
31213
31332
  line,
31214
31333
  column: 1,
31215
- advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
31334
+ advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.34.5 (refined to skip test files for higher precision)."
31216
31335
  });
31217
31336
  }
31218
31337
  return issues;
@@ -41057,14 +41176,14 @@ var init_implicitly_unwrapped_optional = __esm({
41057
41176
  });
41058
41177
 
41059
41178
  // src/rules/swift/print-debug.ts
41060
- var PRINT_REGEX, DEFAULT_THRESHOLD2, TEST_FILE_REGEX, swiftPrintDebugRule;
41179
+ var PRINT_REGEX, DEFAULT_THRESHOLD2, TEST_FILE_REGEX2, swiftPrintDebugRule;
41061
41180
  var init_print_debug = __esm({
41062
41181
  "src/rules/swift/print-debug.ts"() {
41063
41182
  "use strict";
41064
41183
  init_rule();
41065
41184
  PRINT_REGEX = /\bprint\s*\(/g;
41066
41185
  DEFAULT_THRESHOLD2 = 1;
41067
- TEST_FILE_REGEX = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
41186
+ TEST_FILE_REGEX2 = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
41068
41187
  swiftPrintDebugRule = createRule({
41069
41188
  id: "swift/print-debug",
41070
41189
  category: "typo",
@@ -41079,7 +41198,7 @@ var init_print_debug = __esm({
41079
41198
  const source = facts.v2?._source;
41080
41199
  if (!source) return issues;
41081
41200
  if (!/\.swift$/i.test(facts.filePath)) return issues;
41082
- if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
41201
+ if (TEST_FILE_REGEX2.test(facts.filePath)) return issues;
41083
41202
  const matches = [];
41084
41203
  let m;
41085
41204
  PRINT_REGEX.lastIndex = 0;
@@ -51554,7 +51673,7 @@ var init_signal_strength = __esm({
51554
51673
  precision: 0.1268,
51555
51674
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51556
51675
  verdict: "OK",
51557
- _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
51676
+ _calibrationNote: "v0.34.5: REFINED \u2014 rule now skips test files (JUnit4/5 + Android: *Tests.kt, *Test.kt, src/test/, test/ dirs). Per-file unique v9 Kotlin calibration (2698 neg, 213 pos): ratio=1.84 (\u22651.5, OK). The refinement mirrors v0.34.2's swift/print-debug fix: the previous exclusion only matched `\\/test\\/` and `.test.kts?$`, missing JUnit5's `FooTests.kt` naming. Expected post-refinement precision: 12.7% \u2192 25%+ by removing the JUnit assertion / debug-output fires that were ~50% of the FPs. Same direction as java/system-out-println (1.73 refined) and the v0.34.2 swift/print-debug pipeline. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
51558
51677
  aiSpecific: false,
51559
51678
  _v9Verdict: "OK",
51560
51679
  _v9Lift: 1.84,
@@ -51618,7 +51737,7 @@ var init_signal_strength = __esm({
51618
51737
  precision: 0.1091,
51619
51738
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51620
51739
  verdict: "DORMANT",
51621
- _calibrationNote: "v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
51740
+ _calibrationNote: "v0.34.6: REFINED \u2014 rule now requires Thread.sleep to be INSIDE the loop block (verified via brace-counting), not just in a file that happens to contain a for/while/do keyword. v0.30 baseline: 2154 total fires, ratio 0.97 (DORMANT). The refinement targets: (1) Thread.sleep in `main()` no longer fires if a different method has a `for` loop (the v0.30 heuristic fired on every Thread.sleep in the file); (2) Thread.sleep before/after a loop block (not inside it) no longer fires; (3) string literals containing `Thread.sleep(...)` are skipped via the string-state-machine. Expected post-refinement ratio: 1.5+ (positive-signal OK verdict) by removing the cross-method over-fires. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
51622
51741
  aiSpecific: false,
51623
51742
  _v9Verdict: "DORMANT",
51624
51743
  _v9Lift: 0.97,
@@ -51873,7 +51992,7 @@ var init_signal_strength = __esm({
51873
51992
  precision: 0.2187,
51874
51993
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51875
51994
  verdict: "DORMANT",
51876
- _calibrationNote: "v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
51995
+ _calibrationNote: "v0.34.4: REFINED \u2014 expanded allowSet with common constants (-1 sentinel, 100 percent literal, 0.5/1.0/2.0 ratios, 4096/2048/512/128/64/32/16/8 power-of-2 sizes, 50 percentile). Added string-literal and `//` comment stripping so substrings like `\"got 42 errors\"` and `// ticket #4242` no longer fire. v0.33 baseline: 220 TP / 786 FP per-file (ratio 0.86, DORMANT). The refinement targets: (1) sentinel `-1` literals are skipped because `1` is allowlisted and the `-` doesn't match MAGIC_NUMBER_REGEX anyway; (2) hex literals (`0xFF`) are skipped because the digit-only regex requires a word-boundary after digits, and `0x...` has none; (3) literals inside string/comment contexts no longer fire. Expected post-refinement ratio: 1.0-1.2 (still DORMANT but better-targeted). Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
51877
51996
  aiSpecific: true,
51878
51997
  _v7Verdict: "DORMANT",
51879
51998
  _v7Lift: 1,
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.34.3";
22
+ VERSION = "0.34.6";
23
23
  }
24
24
  });
25
25
 
@@ -27716,6 +27716,7 @@ var init_magic_numbers = __esm({
27716
27716
  if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27717
27717
  const lines = source.split("\n");
27718
27718
  const allowSet = /* @__PURE__ */ new Set([
27719
+ // v0.24 originals
27719
27720
  "1024",
27720
27721
  "65535",
27721
27722
  "65536",
@@ -27734,17 +27735,42 @@ var init_magic_numbers = __esm({
27734
27735
  "2",
27735
27736
  "3",
27736
27737
  "4",
27737
- "5"
27738
+ "5",
27739
+ // v0.34.4 additions
27740
+ "-1",
27741
+ // sentinel value for "not found" / "all bits set"
27742
+ "100",
27743
+ // percent literal, very common
27744
+ "0.0",
27745
+ "0.5",
27746
+ "1.0",
27747
+ "2.0",
27748
+ // common probability / ratio
27749
+ "4096",
27750
+ // page size, hash bucket count
27751
+ "2048",
27752
+ "512",
27753
+ // power-of-2 sizes
27754
+ "32",
27755
+ "64",
27756
+ "128",
27757
+ // bit widths, byte sizes
27758
+ "16",
27759
+ "8",
27760
+ // common small constants
27761
+ "50"
27762
+ // percentile literal
27738
27763
  ]);
27739
27764
  for (let i = 0; i < lines.length; i++) {
27740
27765
  const line = lines[i] ?? "";
27741
27766
  if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
27767
+ const codeLine = line.replace(/\/\/.*$/, "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
27742
27768
  let m;
27743
27769
  MAGIC_NUMBER_REGEX.lastIndex = 0;
27744
- while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
27770
+ while ((m = MAGIC_NUMBER_REGEX.exec(codeLine)) !== null) {
27745
27771
  const literal = m[0] ?? "";
27746
27772
  if (allowSet.has(literal)) continue;
27747
- if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
27773
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(codeLine.slice(0, m.index).trimEnd())) continue;
27748
27774
  const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
27749
27775
  if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
27750
27776
  issues.push({
@@ -27755,7 +27781,7 @@ var init_magic_numbers = __esm({
27755
27781
  message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
27756
27782
  line: i + 1,
27757
27783
  column: 1,
27758
- advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
27784
+ advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.34.4 (expanded allowSet + string/comment exclusion).'
27759
27785
  });
27760
27786
  }
27761
27787
  }
@@ -30842,12 +30868,11 @@ var init_system_out_println = __esm({
30842
30868
  });
30843
30869
 
30844
30870
  // src/rules/java/thread-sleep-in-loop.ts
30845
- var THREAD_SLEEP_REGEX, javaThreadSleepInLoopRule;
30871
+ var javaThreadSleepInLoopRule;
30846
30872
  var init_thread_sleep_in_loop = __esm({
30847
30873
  "src/rules/java/thread-sleep-in-loop.ts"() {
30848
30874
  "use strict";
30849
30875
  init_rule();
30850
- THREAD_SLEEP_REGEX = /\bThread\.sleep\s*\(/g;
30851
30876
  javaThreadSleepInLoopRule = createRule({
30852
30877
  id: "java/thread-sleep-in-loop",
30853
30878
  category: "perf",
@@ -30863,11 +30888,104 @@ var init_thread_sleep_in_loop = __esm({
30863
30888
  if (!source) return issues;
30864
30889
  if (!/\.java$/i.test(facts.filePath)) return issues;
30865
30890
  if (!/\bThread\.sleep\s*\(/.test(source)) return issues;
30866
- if (!/\b(?:for|while|do)\b/.test(source)) return issues;
30867
- let m;
30868
- THREAD_SLEEP_REGEX.lastIndex = 0;
30869
- while ((m = THREAD_SLEEP_REGEX.exec(source)) !== null) {
30870
- const line = source.slice(0, m.index).split("\n").length;
30891
+ const braceStack = [];
30892
+ const loopSet = /* @__PURE__ */ new Set();
30893
+ let pendingLoopKeyword = null;
30894
+ let parenDepth = 0;
30895
+ const sleepEvents = [];
30896
+ let inString = false;
30897
+ let inLineComment = false;
30898
+ let inBlockComment = false;
30899
+ for (let i = 0; i < source.length; i++) {
30900
+ const c = source[i] ?? "";
30901
+ const next = source[i + 1] ?? "";
30902
+ const prev = source[i - 1] ?? "";
30903
+ if (inLineComment) {
30904
+ if (c === "\n") inLineComment = false;
30905
+ continue;
30906
+ }
30907
+ if (inBlockComment) {
30908
+ if (c === "*" && next === "/") {
30909
+ inBlockComment = false;
30910
+ i++;
30911
+ }
30912
+ continue;
30913
+ }
30914
+ if (inString) {
30915
+ if (c === "\\") {
30916
+ i++;
30917
+ continue;
30918
+ }
30919
+ if (c === inString) inString = false;
30920
+ continue;
30921
+ }
30922
+ if (c === "/" && next === "/") {
30923
+ inLineComment = true;
30924
+ i++;
30925
+ continue;
30926
+ }
30927
+ if (c === "/" && next === "*") {
30928
+ inBlockComment = true;
30929
+ i++;
30930
+ continue;
30931
+ }
30932
+ if (c === '"' || c === "'") {
30933
+ inString = c;
30934
+ continue;
30935
+ }
30936
+ if (c === "(") {
30937
+ parenDepth++;
30938
+ continue;
30939
+ }
30940
+ if (c === ")") {
30941
+ parenDepth = Math.max(0, parenDepth - 1);
30942
+ continue;
30943
+ }
30944
+ if (/[A-Za-z_]/.test(c) && !/[A-Za-z0-9_]/.test(prev)) {
30945
+ const next3 = source.slice(i, i + 3);
30946
+ const next5 = source.slice(i, i + 5);
30947
+ const next2 = source.slice(i, i + 2);
30948
+ const after3 = source[i + 3] ?? "";
30949
+ const after5 = source[i + 5] ?? "";
30950
+ const after2 = source[i + 2] ?? "";
30951
+ if (next3 === "for" && !/[A-Za-z0-9_]/.test(after3)) {
30952
+ pendingLoopKeyword = { kind: "for", idx: i };
30953
+ i += 2;
30954
+ continue;
30955
+ }
30956
+ if (next5 === "while" && !/[A-Za-z0-9_]/.test(after5)) {
30957
+ pendingLoopKeyword = { kind: "while", idx: i };
30958
+ i += 4;
30959
+ continue;
30960
+ }
30961
+ if (next2 === "do" && !/[A-Za-z0-9_]/.test(after2)) {
30962
+ pendingLoopKeyword = { kind: "do", idx: i };
30963
+ i += 1;
30964
+ continue;
30965
+ }
30966
+ }
30967
+ if (c === "{") {
30968
+ braceStack.push(i);
30969
+ if (pendingLoopKeyword && parenDepth === 0) {
30970
+ loopSet.add(i);
30971
+ pendingLoopKeyword = null;
30972
+ }
30973
+ } else if (c === "}") {
30974
+ const popped = braceStack.pop();
30975
+ if (popped !== void 0 && loopSet.has(popped)) {
30976
+ loopSet.delete(popped);
30977
+ }
30978
+ if (pendingLoopKeyword) {
30979
+ pendingLoopKeyword = null;
30980
+ }
30981
+ } else if (c === "T" && source.slice(i, i + 13) === "Thread.sleep(") {
30982
+ sleepEvents.push({ idx: i, loopDepth: loopSet.size });
30983
+ i += 12;
30984
+ }
30985
+ }
30986
+ for (const ev of sleepEvents) {
30987
+ if (ev.loopDepth === 0) continue;
30988
+ const line = source.slice(0, ev.idx).split("\n").length;
30871
30989
  issues.push({
30872
30990
  ruleId: "java/thread-sleep-in-loop",
30873
30991
  category: "perf",
@@ -30876,7 +30994,7 @@ var init_thread_sleep_in_loop = __esm({
30876
30994
  message: `Thread.sleep() at line ${line}`,
30877
30995
  line,
30878
30996
  column: 1,
30879
- advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.30.'
30997
+ advice: 'Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the classic "polling with sleep" anti-pattern \u2014 the thread blocks for the sleep duration each iteration. In server contexts this ties up Tomcat/Jetty/Netty threads. Reference: java/thread-sleep-in-loop v0.34.6 (refined to require Thread.sleep inside the loop block, not just in the file).'
30880
30998
  });
30881
30999
  }
30882
31000
  return issues;
@@ -31158,13 +31276,14 @@ var init_object_singleton_misuse = __esm({
31158
31276
  });
31159
31277
 
31160
31278
  // src/rules/kotlin/println-as-log.ts
31161
- var PRINTLN_REGEX, REAL_LOGGING_IMPORT_REGEX2, kotlinPrintlnAsLogRule;
31279
+ var PRINTLN_REGEX, REAL_LOGGING_IMPORT_REGEX2, TEST_FILE_REGEX, kotlinPrintlnAsLogRule;
31162
31280
  var init_println_as_log = __esm({
31163
31281
  "src/rules/kotlin/println-as-log.ts"() {
31164
31282
  "use strict";
31165
31283
  init_rule();
31166
31284
  PRINTLN_REGEX = /\bprintln\s*\(/g;
31167
31285
  REAL_LOGGING_IMPORT_REGEX2 = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
31286
+ TEST_FILE_REGEX = /(?:\/src\/test\/|\/test\/|\/Tests\/|\/Test\.kt|\/Tests\.kt|Tests\.kt$|Test\.kt$)/;
31168
31287
  kotlinPrintlnAsLogRule = createRule({
31169
31288
  id: "kotlin/println-as-log",
31170
31289
  category: "logic",
@@ -31179,7 +31298,7 @@ var init_println_as_log = __esm({
31179
31298
  const source = facts.v2?._source;
31180
31299
  if (!source) return issues;
31181
31300
  if (!/\.kts?$/i.test(facts.filePath)) return issues;
31182
- if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
31301
+ if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
31183
31302
  if (REAL_LOGGING_IMPORT_REGEX2.test(source)) return issues;
31184
31303
  let m;
31185
31304
  PRINTLN_REGEX.lastIndex = 0;
@@ -31193,7 +31312,7 @@ var init_println_as_log = __esm({
31193
31312
  message: `println() as logger at line ${line}`,
31194
31313
  line,
31195
31314
  column: 1,
31196
- advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
31315
+ advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.34.5 (refined to skip test files for higher precision)."
31197
31316
  });
31198
31317
  }
31199
31318
  return issues;
@@ -41038,14 +41157,14 @@ var init_implicitly_unwrapped_optional = __esm({
41038
41157
  });
41039
41158
 
41040
41159
  // src/rules/swift/print-debug.ts
41041
- var PRINT_REGEX, DEFAULT_THRESHOLD2, TEST_FILE_REGEX, swiftPrintDebugRule;
41160
+ var PRINT_REGEX, DEFAULT_THRESHOLD2, TEST_FILE_REGEX2, swiftPrintDebugRule;
41042
41161
  var init_print_debug = __esm({
41043
41162
  "src/rules/swift/print-debug.ts"() {
41044
41163
  "use strict";
41045
41164
  init_rule();
41046
41165
  PRINT_REGEX = /\bprint\s*\(/g;
41047
41166
  DEFAULT_THRESHOLD2 = 1;
41048
- TEST_FILE_REGEX = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
41167
+ TEST_FILE_REGEX2 = /(?:\/Tests\/|\/Tests\.swift|\/Test\.swift|Tests\.swift$|Test\.swift$)/;
41049
41168
  swiftPrintDebugRule = createRule({
41050
41169
  id: "swift/print-debug",
41051
41170
  category: "typo",
@@ -41060,7 +41179,7 @@ var init_print_debug = __esm({
41060
41179
  const source = facts.v2?._source;
41061
41180
  if (!source) return issues;
41062
41181
  if (!/\.swift$/i.test(facts.filePath)) return issues;
41063
- if (TEST_FILE_REGEX.test(facts.filePath)) return issues;
41182
+ if (TEST_FILE_REGEX2.test(facts.filePath)) return issues;
41064
41183
  const matches = [];
41065
41184
  let m;
41066
41185
  PRINT_REGEX.lastIndex = 0;
@@ -51532,7 +51651,7 @@ var init_signal_strength = __esm({
51532
51651
  precision: 0.1268,
51533
51652
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51534
51653
  verdict: "OK",
51535
- _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
51654
+ _calibrationNote: "v0.34.5: REFINED \u2014 rule now skips test files (JUnit4/5 + Android: *Tests.kt, *Test.kt, src/test/, test/ dirs). Per-file unique v9 Kotlin calibration (2698 neg, 213 pos): ratio=1.84 (\u22651.5, OK). The refinement mirrors v0.34.2's swift/print-debug fix: the previous exclusion only matched `\\/test\\/` and `.test.kts?$`, missing JUnit5's `FooTests.kt` naming. Expected post-refinement precision: 12.7% \u2192 25%+ by removing the JUnit assertion / debug-output fires that were ~50% of the FPs. Same direction as java/system-out-println (1.73 refined) and the v0.34.2 swift/print-debug pipeline. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
51536
51655
  aiSpecific: false,
51537
51656
  _v9Verdict: "OK",
51538
51657
  _v9Lift: 1.84,
@@ -51596,7 +51715,7 @@ var init_signal_strength = __esm({
51596
51715
  precision: 0.1091,
51597
51716
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51598
51717
  verdict: "DORMANT",
51599
- _calibrationNote: "v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
51718
+ _calibrationNote: "v0.34.6: REFINED \u2014 rule now requires Thread.sleep to be INSIDE the loop block (verified via brace-counting), not just in a file that happens to contain a for/while/do keyword. v0.30 baseline: 2154 total fires, ratio 0.97 (DORMANT). The refinement targets: (1) Thread.sleep in `main()` no longer fires if a different method has a `for` loop (the v0.30 heuristic fired on every Thread.sleep in the file); (2) Thread.sleep before/after a loop block (not inside it) no longer fires; (3) string literals containing `Thread.sleep(...)` are skipped via the string-state-machine. Expected post-refinement ratio: 1.5+ (positive-signal OK verdict) by removing the cross-method over-fires. Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.30: v9 Java calibration (81891 neg, 10305 pos). ratio=0.97 (just below 1.0) \u2014 fires equally on both arms. Borderline era-confound: pre-2022 Java had more Thread.sleep in loops (less mature concurrency APIs); modern Java uses ScheduledExecutorService. Same direction as kotlin/runblocking-misuse (0.50). 2154 total fires \u2014 high absolute count, meaningful measurement. defaultOff.",
51600
51719
  aiSpecific: false,
51601
51720
  _v9Verdict: "DORMANT",
51602
51721
  _v9Lift: 0.97,
@@ -51851,7 +51970,7 @@ var init_signal_strength = __esm({
51851
51970
  precision: 0.2187,
51852
51971
  lastCalibratedAt: "2026-07-03T00:00:00Z",
51853
51972
  verdict: "DORMANT",
51854
- _calibrationNote: "v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
51973
+ _calibrationNote: "v0.34.4: REFINED \u2014 expanded allowSet with common constants (-1 sentinel, 100 percent literal, 0.5/1.0/2.0 ratios, 4096/2048/512/128/64/32/16/8 power-of-2 sizes, 50 percentile). Added string-literal and `//` comment stripping so substrings like `\"got 42 errors\"` and `// ticket #4242` no longer fire. v0.33 baseline: 220 TP / 786 FP per-file (ratio 0.86, DORMANT). The refinement targets: (1) sentinel `-1` literals are skipped because `1` is allowlisted and the `-` doesn't match MAGIC_NUMBER_REGEX anyway; (2) hex literals (`0xFF`) are skipped because the digit-only regex requires a word-boundary after digits, and `0x...` has none; (3) literals inside string/comment contexts no longer fire. Expected post-refinement ratio: 1.0-1.2 (still DORMANT but better-targeted). Full v9 re-calibration is part of the v0.35.0 re-measurement. v0.33: v9 C++ calibration (5107 neg, 1655 pos). Per-file unique: 220 TP, 786 FP, ratio=0.86. Both arms have similar proportions of files with magic numbers \u2014 this is a general C++ anti-pattern that hasn't been eradicated. INSUFFICIENT_DATA: pos arm 1655 files.",
51855
51974
  aiSpecific: true,
51856
51975
  _v7Verdict: "DORMANT",
51857
51976
  _v7Lift: 1,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopbrick",
3
- "version": "0.34.3",
3
+ "version": "0.34.6",
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": {