slopbrick 0.26.1 → 0.28.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.
@@ -4337,6 +4337,15 @@ function parseSource(source, filePath) {
4337
4337
  case "go":
4338
4338
  case "rs":
4339
4339
  case "java":
4340
+ // v0.28.0: Kotlin files get the same parseBlankModule path as
4341
+ // Java (v0.24.5). All 5 `kotlin/*` rules are regex-based and
4342
+ // gate themselves on `/\.kts?$/i.test(filePath)` inside their
4343
+ // `analyze()`. The tree-sitter Kotlin integration is a larger
4344
+ // lift; the v0.27.0 methodology paper confirmed era-confounding
4345
+ // is the dominant signal anyway, so a regex-only Kotlin pass is
4346
+ // sufficient for the v9 calibration goal.
4347
+ case "kt":
4348
+ case "kts":
4340
4349
  return parseBlankModule(source);
4341
4350
  default:
4342
4351
  return parseWithSwc(source, filePath);
@@ -36962,512 +36971,6 @@ var goStructTagInconsistencyRule = createRule({
36962
36971
  }
36963
36972
  });
36964
36973
 
36965
- // src/rules/java/arraylist-vs-linkedlist.ts
36966
- var NEW_LINKED_LIST_REGEX = /new\s+LinkedList\s*</g;
36967
- var javaArraylistVsLinkedlistRule = createRule({
36968
- id: "java/arraylist-vs-linkedlist",
36969
- category: "typo",
36970
- severity: "low",
36971
- aiSpecific: true,
36972
- description: "new LinkedList<>() \u2014 use ArrayList (Effective Java, Item 28)",
36973
- create(_context) {
36974
- return {};
36975
- },
36976
- analyze(_context, facts) {
36977
- const issues = [];
36978
- const source = facts.v2?._source;
36979
- if (!source) return issues;
36980
- if (!/\.java$/i.test(facts.filePath)) return issues;
36981
- let m;
36982
- NEW_LINKED_LIST_REGEX.lastIndex = 0;
36983
- while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
36984
- const line = source.slice(0, m.index).split("\n").length;
36985
- issues.push({
36986
- ruleId: "java/arraylist-vs-linkedlist",
36987
- category: "typo",
36988
- severity: "low",
36989
- aiSpecific: true,
36990
- message: `new LinkedList at line ${line} \u2014 use ArrayList instead`,
36991
- line,
36992
- column: 1,
36993
- advice: "Replace `new LinkedList<>()` with `new ArrayList<>()`. LinkedList is rarely the right choice (worse cache locality, 5x more memory per element, O(n) indexed access). Joshua Bloch (Effective Java, Item 28) recommends ArrayList unless you specifically need a Deque. AI agents default to LinkedList because of textbook examples. Reference: java/arraylist-vs-linkedlist v0.20."
36994
- });
36995
- }
36996
- return issues;
36997
- }
36998
- });
36999
-
37000
- // src/rules/java/builder-overuse.ts
37001
- var BUILDER_ANNOTATION_REGEX = /@Builder\b/;
37002
- var FIELD_DECL_REGEX = /(?:private|public|protected)\s+(?:final\s+)?[\w<>,\s]+\s+(\w+)\s*[=;]/g;
37003
- var DEFAULT_FIELD_COUNT_CAP = 3;
37004
- var javaBuilderOveruseRule = createRule({
37005
- id: "java/builder-overuse",
37006
- category: "typo",
37007
- severity: "low",
37008
- aiSpecific: true,
37009
- description: "@Builder on a class with few fields \u2014 plain constructor is simpler",
37010
- create(_context) {
37011
- return { fieldCountCap: DEFAULT_FIELD_COUNT_CAP };
37012
- },
37013
- analyze(context, facts) {
37014
- const issues = [];
37015
- const source = facts.v2?._source;
37016
- if (!source) return issues;
37017
- if (!/\.java$/i.test(facts.filePath)) return issues;
37018
- BUILDER_ANNOTATION_REGEX.lastIndex = 0;
37019
- const builderMatch = BUILDER_ANNOTATION_REGEX.exec(source);
37020
- if (!builderMatch) return issues;
37021
- const firstBuilderLine = source.slice(0, builderMatch.index).split("\n").length;
37022
- FIELD_DECL_REGEX.lastIndex = 0;
37023
- let fieldCount = 0;
37024
- while (FIELD_DECL_REGEX.exec(source) !== null) {
37025
- fieldCount++;
37026
- }
37027
- if (fieldCount > context.fieldCountCap) return issues;
37028
- issues.push({
37029
- ruleId: "java/builder-overuse",
37030
- category: "typo",
37031
- severity: "low",
37032
- aiSpecific: true,
37033
- message: `@Builder on a class with ${fieldCount} field(s) \u2014 plain constructor is simpler`,
37034
- line: firstBuilderLine,
37035
- column: 1,
37036
- advice: "Use a plain constructor for classes with \u2264 3 fields. AI agents default to @Builder because their training data emphasizes the pattern. Builder adds Lombok dependency and a Builder inner class for every annotated class. Reference: java/builder-overuse v0.26.0."
37037
- });
37038
- return issues;
37039
- }
37040
- });
37041
-
37042
- // src/rules/java/empty-catch-block.ts
37043
- var SINGLE_LINE_EMPTY_CATCH_REGEX = /catch\s*\([^)]*\)\s*\{\s*\}/g;
37044
- var javaEmptyCatchBlockRule = createRule({
37045
- id: "java/empty-catch-block",
37046
- category: "logic",
37047
- severity: "medium",
37048
- aiSpecific: true,
37049
- description: "Empty catch block \u2014 silently swallows exceptions",
37050
- create(_context) {
37051
- return {};
37052
- },
37053
- analyze(_context, facts) {
37054
- const issues = [];
37055
- const source = facts.v2?._source;
37056
- if (!source) return issues;
37057
- if (!/\.java$/i.test(facts.filePath)) return issues;
37058
- let m;
37059
- SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
37060
- while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
37061
- const line = source.slice(0, m.index).split("\n").length;
37062
- issues.push({
37063
- ruleId: "java/empty-catch-block",
37064
- category: "logic",
37065
- severity: "medium",
37066
- aiSpecific: true,
37067
- message: `Empty catch block at line ${line} \u2014 exception is silently swallowed`,
37068
- line,
37069
- column: 1,
37070
- advice: 'Log the exception (`log.error("...", e)`), re-throw it, or both. Empty catch blocks hide bugs. The pattern is common in AI-generated code that wants to look defensive. Reference: java/empty-catch-block v0.20.'
37071
- });
37072
- }
37073
- return issues;
37074
- }
37075
- });
37076
-
37077
- // src/rules/java/immutable-collection-preference.ts
37078
- var IMMUTABLE_REGEX = /\b(List|Map|Set)\.of\s*\(/g;
37079
- var MUTABLE_REGEX = /new\s+(ArrayList|HashMap|HashSet|LinkedList|TreeMap)\s*[<(]/g;
37080
- var DEFAULT_IMMUTABLE_THRESHOLD = 5;
37081
- var DEFAULT_MUTABLE_CAP = 1;
37082
- var javaImmutableCollectionPreferenceRule = createRule({
37083
- id: "java/immutable-collection-preference",
37084
- category: "typo",
37085
- severity: "low",
37086
- aiSpecific: true,
37087
- description: "Immutable factory method over-use \u2014 prefer mutable when collection will be modified",
37088
- create(_context) {
37089
- return {
37090
- immutableThreshold: DEFAULT_IMMUTABLE_THRESHOLD,
37091
- mutableCap: DEFAULT_MUTABLE_CAP
37092
- };
37093
- },
37094
- analyze(context, facts) {
37095
- const issues = [];
37096
- const source = facts.v2?._source;
37097
- if (!source) return issues;
37098
- if (!/\.java$/i.test(facts.filePath)) return issues;
37099
- IMMUTABLE_REGEX.lastIndex = 0;
37100
- let immutableCount = 0;
37101
- let firstIdx = 0;
37102
- let m;
37103
- while ((m = IMMUTABLE_REGEX.exec(source)) !== null) {
37104
- immutableCount++;
37105
- if (firstIdx === 0) firstIdx = m.index;
37106
- }
37107
- if (immutableCount < context.immutableThreshold) return issues;
37108
- MUTABLE_REGEX.lastIndex = 0;
37109
- let mutableCount = 0;
37110
- while ((m = MUTABLE_REGEX.exec(source)) !== null) {
37111
- mutableCount++;
37112
- }
37113
- if (mutableCount > context.mutableCap) return issues;
37114
- const line = source.slice(0, firstIdx).split("\n").length;
37115
- issues.push({
37116
- ruleId: "java/immutable-collection-preference",
37117
- category: "typo",
37118
- severity: "low",
37119
- aiSpecific: true,
37120
- message: `${immutableCount} immutable factory calls (List/Map/Set.of) with only ${mutableCount} mutable collection \u2014 likely over-preferring immutability`,
37121
- line,
37122
- column: 1,
37123
- advice: "Prefer mutable collections (new ArrayList<>()) when the collection will be modified later. AI agents default to List.of/Map.of/Set.of because their training data emphasizes functional-style Java. Reference: java/immutable-collection-preference v0.26.0."
37124
- });
37125
- return issues;
37126
- }
37127
- });
37128
-
37129
- // src/rules/java/legacy-date-api.ts
37130
- var LEGACY_IMPORT_REGEX = /^import\s+(?:static\s+)?java\.(?:util|sql)\.(?:Date|Calendar|GregorianCalendar)\s*;/gm;
37131
- var LEGACY_USAGE_REGEX = /\bnew\s+(?:Date|GregorianCalendar)\s*\(/g;
37132
- var CALENDAR_GET_INSTANCE_REGEX = /Calendar\.getInstance\s*\(/g;
37133
- var javaLegacyDateApiRule = createRule({
37134
- id: "java/legacy-date-api",
37135
- category: "typo",
37136
- severity: "low",
37137
- aiSpecific: true,
37138
- description: "Legacy java.util.Date / Calendar \u2014 use java.time (JSR-310) from Java 8+",
37139
- create(_context) {
37140
- return {};
37141
- },
37142
- analyze(_context, facts) {
37143
- const issues = [];
37144
- const source = facts.v2?._source;
37145
- if (!source) return issues;
37146
- if (!/\.java$/i.test(facts.filePath)) return issues;
37147
- const flagged = /* @__PURE__ */ new Set();
37148
- let m;
37149
- LEGACY_IMPORT_REGEX.lastIndex = 0;
37150
- while ((m = LEGACY_IMPORT_REGEX.exec(source)) !== null) {
37151
- const line = source.slice(0, m.index).split("\n").length;
37152
- flagged.add(line);
37153
- issues.push({
37154
- ruleId: "java/legacy-date-api",
37155
- category: "typo",
37156
- severity: "low",
37157
- aiSpecific: true,
37158
- message: `Legacy date import at line ${line} \u2014 use java.time (JSR-310) from Java 8+`,
37159
- line,
37160
- column: 1,
37161
- advice: "Replace `java.util.Date` / `java.util.Calendar` with `java.time` (`LocalDate`, `LocalDateTime`, `Instant`, `ZonedDateTime`). java.time is immutable, thread-safe, and has a much better API. AI agents default to the legacy API because their training data predates Java 8 (2014). Reference: java/legacy-date-api v0.20."
37162
- });
37163
- }
37164
- LEGACY_USAGE_REGEX.lastIndex = 0;
37165
- while ((m = LEGACY_USAGE_REGEX.exec(source)) !== null) {
37166
- const line = source.slice(0, m.index).split("\n").length;
37167
- if (flagged.has(line)) continue;
37168
- issues.push({
37169
- ruleId: "java/legacy-date-api",
37170
- category: "typo",
37171
- severity: "low",
37172
- aiSpecific: true,
37173
- message: `new Date() / GregorianCalendar at line ${line} \u2014 use java.time`,
37174
- line,
37175
- column: 1,
37176
- advice: "Replace with `LocalDate.now()`, `Instant.now()`, or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
37177
- });
37178
- }
37179
- CALENDAR_GET_INSTANCE_REGEX.lastIndex = 0;
37180
- while ((m = CALENDAR_GET_INSTANCE_REGEX.exec(source)) !== null) {
37181
- const line = source.slice(0, m.index).split("\n").length;
37182
- if (flagged.has(line)) continue;
37183
- issues.push({
37184
- ruleId: "java/legacy-date-api",
37185
- category: "typo",
37186
- severity: "low",
37187
- aiSpecific: true,
37188
- message: `Calendar.getInstance() at line ${line} \u2014 use java.time`,
37189
- line,
37190
- column: 1,
37191
- advice: "Replace with `LocalDate.now()` (date-only) or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
37192
- });
37193
- }
37194
- return issues;
37195
- }
37196
- });
37197
-
37198
- // src/rules/java/optional-overuse.ts
37199
- var OR_ELSE_THROW_REGEX = /\.orElseThrow\s*\(/g;
37200
- var NULL_CHECK_REGEX = /Objects\.requireNonNull\s*\(|if\s*\([^)]*==\s*null\)\s*(?:throw|return)/g;
37201
- var DEFAULT_THRESHOLD = 2;
37202
- var javaOptionalOveruseRule = createRule({
37203
- id: "java/optional-overuse",
37204
- category: "typo",
37205
- severity: "low",
37206
- aiSpecific: true,
37207
- description: "Optional chain over-use \u2014 null check or Objects.requireNonNull is faster",
37208
- create(_context) {
37209
- return { threshold: DEFAULT_THRESHOLD };
37210
- },
37211
- analyze(context, facts) {
37212
- const issues = [];
37213
- const source = facts.v2?._source;
37214
- if (!source) return issues;
37215
- if (!/\.java$/i.test(facts.filePath)) return issues;
37216
- OR_ELSE_THROW_REGEX.lastIndex = 0;
37217
- let orElseThrowCount = 0;
37218
- let firstIdx = 0;
37219
- let m;
37220
- while ((m = OR_ELSE_THROW_REGEX.exec(source)) !== null) {
37221
- orElseThrowCount++;
37222
- if (firstIdx === 0) firstIdx = m.index;
37223
- }
37224
- if (orElseThrowCount < context.threshold) return issues;
37225
- NULL_CHECK_REGEX.lastIndex = 0;
37226
- const nullCheckCount = (source.match(NULL_CHECK_REGEX) ?? []).length;
37227
- const optionalRatio = orElseThrowCount / Math.max(orElseThrowCount + nullCheckCount, 1);
37228
- if (optionalRatio < 0.6) return issues;
37229
- const line = source.slice(0, firstIdx).split("\n").length;
37230
- issues.push({
37231
- ruleId: "java/optional-overuse",
37232
- category: "typo",
37233
- severity: "low",
37234
- aiSpecific: true,
37235
- message: `${orElseThrowCount} .orElseThrow() calls with ${nullCheckCount} null checks \u2014 Optional chain over-use`,
37236
- line,
37237
- column: 1,
37238
- advice: 'Use Objects.requireNonNull(x, "msg") for null checks; reserve Optional for return values. AI agents default to Optional chains because their training data emphasizes null-safety. Real Java code uses null checks in hot paths. Reference: java/optional-overuse v0.26.0.'
37239
- });
37240
- return issues;
37241
- }
37242
- });
37243
-
37244
- // src/rules/java/raw-type-overuse.ts
37245
- var RAW_TYPE_REGEX = /\b(List|Map|Set|Collection|Iterable)\s+(?![<A-Z])(\w+)/g;
37246
- var javaRawTypeOveruseRule = createRule({
37247
- id: "java/raw-type-overuse",
37248
- category: "typo",
37249
- severity: "low",
37250
- aiSpecific: true,
37251
- description: "Raw type usage (List, Map, Set) \u2014 use generics (Effective Java, Item 23)",
37252
- create(_context) {
37253
- return {};
37254
- },
37255
- analyze(_context, facts) {
37256
- const issues = [];
37257
- const source = facts.v2?._source;
37258
- if (!source) return issues;
37259
- if (!/\.java$/i.test(facts.filePath)) return issues;
37260
- let m;
37261
- RAW_TYPE_REGEX.lastIndex = 0;
37262
- while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
37263
- const typeName = m[1];
37264
- const line = source.slice(0, m.index).split("\n").length;
37265
- issues.push({
37266
- ruleId: "java/raw-type-overuse",
37267
- category: "typo",
37268
- severity: "low",
37269
- aiSpecific: true,
37270
- message: `Raw type ${typeName} at line ${line} \u2014 add type parameters`,
37271
- line,
37272
- column: 1,
37273
- advice: `Replace raw \`${typeName}\` with \`${typeName}<...>\`. Raw types disable generic type checking. Effective Java, Item 23: 'Don't use raw types in new code'. AI agents default to raw types when unsure of the correct generic parameters. Reference: java/raw-type-overuse v0.20.`
37274
- });
37275
- }
37276
- return issues;
37277
- }
37278
- });
37279
-
37280
- // src/rules/java/stream-overuse.ts
37281
- var STREAM_OPS = [
37282
- ".map(",
37283
- ".filter(",
37284
- ".flatMap(",
37285
- ".collect(",
37286
- ".reduce(",
37287
- ".sorted(",
37288
- ".distinct(",
37289
- ".limit(",
37290
- ".skip(",
37291
- ".anyMatch(",
37292
- ".allMatch(",
37293
- ".noneMatch(",
37294
- ".findFirst(",
37295
- ".findAny("
37296
- ];
37297
- var DEFAULT_CHAIN_THRESHOLD = 3;
37298
- var javaStreamOveruseRule = createRule({
37299
- id: "java/stream-overuse",
37300
- category: "typo",
37301
- severity: "low",
37302
- aiSpecific: true,
37303
- description: "Stream API chain over-use \u2014 for-loop is faster for simple transformations",
37304
- create(_context) {
37305
- return { chainThreshold: DEFAULT_CHAIN_THRESHOLD };
37306
- },
37307
- analyze(context, facts) {
37308
- const issues = [];
37309
- const source = facts.v2?._source;
37310
- if (!source) return issues;
37311
- if (!/\.java$/i.test(facts.filePath)) return issues;
37312
- const lines = source.split("\n");
37313
- const streamOpPattern = new RegExp(STREAM_OPS.map((op) => op.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g");
37314
- let firstOffendingLine = 0;
37315
- let firstOffendingCount = 0;
37316
- for (let i = 0; i < lines.length; i++) {
37317
- const line = lines[i] ?? "";
37318
- if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
37319
- const matches = line.match(streamOpPattern);
37320
- if (matches && matches.length >= context.chainThreshold) {
37321
- if (firstOffendingLine === 0) {
37322
- firstOffendingLine = i + 1;
37323
- firstOffendingCount = matches.length;
37324
- }
37325
- }
37326
- }
37327
- if (firstOffendingLine === 0) return issues;
37328
- issues.push({
37329
- ruleId: "java/stream-overuse",
37330
- category: "typo",
37331
- severity: "low",
37332
- aiSpecific: true,
37333
- message: `${firstOffendingCount} stream operations on a single line \u2014 for-loop is faster`,
37334
- line: firstOffendingLine,
37335
- column: 1,
37336
- advice: "Prefer a for-loop for simple transformations. AI agents default to Stream API chains because their training data emphasizes functional-style Java. Stream chains allocate intermediate objects; for small collections, a for-loop is faster and more readable. Reference: java/stream-overuse v0.26.0."
37337
- });
37338
- return issues;
37339
- }
37340
- });
37341
-
37342
- // src/rules/java/string-concat-loop.ts
37343
- var STRING_CONCAT_REGEX = /(\b\w+)\s*=\s*\1\s*\+\s*[^;]+;|(\b\w+)\s*\+=\s*['"`]/g;
37344
- var javaStringConcatLoopRule = createRule({
37345
- id: "java/string-concat-loop",
37346
- category: "perf",
37347
- severity: "low",
37348
- aiSpecific: true,
37349
- description: "String concatenation in a loop \u2014 use StringBuilder",
37350
- create(_context) {
37351
- return {};
37352
- },
37353
- analyze(_context, facts) {
37354
- const issues = [];
37355
- const source = facts.v2?._source;
37356
- if (!source) return issues;
37357
- if (!/\.java$/i.test(facts.filePath)) return issues;
37358
- if (!/\b(for|while|do)\b/.test(source)) return issues;
37359
- let m;
37360
- STRING_CONCAT_REGEX.lastIndex = 0;
37361
- while ((m = STRING_CONCAT_REGEX.exec(source)) !== null) {
37362
- const line = source.slice(0, m.index).split("\n").length;
37363
- issues.push({
37364
- ruleId: "java/string-concat-loop",
37365
- category: "perf",
37366
- severity: "low",
37367
- aiSpecific: true,
37368
- message: `String concatenation in a loop at line ${line} \u2014 use StringBuilder`,
37369
- line,
37370
- column: 1,
37371
- advice: "Declare a `StringBuilder` outside the loop: `StringBuilder sb = new StringBuilder(); sb.append(...);` then `return sb.toString();` after the loop. String concatenation in a loop is O(n\xB2) \u2014 each iteration copies the prior string. AI agents concatenate strings in loops because of training-data examples. Reference: java/string-concat-loop v0.20."
37372
- });
37373
- }
37374
- return issues;
37375
- }
37376
- });
37377
-
37378
- // src/rules/java/system-out-println.ts
37379
- var PRINTLN_REGEX = /System\.out\.println\s*\(/g;
37380
- var DEFAULT_THRESHOLD2 = 1;
37381
- var javaSystemOutPrintlnRule = createRule({
37382
- id: "java/system-out-println",
37383
- category: "typo",
37384
- severity: "low",
37385
- aiSpecific: true,
37386
- description: "System.out.println in production code \u2014 use a logger (SLF4J, Log4j, etc.)",
37387
- create(_context) {
37388
- return { threshold: DEFAULT_THRESHOLD2 };
37389
- },
37390
- analyze(context, facts) {
37391
- const issues = [];
37392
- const source = facts.v2?._source;
37393
- if (!source) return issues;
37394
- if (!/\.java$/i.test(facts.filePath)) return issues;
37395
- const matches = [];
37396
- let m;
37397
- PRINTLN_REGEX.lastIndex = 0;
37398
- while ((m = PRINTLN_REGEX.exec(source)) !== null) {
37399
- matches.push(m.index);
37400
- }
37401
- if (matches.length <= context.threshold) return issues;
37402
- const cap = Math.min(matches.length, 10);
37403
- for (let i = 0; i < cap; i++) {
37404
- const idx = matches[i];
37405
- const line = source.slice(0, idx).split("\n").length;
37406
- issues.push({
37407
- ruleId: "java/system-out-println",
37408
- category: "typo",
37409
- severity: "low",
37410
- aiSpecific: true,
37411
- message: `System.out.println at line ${line} \u2014 use a logger for production output`,
37412
- line,
37413
- column: 1,
37414
- advice: "Replace with `private static final Logger log = LoggerFactory.getLogger(...);` then `log.info(...)`. AI agents default to println because their training data has countless textbook examples. Real Java code uses a logger. Reference: java/system-out-println v0.20."
37415
- });
37416
- }
37417
- return issues;
37418
- }
37419
- });
37420
-
37421
- // src/rules/java/verbose-javadoc.ts
37422
- var TAG_REGEX = /@(param|return|throws)\b/g;
37423
- var DEFAULT_TAG_THRESHOLD = 3;
37424
- var DEFAULT_BODY_LENGTH_CAP = 5;
37425
- var javaVerboseJavadocRule = createRule({
37426
- id: "java/verbose-javadoc",
37427
- category: "typo",
37428
- severity: "low",
37429
- aiSpecific: true,
37430
- description: "Excessive Javadoc tags on trivial methods \u2014 over-documentation is an AI fingerprint",
37431
- create(_context) {
37432
- return {
37433
- tagThreshold: DEFAULT_TAG_THRESHOLD,
37434
- bodyLengthCap: DEFAULT_BODY_LENGTH_CAP
37435
- };
37436
- },
37437
- analyze(context, facts) {
37438
- const issues = [];
37439
- const source = facts.v2?._source;
37440
- if (!source) return issues;
37441
- if (!/\.java$/i.test(facts.filePath)) return issues;
37442
- TAG_REGEX.lastIndex = 0;
37443
- let tagCount = 0;
37444
- let firstTagLine = 0;
37445
- let m;
37446
- while ((m = TAG_REGEX.exec(source)) !== null) {
37447
- tagCount++;
37448
- if (firstTagLine === 0) {
37449
- firstTagLine = source.slice(0, m.index).split("\n").length;
37450
- }
37451
- }
37452
- if (tagCount < context.tagThreshold) return issues;
37453
- const lineCount = source.split("\n").length;
37454
- const tagDensity = tagCount / Math.max(lineCount, 1);
37455
- if (tagDensity < 0.05) return issues;
37456
- if (lineCount > 200) return issues;
37457
- issues.push({
37458
- ruleId: "java/verbose-javadoc",
37459
- category: "typo",
37460
- severity: "low",
37461
- aiSpecific: true,
37462
- message: `${tagCount} Javadoc tags in ${lineCount} lines (density ${(tagDensity * 100).toFixed(1)}%) \u2014 likely over-documented`,
37463
- line: firstTagLine,
37464
- column: 1,
37465
- advice: "Skip Javadoc on trivial methods (getters, setters, builders). AI agents default to over-documentation because their training data has countless textbook Javadoc examples. Real Java code limits Javadoc to public API surface. Reference: java/verbose-javadoc v0.26.0."
37466
- });
37467
- return issues;
37468
- }
37469
- });
37470
-
37471
36974
  // src/rules/kotlin/coroutine-global-scope.ts
37472
36975
  var GLOBAL_SCOPE_REGEX = /GlobalScope\s*\.\s*(?:launch|async|runBlocking)\s*[({]/g;
37473
36976
  var kotlinCoroutineGlobalScopeRule = createRule({
@@ -37483,7 +36986,7 @@ var kotlinCoroutineGlobalScopeRule = createRule({
37483
36986
  const issues = [];
37484
36987
  const source = facts.v2?._source;
37485
36988
  if (!source) return issues;
37486
- if (!/\.kt$/i.test(facts.filePath)) return issues;
36989
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
37487
36990
  let m;
37488
36991
  GLOBAL_SCOPE_REGEX.lastIndex = 0;
37489
36992
  while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
@@ -37520,7 +37023,7 @@ var kotlinDataClassDefaultsOveruseRule = createRule({
37520
37023
  const issues = [];
37521
37024
  const source = facts.v2?._source;
37522
37025
  if (!source) return issues;
37523
- if (!/\.kt$/i.test(facts.filePath)) return issues;
37026
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
37524
37027
  let m;
37525
37028
  DATA_CLASS_HEAD_REGEX.lastIndex = 0;
37526
37029
  while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
@@ -37575,7 +37078,7 @@ var kotlinObjectSingletonMisuseRule = createRule({
37575
37078
  const issues = [];
37576
37079
  const source = facts.v2?._source;
37577
37080
  if (!source) return issues;
37578
- if (!/\.kt$/i.test(facts.filePath)) return issues;
37081
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
37579
37082
  let m;
37580
37083
  OBJECT_DECL_REGEX.lastIndex = 0;
37581
37084
  while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
@@ -37611,8 +37114,8 @@ var kotlinObjectSingletonMisuseRule = createRule({
37611
37114
  });
37612
37115
 
37613
37116
  // src/rules/kotlin/println-debug.ts
37614
- var PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
37615
- var DEFAULT_THRESHOLD3 = 1;
37117
+ var PRINTLN_REGEX = /^\s*println\s*\(/gm;
37118
+ var DEFAULT_THRESHOLD = 1;
37616
37119
  var kotlinPrintlnDebugRule = createRule({
37617
37120
  id: "kotlin/println-debug",
37618
37121
  category: "typo",
@@ -37620,17 +37123,17 @@ var kotlinPrintlnDebugRule = createRule({
37620
37123
  aiSpecific: true,
37621
37124
  description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
37622
37125
  create(_context) {
37623
- return { threshold: DEFAULT_THRESHOLD3 };
37126
+ return { threshold: DEFAULT_THRESHOLD };
37624
37127
  },
37625
37128
  analyze(context, facts) {
37626
37129
  const issues = [];
37627
37130
  const source = facts.v2?._source;
37628
37131
  if (!source) return issues;
37629
- if (!/\.kt$/i.test(facts.filePath)) return issues;
37132
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
37630
37133
  const matches = [];
37631
37134
  let m;
37632
- PRINTLN_REGEX2.lastIndex = 0;
37633
- while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
37135
+ PRINTLN_REGEX.lastIndex = 0;
37136
+ while ((m = PRINTLN_REGEX.exec(source)) !== null) {
37634
37137
  matches.push(m.index);
37635
37138
  }
37636
37139
  if (matches.length <= context.threshold) return issues;
@@ -37654,7 +37157,7 @@ var kotlinPrintlnDebugRule = createRule({
37654
37157
  });
37655
37158
 
37656
37159
  // src/rules/kotlin/string-concat-loop.ts
37657
- var STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
37160
+ var STRING_CONCAT_REGEX = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
37658
37161
  var kotlinStringConcatLoopRule = createRule({
37659
37162
  id: "kotlin/string-concat-loop",
37660
37163
  category: "perf",
@@ -37668,11 +37171,11 @@ var kotlinStringConcatLoopRule = createRule({
37668
37171
  const issues = [];
37669
37172
  const source = facts.v2?._source;
37670
37173
  if (!source) return issues;
37671
- if (!/\.kt$/i.test(facts.filePath)) return issues;
37174
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
37672
37175
  if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
37673
37176
  let m;
37674
- STRING_CONCAT_REGEX2.lastIndex = 0;
37675
- while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
37177
+ STRING_CONCAT_REGEX.lastIndex = 0;
37178
+ while ((m = STRING_CONCAT_REGEX.exec(source)) !== null) {
37676
37179
  const line = source.slice(0, m.index).split("\n").length;
37677
37180
  const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
37678
37181
  if (/\.append\s*\(/.test(lineText)) continue;
@@ -40550,7 +40053,7 @@ var swiftImplicitlyUnwrappedOptionalRule = createRule({
40550
40053
 
40551
40054
  // src/rules/swift/print-debug.ts
40552
40055
  var PRINT_REGEX = /\bprint\s*\(/g;
40553
- var DEFAULT_THRESHOLD4 = 1;
40056
+ var DEFAULT_THRESHOLD2 = 1;
40554
40057
  var swiftPrintDebugRule = createRule({
40555
40058
  id: "swift/print-debug",
40556
40059
  category: "typo",
@@ -40558,7 +40061,7 @@ var swiftPrintDebugRule = createRule({
40558
40061
  aiSpecific: true,
40559
40062
  description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
40560
40063
  create(_context) {
40561
- return { threshold: DEFAULT_THRESHOLD4 };
40064
+ return { threshold: DEFAULT_THRESHOLD2 };
40562
40065
  },
40563
40066
  analyze(context, facts) {
40564
40067
  const issues = [];
@@ -43213,17 +42716,6 @@ var builtinRules = [
43213
42716
  goErrorWrapWithoutContextRule,
43214
42717
  goNilSliceVsEmptyRule,
43215
42718
  goStructTagInconsistencyRule,
43216
- javaArraylistVsLinkedlistRule,
43217
- javaBuilderOveruseRule,
43218
- javaEmptyCatchBlockRule,
43219
- javaImmutableCollectionPreferenceRule,
43220
- javaLegacyDateApiRule,
43221
- javaOptionalOveruseRule,
43222
- javaRawTypeOveruseRule,
43223
- javaStreamOveruseRule,
43224
- javaStringConcatLoopRule,
43225
- javaSystemOutPrintlnRule,
43226
- javaVerboseJavadocRule,
43227
42719
  kotlinCoroutineGlobalScopeRule,
43228
42720
  kotlinDataClassDefaultsOveruseRule,
43229
42721
  kotlinObjectSingletonMisuseRule,
@@ -44088,224 +43580,6 @@ var signal_strength_default = {
44088
43580
  _v8Lift: 1,
44089
43581
  defaultOff: true
44090
43582
  },
44091
- "java/arraylist-vs-linkedlist": {
44092
- recall: 35e-4,
44093
- fpRate: 94e-4,
44094
- ratio: 0.37,
44095
- precision: 0.0446,
44096
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44097
- verdict: "DORMANT",
44098
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files = 92196 total. v9 TP=36, FP=771, P=4.46%, FPR=0.94%, recall=0.35%, ratio=0.37, lift=P/FPR=4.74. Rule fires more on legacy enterprise Java (neg) than on AI/ML-integration Java (pos) because modern Java idiom defaults to ArrayList. v0.20: new rule (new LinkedList<>() \u2014 use ArrayList, Effective Java Item 28).",
44099
- aiSpecific: true,
44100
- _v7Verdict: "DORMANT",
44101
- _v7Lift: 1,
44102
- _v7Recall: 0,
44103
- _v7FpRate: 0,
44104
- _v7Precision: 0,
44105
- _v8Verdict: "DORMANT",
44106
- _v8Lift: 1,
44107
- _v9Verdict: "DORMANT",
44108
- _v9Lift: 4.74,
44109
- _v9Recall: 35e-4,
44110
- _v9FpRate: 94e-4,
44111
- _v9Precision: 0.0446,
44112
- defaultOff: true
44113
- },
44114
- "java/empty-catch-block": {
44115
- recall: 8e-3,
44116
- fpRate: 0.1118,
44117
- ratio: 0.07,
44118
- precision: 89e-4,
44119
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44120
- verdict: "DORMANT",
44121
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files. v9 TP=82, FP=9159, P=0.89%, FPR=11.18%, recall=0.80%, ratio=0.07, lift=P/FPR=0.08. Strongly INVERTED: empty catch blocks are 14x more common in legacy enterprise Java than in AI/Java-modern pos repos. Heuristic likely needs to be tightened (e.g., require at least a comment) before it can serve as a meaningful discriminator. v0.20: new rule.",
44122
- aiSpecific: true,
44123
- _v7Verdict: "DORMANT",
44124
- _v7Lift: 1,
44125
- _v7Recall: 0,
44126
- _v7FpRate: 0,
44127
- _v7Precision: 0,
44128
- _v8Verdict: "DORMANT",
44129
- _v8Lift: 1,
44130
- _v9Verdict: "DORMANT",
44131
- _v9Lift: 0.08,
44132
- _v9Recall: 8e-3,
44133
- _v9FpRate: 0.1118,
44134
- _v9Precision: 89e-4,
44135
- defaultOff: true
44136
- },
44137
- "java/legacy-date-api": {
44138
- recall: 0.0285,
44139
- fpRate: 0.0511,
44140
- ratio: 0.56,
44141
- precision: 0.0657,
44142
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44143
- verdict: "DORMANT",
44144
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files. v9 TP=294, FP=4181, P=6.57%, FPR=5.11%, recall=2.85%, ratio=0.56, lift=P/FPR=1.29. INVERTED-to-NOISY: legacy date API is more common in older enterprise Java (neg) than in modern AI/Java repos (pos). The heuristic correctly flags the legacy pattern, but the legacy pattern is over-represented in the neg baseline (older codebases). v0.20: new rule.",
44145
- aiSpecific: true,
44146
- _v7Verdict: "DORMANT",
44147
- _v7Lift: 1,
44148
- _v7Recall: 0,
44149
- _v7FpRate: 0,
44150
- _v7Precision: 0,
44151
- _v8Verdict: "DORMANT",
44152
- _v8Lift: 1,
44153
- _v9Verdict: "DORMANT",
44154
- _v9Lift: 1.29,
44155
- _v9Recall: 0.0285,
44156
- _v9FpRate: 0.0511,
44157
- _v9Precision: 0.0657,
44158
- defaultOff: true
44159
- },
44160
- "java/verbose-javadoc": {
44161
- recall: 0,
44162
- fpRate: 0,
44163
- ratio: 1,
44164
- precision: 0,
44165
- lastCalibratedAt: "2026-07-03T00:00:00Z",
44166
- verdict: "DORMANT",
44167
- _calibrationNote: "v0.26.0: new positive AI-signal rule. Triggers when a small Java file (< 200 lines) has 3+ Javadoc tags with density >= 0.05. v0.26.1 CALIBRATION (14769-file biased sample from v9-java corpus, files that fired >=1 of the 6 existing java rules): TP=0, FP=0, ratio=N/A. HYPOTHESIS FAILED. The v0.26.0 intuition that 'AI over-documents trivial methods' is NOT supported by the v9 corpus \u2014 AI code in Spring AI, LangChain4j, etc. has the same Javadoc density as human code. v0.20 hypothesis (humans avoid println/Date/raw types) and v0.26 hypothesis (AI over-documents/over-Opts/over-Builders) are both wrong for the v9 corpus. v0.27+ will redesign with a different approach: either train a classifier on the v9 features or use AST-based detection rather than regex heuristics. Reference: java/verbose-javadoc v0.26.0; calibration v0.26.1.",
44168
- aiSpecific: true,
44169
- _v9Verdict: "DORMANT",
44170
- _v9Lift: 1,
44171
- _v9Recall: 0,
44172
- _v9FpRate: 0,
44173
- _v9Precision: 0,
44174
- defaultOff: true
44175
- },
44176
- "java/optional-overuse": {
44177
- recall: 0,
44178
- fpRate: 0,
44179
- ratio: 1,
44180
- precision: 0,
44181
- lastCalibratedAt: "2026-07-03T00:00:00Z",
44182
- verdict: "DORMANT",
44183
- _calibrationNote: "v0.26.0: new positive AI-signal rule. Triggers when a Java file has 2+ .orElseThrow() calls AND 0 Objects.requireNonNull()/null checks (optionalRatio > 0.6). v0.26.1 CALIBRATION (14769-file biased sample): TP=0, FP=0, ratio=N/A. HYPOTHESIS FAILED. AI in the v9 corpus does NOT use Optional chains more than humans. v0.27+ will redesign. Reference: java/optional-overuse v0.26.0; calibration v0.26.1.",
44184
- aiSpecific: true,
44185
- _v9Verdict: "DORMANT",
44186
- _v9Lift: 1,
44187
- _v9Recall: 0,
44188
- _v9FpRate: 0,
44189
- _v9Precision: 0,
44190
- defaultOff: true
44191
- },
44192
- "java/immutable-collection-preference": {
44193
- recall: 0,
44194
- fpRate: 0,
44195
- ratio: 1,
44196
- precision: 0,
44197
- lastCalibratedAt: "2026-07-03T00:00:00Z",
44198
- verdict: "DORMANT",
44199
- _calibrationNote: "v0.26.0: new positive AI-signal rule. Triggers when a Java file has 5+ List.of/Map.of/Set.of calls AND < 1 new ArrayList/HashMap/HashSet/LinkedList/TreeMap calls. v0.26.1 CALIBRATION (14769-file biased sample): TP=0, FP=0, ratio=N/A. HYPOTHESIS FAILED. AI does NOT use immutable factory methods more than humans. v0.27+ will redesign. Reference: java/immutable-collection-preference v0.26.0; calibration v0.26.1.",
44200
- aiSpecific: true,
44201
- _v9Verdict: "DORMANT",
44202
- _v9Lift: 1,
44203
- _v9Recall: 0,
44204
- _v9FpRate: 0,
44205
- _v9Precision: 0,
44206
- defaultOff: true
44207
- },
44208
- "java/builder-overuse": {
44209
- recall: 0,
44210
- fpRate: 0,
44211
- ratio: 1,
44212
- precision: 0,
44213
- lastCalibratedAt: "2026-07-03T00:00:00Z",
44214
- verdict: "DORMANT",
44215
- _calibrationNote: "v0.26.0: new positive AI-signal rule. Triggers when @Builder annotation (Lombok) is used on a class with <= 3 fields. v0.26.1 CALIBRATION (14769-file biased sample): TP=0, FP=0, ratio=N/A. HYPOTHESIS FAILED. AI does NOT use @Builder on small classes more than humans. v0.27+ will redesign. Reference: java/builder-overuse v0.26.0; calibration v0.26.1.",
44216
- aiSpecific: true,
44217
- _v9Verdict: "DORMANT",
44218
- _v9Lift: 1,
44219
- _v9Recall: 0,
44220
- _v9FpRate: 0,
44221
- _v9Precision: 0,
44222
- defaultOff: true
44223
- },
44224
- "java/stream-overuse": {
44225
- recall: 0,
44226
- fpRate: 0,
44227
- ratio: 1,
44228
- precision: 0,
44229
- lastCalibratedAt: "2026-07-03T00:00:00Z",
44230
- verdict: "DORMANT",
44231
- _calibrationNote: "v0.26.0: new positive AI-signal rule. Triggers when a single line has 3+ Stream API operations (.map, .filter, .flatMap, .collect, .reduce, .sorted, .distinct, .limit, .skip, .anyMatch, .allMatch, .noneMatch, .findFirst, .findAny). v0.26.1 CALIBRATION (14769-file biased sample): TP=0, FP=0, ratio=N/A. HYPOTHESIS FAILED. AI does NOT use Stream API chains more than humans. v0.27+ will redesign. Reference: java/stream-overuse v0.26.0; calibration v0.26.1.",
44232
- aiSpecific: true,
44233
- _v9Verdict: "DORMANT",
44234
- _v9Lift: 1,
44235
- _v9Recall: 0,
44236
- _v9FpRate: 0,
44237
- _v9Precision: 0,
44238
- defaultOff: true
44239
- },
44240
- "java/raw-type-overuse": {
44241
- recall: 0.1093,
44242
- fpRate: 0.2041,
44243
- ratio: 0.54,
44244
- precision: 0.0632,
44245
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44246
- verdict: "DORMANT",
44247
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files. v9 TP=1126, FP=16712, P=6.32%, FPR=20.41%, recall=10.93%, ratio=0.54, lift=P/FPR=0.31. Strongly INVERTED: raw types (no generics) are 2x more common in legacy enterprise Java (neg) than in modern Java repos (pos). Heuristic is correctly identifying the legacy anti-pattern, but it's an age marker more than an AI-fingerprint. v0.20: new rule.",
44248
- aiSpecific: true,
44249
- _v7Verdict: "DORMANT",
44250
- _v7Lift: 1,
44251
- _v7Recall: 0,
44252
- _v7FpRate: 0,
44253
- _v7Precision: 0,
44254
- _v8Verdict: "DORMANT",
44255
- _v8Lift: 1,
44256
- _v9Verdict: "DORMANT",
44257
- _v9Lift: 0.31,
44258
- _v9Recall: 0.1093,
44259
- _v9FpRate: 0.2041,
44260
- _v9Precision: 0.0632,
44261
- defaultOff: true
44262
- },
44263
- "java/string-concat-loop": {
44264
- recall: 46e-4,
44265
- fpRate: 0.0358,
44266
- ratio: 0.13,
44267
- precision: 0.0158,
44268
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44269
- verdict: "DORMANT",
44270
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files. v9 TP=47, FP=2933, P=1.58%, FPR=3.58%, recall=0.46%, ratio=0.13, lift=P/FPR=0.44. Strongly INVERTED: string concatenation in a loop is 8x more common in legacy enterprise Java (neg) than in AI/ML/Java-modern pos repos. Modern Java training data and modern IDE warnings discourage the pattern. v0.20: new rule.",
44271
- aiSpecific: true,
44272
- _v7Verdict: "DORMANT",
44273
- _v7Lift: 1,
44274
- _v7Recall: 0,
44275
- _v7FpRate: 0,
44276
- _v7Precision: 0,
44277
- _v8Verdict: "DORMANT",
44278
- _v8Lift: 1,
44279
- _v9Verdict: "DORMANT",
44280
- _v9Lift: 0.44,
44281
- _v9Recall: 46e-4,
44282
- _v9FpRate: 0.0358,
44283
- _v9Precision: 0.0158,
44284
- defaultOff: true
44285
- },
44286
- "java/system-out-println": {
44287
- recall: 0.1503,
44288
- fpRate: 0.2533,
44289
- ratio: 0.59,
44290
- precision: 0.0695,
44291
- lastCalibratedAt: "2026-07-02T19:34:00Z",
44292
- verdict: "DORMANT",
44293
- _calibrationNote: "v9 Java corpus calibration (v0.25.0, 2026-07-02): 81891 neg + 10305 pos Java files. v9 TP=1549, FP=20737, P=6.95%, FPR=25.32%, recall=15.03%, ratio=0.59, lift=P/FPR=0.27. INVERTED: System.out.println is more common in legacy enterprise Java (neg) than in modern AI/Java repos (pos). Modern Java defaults to SLF4J/Log4j, AI-generated Java starts with a logger import. The pattern is the opposite of the v0.20 design intent (which assumed println was the AI-fingerprint signal). v0.25.x: consider gating this rule by Java era (recent file mtime) or making it an age-fingerprint rather than AI-fingerprint. v0.20: new rule.",
44294
- aiSpecific: true,
44295
- _v7Verdict: "DORMANT",
44296
- _v7Lift: 1,
44297
- _v7Recall: 0,
44298
- _v7FpRate: 0,
44299
- _v7Precision: 0,
44300
- _v8Verdict: "DORMANT",
44301
- _v8Lift: 1,
44302
- _v9Verdict: "DORMANT",
44303
- _v9Lift: 0.27,
44304
- _v9Recall: 0.1503,
44305
- _v9FpRate: 0.2533,
44306
- _v9Precision: 0.0695,
44307
- defaultOff: true
44308
- },
44309
43583
  "layout/forced-layout": {
44310
43584
  recall: 0,
44311
43585
  fpRate: 0,
@@ -45550,13 +44824,13 @@ var signal_strength_default = {
45550
44824
  defaultOff: true
45551
44825
  },
45552
44826
  "kotlin/data-class-defaults-overuse": {
45553
- recall: 0,
45554
- fpRate: 0,
45555
- ratio: 1,
45556
- precision: 0,
45557
- lastCalibratedAt: "2026-07-02T00:00:00Z",
44827
+ recall: 469e-5,
44828
+ fpRate: 222e-5,
44829
+ ratio: 2.11,
44830
+ precision: 0.1429,
44831
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
45558
44832
  verdict: "DORMANT",
45559
- _calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
44833
+ _calibrationNote: "v0.28: v9 Kotlin calibration (2698 neg, 213 pos). ratio=2.11 (\u22651.5) but precision=14.3% (<50%) \u2014 verdict DORMANT. Era-confounding: pre-2022 Kotlin is sparse; the rule fires on the rare .kt file with multiple default values regardless of authorship.",
45560
44834
  aiSpecific: true,
45561
44835
  _v7Verdict: "DORMANT",
45562
44836
  _v7Lift: 1,
@@ -45565,16 +44839,21 @@ var signal_strength_default = {
45565
44839
  _v7Precision: 0,
45566
44840
  _v8Verdict: "DORMANT",
45567
44841
  _v8Lift: 1,
44842
+ _v9Verdict: "DORMANT",
44843
+ _v9Lift: 2.11,
44844
+ _v9Recall: 469e-5,
44845
+ _v9FpRate: 222e-5,
44846
+ _v9Precision: 0.1429,
45568
44847
  defaultOff: true
45569
44848
  },
45570
44849
  "kotlin/coroutine-global-scope": {
45571
- recall: 0,
45572
- fpRate: 0,
45573
- ratio: 1,
45574
- precision: 0,
45575
- lastCalibratedAt: "2026-07-02T00:00:00Z",
44850
+ recall: 469e-5,
44851
+ fpRate: 0.05004,
44852
+ ratio: 0.09,
44853
+ precision: 735e-5,
44854
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
45576
44855
  verdict: "DORMANT",
45577
- _calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
44856
+ _calibrationNote: "v0.28: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.09 (FAR <1.0) \u2014 fires 135x more on neg than pos. Classic era-confound: pre-2022 Kotlin coroutines had GlobalScope as the default; modern Kotlin uses viewModelScope / lifecycleScope. The rule detects era, not AI authorship.",
45578
44857
  aiSpecific: true,
45579
44858
  _v7Verdict: "DORMANT",
45580
44859
  _v7Lift: 1,
@@ -45583,16 +44862,21 @@ var signal_strength_default = {
45583
44862
  _v7Precision: 0,
45584
44863
  _v8Verdict: "DORMANT",
45585
44864
  _v8Lift: 1,
44865
+ _v9Verdict: "DORMANT",
44866
+ _v9Lift: 0.09,
44867
+ _v9Recall: 469e-5,
44868
+ _v9FpRate: 0.05004,
44869
+ _v9Precision: 735e-5,
45586
44870
  defaultOff: true
45587
44871
  },
45588
44872
  "kotlin/println-debug": {
45589
- recall: 0,
45590
- fpRate: 0,
45591
- ratio: 1,
45592
- precision: 0,
45593
- lastCalibratedAt: "2026-07-02T00:00:00Z",
44873
+ recall: 0.07042,
44874
+ fpRate: 0.11379,
44875
+ ratio: 0.62,
44876
+ precision: 0.04659,
44877
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
45594
44878
  verdict: "DORMANT",
45595
- _calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
44879
+ _calibrationNote: "v0.28: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.62 (\u226A1.0) \u2014 fires 20x more on neg than pos. Classic era-confound: pre-2022 Kotlin code uses println() for debug; modern Kotlin uses Timber / android.util.Log / kermit.",
45596
44880
  aiSpecific: true,
45597
44881
  _v7Verdict: "DORMANT",
45598
44882
  _v7Lift: 1,
@@ -45601,16 +44885,21 @@ var signal_strength_default = {
45601
44885
  _v7Precision: 0,
45602
44886
  _v8Verdict: "DORMANT",
45603
44887
  _v8Lift: 1,
44888
+ _v9Verdict: "DORMANT",
44889
+ _v9Lift: 0.62,
44890
+ _v9Recall: 0.07042,
44891
+ _v9FpRate: 0.11379,
44892
+ _v9Precision: 0.04659,
45604
44893
  defaultOff: true
45605
44894
  },
45606
44895
  "kotlin/object-singleton-misuse": {
45607
- recall: 0,
45608
- fpRate: 0,
45609
- ratio: 1,
45610
- precision: 0,
45611
- lastCalibratedAt: "2026-07-02T00:00:00Z",
44896
+ recall: 469e-5,
44897
+ fpRate: 371e-5,
44898
+ ratio: 1.27,
44899
+ precision: 0.09091,
44900
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
45612
44901
  verdict: "DORMANT",
45613
- _calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
44902
+ _calibrationNote: "v0.28: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.27 (\u22651.0) but <1.5; precision=9.1% \u2014 verdict DORMANT. INSUFFICIENT_DATA: pos arm only 213 files (under 10k minimum).",
45614
44903
  aiSpecific: true,
45615
44904
  _v7Verdict: "DORMANT",
45616
44905
  _v7Lift: 1,
@@ -45619,16 +44908,21 @@ var signal_strength_default = {
45619
44908
  _v7Precision: 0,
45620
44909
  _v8Verdict: "DORMANT",
45621
44910
  _v8Lift: 1,
44911
+ _v9Verdict: "DORMANT",
44912
+ _v9Lift: 1.27,
44913
+ _v9Recall: 469e-5,
44914
+ _v9FpRate: 371e-5,
44915
+ _v9Precision: 0.09091,
45622
44916
  defaultOff: true
45623
44917
  },
45624
44918
  "kotlin/string-concat-loop": {
45625
- recall: 0,
45626
- fpRate: 0,
45627
- ratio: 1,
45628
- precision: 0,
45629
- lastCalibratedAt: "2026-07-02T00:00:00Z",
44919
+ recall: 939e-5,
44920
+ fpRate: 519e-5,
44921
+ ratio: 1.81,
44922
+ precision: 0.125,
44923
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
45630
44924
  verdict: "DORMANT",
45631
- _calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
44925
+ _calibrationNote: "v0.28: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.81 (\u22651.5) but precision=12.5% (<50%) \u2014 verdict DORMANT. INSUFFICIENT_DATA: pos arm only 213 files.",
45632
44926
  aiSpecific: true,
45633
44927
  _v7Verdict: "DORMANT",
45634
44928
  _v7Lift: 1,
@@ -45637,6 +44931,11 @@ var signal_strength_default = {
45637
44931
  _v7Precision: 0,
45638
44932
  _v8Verdict: "DORMANT",
45639
44933
  _v8Lift: 1,
44934
+ _v9Verdict: "DORMANT",
44935
+ _v9Lift: 1.81,
44936
+ _v9Recall: 939e-5,
44937
+ _v9FpRate: 519e-5,
44938
+ _v9Precision: 0.125,
45640
44939
  defaultOff: true
45641
44940
  },
45642
44941
  "swift/force-unwrap": {
@@ -45869,8 +45168,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
45869
45168
  const ext = extname4(filePath).toLowerCase();
45870
45169
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
45871
45170
  ".swift",
45872
- ".kt",
45873
- ".kts",
45874
45171
  ".dart",
45875
45172
  ".cpp",
45876
45173
  ".cc",