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.
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.26.1";
39
+ VERSION = "0.28.0";
40
40
  }
41
41
  });
42
42
 
@@ -30662,589 +30662,6 @@ var init_struct_tag_inconsistency = __esm({
30662
30662
  }
30663
30663
  });
30664
30664
 
30665
- // src/rules/java/arraylist-vs-linkedlist.ts
30666
- var NEW_LINKED_LIST_REGEX, javaArraylistVsLinkedlistRule;
30667
- var init_arraylist_vs_linkedlist = __esm({
30668
- "src/rules/java/arraylist-vs-linkedlist.ts"() {
30669
- "use strict";
30670
- init_rule();
30671
- NEW_LINKED_LIST_REGEX = /new\s+LinkedList\s*</g;
30672
- javaArraylistVsLinkedlistRule = createRule({
30673
- id: "java/arraylist-vs-linkedlist",
30674
- category: "typo",
30675
- severity: "low",
30676
- aiSpecific: true,
30677
- description: "new LinkedList<>() \u2014 use ArrayList (Effective Java, Item 28)",
30678
- create(_context) {
30679
- return {};
30680
- },
30681
- analyze(_context, facts) {
30682
- const issues = [];
30683
- const source = facts.v2?._source;
30684
- if (!source) return issues;
30685
- if (!/\.java$/i.test(facts.filePath)) return issues;
30686
- let m;
30687
- NEW_LINKED_LIST_REGEX.lastIndex = 0;
30688
- while ((m = NEW_LINKED_LIST_REGEX.exec(source)) !== null) {
30689
- const line = source.slice(0, m.index).split("\n").length;
30690
- issues.push({
30691
- ruleId: "java/arraylist-vs-linkedlist",
30692
- category: "typo",
30693
- severity: "low",
30694
- aiSpecific: true,
30695
- message: `new LinkedList at line ${line} \u2014 use ArrayList instead`,
30696
- line,
30697
- column: 1,
30698
- 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."
30699
- });
30700
- }
30701
- return issues;
30702
- }
30703
- });
30704
- }
30705
- });
30706
-
30707
- // src/rules/java/builder-overuse.ts
30708
- var BUILDER_ANNOTATION_REGEX, FIELD_DECL_REGEX, DEFAULT_FIELD_COUNT_CAP, javaBuilderOveruseRule;
30709
- var init_builder_overuse = __esm({
30710
- "src/rules/java/builder-overuse.ts"() {
30711
- "use strict";
30712
- init_rule();
30713
- BUILDER_ANNOTATION_REGEX = /@Builder\b/;
30714
- FIELD_DECL_REGEX = /(?:private|public|protected)\s+(?:final\s+)?[\w<>,\s]+\s+(\w+)\s*[=;]/g;
30715
- DEFAULT_FIELD_COUNT_CAP = 3;
30716
- javaBuilderOveruseRule = createRule({
30717
- id: "java/builder-overuse",
30718
- category: "typo",
30719
- severity: "low",
30720
- aiSpecific: true,
30721
- description: "@Builder on a class with few fields \u2014 plain constructor is simpler",
30722
- create(_context) {
30723
- return { fieldCountCap: DEFAULT_FIELD_COUNT_CAP };
30724
- },
30725
- analyze(context, facts) {
30726
- const issues = [];
30727
- const source = facts.v2?._source;
30728
- if (!source) return issues;
30729
- if (!/\.java$/i.test(facts.filePath)) return issues;
30730
- BUILDER_ANNOTATION_REGEX.lastIndex = 0;
30731
- const builderMatch = BUILDER_ANNOTATION_REGEX.exec(source);
30732
- if (!builderMatch) return issues;
30733
- const firstBuilderLine = source.slice(0, builderMatch.index).split("\n").length;
30734
- FIELD_DECL_REGEX.lastIndex = 0;
30735
- let fieldCount = 0;
30736
- while (FIELD_DECL_REGEX.exec(source) !== null) {
30737
- fieldCount++;
30738
- }
30739
- if (fieldCount > context.fieldCountCap) return issues;
30740
- issues.push({
30741
- ruleId: "java/builder-overuse",
30742
- category: "typo",
30743
- severity: "low",
30744
- aiSpecific: true,
30745
- message: `@Builder on a class with ${fieldCount} field(s) \u2014 plain constructor is simpler`,
30746
- line: firstBuilderLine,
30747
- column: 1,
30748
- 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."
30749
- });
30750
- return issues;
30751
- }
30752
- });
30753
- }
30754
- });
30755
-
30756
- // src/rules/java/empty-catch-block.ts
30757
- var SINGLE_LINE_EMPTY_CATCH_REGEX, javaEmptyCatchBlockRule;
30758
- var init_empty_catch_block = __esm({
30759
- "src/rules/java/empty-catch-block.ts"() {
30760
- "use strict";
30761
- init_rule();
30762
- SINGLE_LINE_EMPTY_CATCH_REGEX = /catch\s*\([^)]*\)\s*\{\s*\}/g;
30763
- javaEmptyCatchBlockRule = createRule({
30764
- id: "java/empty-catch-block",
30765
- category: "logic",
30766
- severity: "medium",
30767
- aiSpecific: true,
30768
- description: "Empty catch block \u2014 silently swallows exceptions",
30769
- create(_context) {
30770
- return {};
30771
- },
30772
- analyze(_context, facts) {
30773
- const issues = [];
30774
- const source = facts.v2?._source;
30775
- if (!source) return issues;
30776
- if (!/\.java$/i.test(facts.filePath)) return issues;
30777
- let m;
30778
- SINGLE_LINE_EMPTY_CATCH_REGEX.lastIndex = 0;
30779
- while ((m = SINGLE_LINE_EMPTY_CATCH_REGEX.exec(source)) !== null) {
30780
- const line = source.slice(0, m.index).split("\n").length;
30781
- issues.push({
30782
- ruleId: "java/empty-catch-block",
30783
- category: "logic",
30784
- severity: "medium",
30785
- aiSpecific: true,
30786
- message: `Empty catch block at line ${line} \u2014 exception is silently swallowed`,
30787
- line,
30788
- column: 1,
30789
- 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.'
30790
- });
30791
- }
30792
- return issues;
30793
- }
30794
- });
30795
- }
30796
- });
30797
-
30798
- // src/rules/java/immutable-collection-preference.ts
30799
- var IMMUTABLE_REGEX, MUTABLE_REGEX, DEFAULT_IMMUTABLE_THRESHOLD, DEFAULT_MUTABLE_CAP, javaImmutableCollectionPreferenceRule;
30800
- var init_immutable_collection_preference = __esm({
30801
- "src/rules/java/immutable-collection-preference.ts"() {
30802
- "use strict";
30803
- init_rule();
30804
- IMMUTABLE_REGEX = /\b(List|Map|Set)\.of\s*\(/g;
30805
- MUTABLE_REGEX = /new\s+(ArrayList|HashMap|HashSet|LinkedList|TreeMap)\s*[<(]/g;
30806
- DEFAULT_IMMUTABLE_THRESHOLD = 5;
30807
- DEFAULT_MUTABLE_CAP = 1;
30808
- javaImmutableCollectionPreferenceRule = createRule({
30809
- id: "java/immutable-collection-preference",
30810
- category: "typo",
30811
- severity: "low",
30812
- aiSpecific: true,
30813
- description: "Immutable factory method over-use \u2014 prefer mutable when collection will be modified",
30814
- create(_context) {
30815
- return {
30816
- immutableThreshold: DEFAULT_IMMUTABLE_THRESHOLD,
30817
- mutableCap: DEFAULT_MUTABLE_CAP
30818
- };
30819
- },
30820
- analyze(context, facts) {
30821
- const issues = [];
30822
- const source = facts.v2?._source;
30823
- if (!source) return issues;
30824
- if (!/\.java$/i.test(facts.filePath)) return issues;
30825
- IMMUTABLE_REGEX.lastIndex = 0;
30826
- let immutableCount = 0;
30827
- let firstIdx = 0;
30828
- let m;
30829
- while ((m = IMMUTABLE_REGEX.exec(source)) !== null) {
30830
- immutableCount++;
30831
- if (firstIdx === 0) firstIdx = m.index;
30832
- }
30833
- if (immutableCount < context.immutableThreshold) return issues;
30834
- MUTABLE_REGEX.lastIndex = 0;
30835
- let mutableCount = 0;
30836
- while ((m = MUTABLE_REGEX.exec(source)) !== null) {
30837
- mutableCount++;
30838
- }
30839
- if (mutableCount > context.mutableCap) return issues;
30840
- const line = source.slice(0, firstIdx).split("\n").length;
30841
- issues.push({
30842
- ruleId: "java/immutable-collection-preference",
30843
- category: "typo",
30844
- severity: "low",
30845
- aiSpecific: true,
30846
- message: `${immutableCount} immutable factory calls (List/Map/Set.of) with only ${mutableCount} mutable collection \u2014 likely over-preferring immutability`,
30847
- line,
30848
- column: 1,
30849
- 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."
30850
- });
30851
- return issues;
30852
- }
30853
- });
30854
- }
30855
- });
30856
-
30857
- // src/rules/java/legacy-date-api.ts
30858
- var LEGACY_IMPORT_REGEX, LEGACY_USAGE_REGEX, CALENDAR_GET_INSTANCE_REGEX, javaLegacyDateApiRule;
30859
- var init_legacy_date_api = __esm({
30860
- "src/rules/java/legacy-date-api.ts"() {
30861
- "use strict";
30862
- init_rule();
30863
- LEGACY_IMPORT_REGEX = /^import\s+(?:static\s+)?java\.(?:util|sql)\.(?:Date|Calendar|GregorianCalendar)\s*;/gm;
30864
- LEGACY_USAGE_REGEX = /\bnew\s+(?:Date|GregorianCalendar)\s*\(/g;
30865
- CALENDAR_GET_INSTANCE_REGEX = /Calendar\.getInstance\s*\(/g;
30866
- javaLegacyDateApiRule = createRule({
30867
- id: "java/legacy-date-api",
30868
- category: "typo",
30869
- severity: "low",
30870
- aiSpecific: true,
30871
- description: "Legacy java.util.Date / Calendar \u2014 use java.time (JSR-310) from Java 8+",
30872
- create(_context) {
30873
- return {};
30874
- },
30875
- analyze(_context, facts) {
30876
- const issues = [];
30877
- const source = facts.v2?._source;
30878
- if (!source) return issues;
30879
- if (!/\.java$/i.test(facts.filePath)) return issues;
30880
- const flagged = /* @__PURE__ */ new Set();
30881
- let m;
30882
- LEGACY_IMPORT_REGEX.lastIndex = 0;
30883
- while ((m = LEGACY_IMPORT_REGEX.exec(source)) !== null) {
30884
- const line = source.slice(0, m.index).split("\n").length;
30885
- flagged.add(line);
30886
- issues.push({
30887
- ruleId: "java/legacy-date-api",
30888
- category: "typo",
30889
- severity: "low",
30890
- aiSpecific: true,
30891
- message: `Legacy date import at line ${line} \u2014 use java.time (JSR-310) from Java 8+`,
30892
- line,
30893
- column: 1,
30894
- 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."
30895
- });
30896
- }
30897
- LEGACY_USAGE_REGEX.lastIndex = 0;
30898
- while ((m = LEGACY_USAGE_REGEX.exec(source)) !== null) {
30899
- const line = source.slice(0, m.index).split("\n").length;
30900
- if (flagged.has(line)) continue;
30901
- issues.push({
30902
- ruleId: "java/legacy-date-api",
30903
- category: "typo",
30904
- severity: "low",
30905
- aiSpecific: true,
30906
- message: `new Date() / GregorianCalendar at line ${line} \u2014 use java.time`,
30907
- line,
30908
- column: 1,
30909
- advice: "Replace with `LocalDate.now()`, `Instant.now()`, or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
30910
- });
30911
- }
30912
- CALENDAR_GET_INSTANCE_REGEX.lastIndex = 0;
30913
- while ((m = CALENDAR_GET_INSTANCE_REGEX.exec(source)) !== null) {
30914
- const line = source.slice(0, m.index).split("\n").length;
30915
- if (flagged.has(line)) continue;
30916
- issues.push({
30917
- ruleId: "java/legacy-date-api",
30918
- category: "typo",
30919
- severity: "low",
30920
- aiSpecific: true,
30921
- message: `Calendar.getInstance() at line ${line} \u2014 use java.time`,
30922
- line,
30923
- column: 1,
30924
- advice: "Replace with `LocalDate.now()` (date-only) or `ZonedDateTime.now()`. Reference: java/legacy-date-api v0.20."
30925
- });
30926
- }
30927
- return issues;
30928
- }
30929
- });
30930
- }
30931
- });
30932
-
30933
- // src/rules/java/optional-overuse.ts
30934
- var OR_ELSE_THROW_REGEX, NULL_CHECK_REGEX, DEFAULT_THRESHOLD, javaOptionalOveruseRule;
30935
- var init_optional_overuse = __esm({
30936
- "src/rules/java/optional-overuse.ts"() {
30937
- "use strict";
30938
- init_rule();
30939
- OR_ELSE_THROW_REGEX = /\.orElseThrow\s*\(/g;
30940
- NULL_CHECK_REGEX = /Objects\.requireNonNull\s*\(|if\s*\([^)]*==\s*null\)\s*(?:throw|return)/g;
30941
- DEFAULT_THRESHOLD = 2;
30942
- javaOptionalOveruseRule = createRule({
30943
- id: "java/optional-overuse",
30944
- category: "typo",
30945
- severity: "low",
30946
- aiSpecific: true,
30947
- description: "Optional chain over-use \u2014 null check or Objects.requireNonNull is faster",
30948
- create(_context) {
30949
- return { threshold: DEFAULT_THRESHOLD };
30950
- },
30951
- analyze(context, facts) {
30952
- const issues = [];
30953
- const source = facts.v2?._source;
30954
- if (!source) return issues;
30955
- if (!/\.java$/i.test(facts.filePath)) return issues;
30956
- OR_ELSE_THROW_REGEX.lastIndex = 0;
30957
- let orElseThrowCount = 0;
30958
- let firstIdx = 0;
30959
- let m;
30960
- while ((m = OR_ELSE_THROW_REGEX.exec(source)) !== null) {
30961
- orElseThrowCount++;
30962
- if (firstIdx === 0) firstIdx = m.index;
30963
- }
30964
- if (orElseThrowCount < context.threshold) return issues;
30965
- NULL_CHECK_REGEX.lastIndex = 0;
30966
- const nullCheckCount = (source.match(NULL_CHECK_REGEX) ?? []).length;
30967
- const optionalRatio = orElseThrowCount / Math.max(orElseThrowCount + nullCheckCount, 1);
30968
- if (optionalRatio < 0.6) return issues;
30969
- const line = source.slice(0, firstIdx).split("\n").length;
30970
- issues.push({
30971
- ruleId: "java/optional-overuse",
30972
- category: "typo",
30973
- severity: "low",
30974
- aiSpecific: true,
30975
- message: `${orElseThrowCount} .orElseThrow() calls with ${nullCheckCount} null checks \u2014 Optional chain over-use`,
30976
- line,
30977
- column: 1,
30978
- 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.'
30979
- });
30980
- return issues;
30981
- }
30982
- });
30983
- }
30984
- });
30985
-
30986
- // src/rules/java/raw-type-overuse.ts
30987
- var RAW_TYPE_REGEX, javaRawTypeOveruseRule;
30988
- var init_raw_type_overuse = __esm({
30989
- "src/rules/java/raw-type-overuse.ts"() {
30990
- "use strict";
30991
- init_rule();
30992
- RAW_TYPE_REGEX = /\b(List|Map|Set|Collection|Iterable)\s+(?![<A-Z])(\w+)/g;
30993
- javaRawTypeOveruseRule = createRule({
30994
- id: "java/raw-type-overuse",
30995
- category: "typo",
30996
- severity: "low",
30997
- aiSpecific: true,
30998
- description: "Raw type usage (List, Map, Set) \u2014 use generics (Effective Java, Item 23)",
30999
- create(_context) {
31000
- return {};
31001
- },
31002
- analyze(_context, facts) {
31003
- const issues = [];
31004
- const source = facts.v2?._source;
31005
- if (!source) return issues;
31006
- if (!/\.java$/i.test(facts.filePath)) return issues;
31007
- let m;
31008
- RAW_TYPE_REGEX.lastIndex = 0;
31009
- while ((m = RAW_TYPE_REGEX.exec(source)) !== null) {
31010
- const typeName = m[1];
31011
- const line = source.slice(0, m.index).split("\n").length;
31012
- issues.push({
31013
- ruleId: "java/raw-type-overuse",
31014
- category: "typo",
31015
- severity: "low",
31016
- aiSpecific: true,
31017
- message: `Raw type ${typeName} at line ${line} \u2014 add type parameters`,
31018
- line,
31019
- column: 1,
31020
- 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.`
31021
- });
31022
- }
31023
- return issues;
31024
- }
31025
- });
31026
- }
31027
- });
31028
-
31029
- // src/rules/java/stream-overuse.ts
31030
- var STREAM_OPS, DEFAULT_CHAIN_THRESHOLD, javaStreamOveruseRule;
31031
- var init_stream_overuse = __esm({
31032
- "src/rules/java/stream-overuse.ts"() {
31033
- "use strict";
31034
- init_rule();
31035
- STREAM_OPS = [
31036
- ".map(",
31037
- ".filter(",
31038
- ".flatMap(",
31039
- ".collect(",
31040
- ".reduce(",
31041
- ".sorted(",
31042
- ".distinct(",
31043
- ".limit(",
31044
- ".skip(",
31045
- ".anyMatch(",
31046
- ".allMatch(",
31047
- ".noneMatch(",
31048
- ".findFirst(",
31049
- ".findAny("
31050
- ];
31051
- DEFAULT_CHAIN_THRESHOLD = 3;
31052
- javaStreamOveruseRule = createRule({
31053
- id: "java/stream-overuse",
31054
- category: "typo",
31055
- severity: "low",
31056
- aiSpecific: true,
31057
- description: "Stream API chain over-use \u2014 for-loop is faster for simple transformations",
31058
- create(_context) {
31059
- return { chainThreshold: DEFAULT_CHAIN_THRESHOLD };
31060
- },
31061
- analyze(context, facts) {
31062
- const issues = [];
31063
- const source = facts.v2?._source;
31064
- if (!source) return issues;
31065
- if (!/\.java$/i.test(facts.filePath)) return issues;
31066
- const lines = source.split("\n");
31067
- const streamOpPattern = new RegExp(STREAM_OPS.map((op) => op.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g");
31068
- let firstOffendingLine = 0;
31069
- let firstOffendingCount = 0;
31070
- for (let i = 0; i < lines.length; i++) {
31071
- const line = lines[i] ?? "";
31072
- if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
31073
- const matches = line.match(streamOpPattern);
31074
- if (matches && matches.length >= context.chainThreshold) {
31075
- if (firstOffendingLine === 0) {
31076
- firstOffendingLine = i + 1;
31077
- firstOffendingCount = matches.length;
31078
- }
31079
- }
31080
- }
31081
- if (firstOffendingLine === 0) return issues;
31082
- issues.push({
31083
- ruleId: "java/stream-overuse",
31084
- category: "typo",
31085
- severity: "low",
31086
- aiSpecific: true,
31087
- message: `${firstOffendingCount} stream operations on a single line \u2014 for-loop is faster`,
31088
- line: firstOffendingLine,
31089
- column: 1,
31090
- 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."
31091
- });
31092
- return issues;
31093
- }
31094
- });
31095
- }
31096
- });
31097
-
31098
- // src/rules/java/string-concat-loop.ts
31099
- var STRING_CONCAT_REGEX, javaStringConcatLoopRule;
31100
- var init_string_concat_loop = __esm({
31101
- "src/rules/java/string-concat-loop.ts"() {
31102
- "use strict";
31103
- init_rule();
31104
- STRING_CONCAT_REGEX = /(\b\w+)\s*=\s*\1\s*\+\s*[^;]+;|(\b\w+)\s*\+=\s*['"`]/g;
31105
- javaStringConcatLoopRule = createRule({
31106
- id: "java/string-concat-loop",
31107
- category: "perf",
31108
- severity: "low",
31109
- aiSpecific: true,
31110
- description: "String concatenation in a loop \u2014 use StringBuilder",
31111
- create(_context) {
31112
- return {};
31113
- },
31114
- analyze(_context, facts) {
31115
- const issues = [];
31116
- const source = facts.v2?._source;
31117
- if (!source) return issues;
31118
- if (!/\.java$/i.test(facts.filePath)) return issues;
31119
- if (!/\b(for|while|do)\b/.test(source)) return issues;
31120
- let m;
31121
- STRING_CONCAT_REGEX.lastIndex = 0;
31122
- while ((m = STRING_CONCAT_REGEX.exec(source)) !== null) {
31123
- const line = source.slice(0, m.index).split("\n").length;
31124
- issues.push({
31125
- ruleId: "java/string-concat-loop",
31126
- category: "perf",
31127
- severity: "low",
31128
- aiSpecific: true,
31129
- message: `String concatenation in a loop at line ${line} \u2014 use StringBuilder`,
31130
- line,
31131
- column: 1,
31132
- 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."
31133
- });
31134
- }
31135
- return issues;
31136
- }
31137
- });
31138
- }
31139
- });
31140
-
31141
- // src/rules/java/system-out-println.ts
31142
- var PRINTLN_REGEX, DEFAULT_THRESHOLD2, javaSystemOutPrintlnRule;
31143
- var init_system_out_println = __esm({
31144
- "src/rules/java/system-out-println.ts"() {
31145
- "use strict";
31146
- init_rule();
31147
- PRINTLN_REGEX = /System\.out\.println\s*\(/g;
31148
- DEFAULT_THRESHOLD2 = 1;
31149
- javaSystemOutPrintlnRule = createRule({
31150
- id: "java/system-out-println",
31151
- category: "typo",
31152
- severity: "low",
31153
- aiSpecific: true,
31154
- description: "System.out.println in production code \u2014 use a logger (SLF4J, Log4j, etc.)",
31155
- create(_context) {
31156
- return { threshold: DEFAULT_THRESHOLD2 };
31157
- },
31158
- analyze(context, facts) {
31159
- const issues = [];
31160
- const source = facts.v2?._source;
31161
- if (!source) return issues;
31162
- if (!/\.java$/i.test(facts.filePath)) return issues;
31163
- const matches = [];
31164
- let m;
31165
- PRINTLN_REGEX.lastIndex = 0;
31166
- while ((m = PRINTLN_REGEX.exec(source)) !== null) {
31167
- matches.push(m.index);
31168
- }
31169
- if (matches.length <= context.threshold) return issues;
31170
- const cap = Math.min(matches.length, 10);
31171
- for (let i = 0; i < cap; i++) {
31172
- const idx = matches[i];
31173
- const line = source.slice(0, idx).split("\n").length;
31174
- issues.push({
31175
- ruleId: "java/system-out-println",
31176
- category: "typo",
31177
- severity: "low",
31178
- aiSpecific: true,
31179
- message: `System.out.println at line ${line} \u2014 use a logger for production output`,
31180
- line,
31181
- column: 1,
31182
- 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."
31183
- });
31184
- }
31185
- return issues;
31186
- }
31187
- });
31188
- }
31189
- });
31190
-
31191
- // src/rules/java/verbose-javadoc.ts
31192
- var TAG_REGEX, DEFAULT_TAG_THRESHOLD, DEFAULT_BODY_LENGTH_CAP, javaVerboseJavadocRule;
31193
- var init_verbose_javadoc = __esm({
31194
- "src/rules/java/verbose-javadoc.ts"() {
31195
- "use strict";
31196
- init_rule();
31197
- TAG_REGEX = /@(param|return|throws)\b/g;
31198
- DEFAULT_TAG_THRESHOLD = 3;
31199
- DEFAULT_BODY_LENGTH_CAP = 5;
31200
- javaVerboseJavadocRule = createRule({
31201
- id: "java/verbose-javadoc",
31202
- category: "typo",
31203
- severity: "low",
31204
- aiSpecific: true,
31205
- description: "Excessive Javadoc tags on trivial methods \u2014 over-documentation is an AI fingerprint",
31206
- create(_context) {
31207
- return {
31208
- tagThreshold: DEFAULT_TAG_THRESHOLD,
31209
- bodyLengthCap: DEFAULT_BODY_LENGTH_CAP
31210
- };
31211
- },
31212
- analyze(context, facts) {
31213
- const issues = [];
31214
- const source = facts.v2?._source;
31215
- if (!source) return issues;
31216
- if (!/\.java$/i.test(facts.filePath)) return issues;
31217
- TAG_REGEX.lastIndex = 0;
31218
- let tagCount = 0;
31219
- let firstTagLine = 0;
31220
- let m;
31221
- while ((m = TAG_REGEX.exec(source)) !== null) {
31222
- tagCount++;
31223
- if (firstTagLine === 0) {
31224
- firstTagLine = source.slice(0, m.index).split("\n").length;
31225
- }
31226
- }
31227
- if (tagCount < context.tagThreshold) return issues;
31228
- const lineCount = source.split("\n").length;
31229
- const tagDensity = tagCount / Math.max(lineCount, 1);
31230
- if (tagDensity < 0.05) return issues;
31231
- if (lineCount > 200) return issues;
31232
- issues.push({
31233
- ruleId: "java/verbose-javadoc",
31234
- category: "typo",
31235
- severity: "low",
31236
- aiSpecific: true,
31237
- message: `${tagCount} Javadoc tags in ${lineCount} lines (density ${(tagDensity * 100).toFixed(1)}%) \u2014 likely over-documented`,
31238
- line: firstTagLine,
31239
- column: 1,
31240
- 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."
31241
- });
31242
- return issues;
31243
- }
31244
- });
31245
- }
31246
- });
31247
-
31248
30665
  // src/rules/kotlin/coroutine-global-scope.ts
31249
30666
  var GLOBAL_SCOPE_REGEX, kotlinCoroutineGlobalScopeRule;
31250
30667
  var init_coroutine_global_scope = __esm({
@@ -31265,7 +30682,7 @@ var init_coroutine_global_scope = __esm({
31265
30682
  const issues = [];
31266
30683
  const source = facts.v2?._source;
31267
30684
  if (!source) return issues;
31268
- if (!/\.kt$/i.test(facts.filePath)) return issues;
30685
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31269
30686
  let m;
31270
30687
  GLOBAL_SCOPE_REGEX.lastIndex = 0;
31271
30688
  while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
@@ -31309,7 +30726,7 @@ var init_data_class_defaults_overuse = __esm({
31309
30726
  const issues = [];
31310
30727
  const source = facts.v2?._source;
31311
30728
  if (!source) return issues;
31312
- if (!/\.kt$/i.test(facts.filePath)) return issues;
30729
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31313
30730
  let m;
31314
30731
  DATA_CLASS_HEAD_REGEX.lastIndex = 0;
31315
30732
  while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
@@ -31371,7 +30788,7 @@ var init_object_singleton_misuse = __esm({
31371
30788
  const issues = [];
31372
30789
  const source = facts.v2?._source;
31373
30790
  if (!source) return issues;
31374
- if (!/\.kt$/i.test(facts.filePath)) return issues;
30791
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31375
30792
  let m;
31376
30793
  OBJECT_DECL_REGEX.lastIndex = 0;
31377
30794
  while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
@@ -31409,13 +30826,13 @@ var init_object_singleton_misuse = __esm({
31409
30826
  });
31410
30827
 
31411
30828
  // src/rules/kotlin/println-debug.ts
31412
- var PRINTLN_REGEX2, DEFAULT_THRESHOLD3, kotlinPrintlnDebugRule;
30829
+ var PRINTLN_REGEX, DEFAULT_THRESHOLD, kotlinPrintlnDebugRule;
31413
30830
  var init_println_debug = __esm({
31414
30831
  "src/rules/kotlin/println-debug.ts"() {
31415
30832
  "use strict";
31416
30833
  init_rule();
31417
- PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
31418
- DEFAULT_THRESHOLD3 = 1;
30834
+ PRINTLN_REGEX = /^\s*println\s*\(/gm;
30835
+ DEFAULT_THRESHOLD = 1;
31419
30836
  kotlinPrintlnDebugRule = createRule({
31420
30837
  id: "kotlin/println-debug",
31421
30838
  category: "typo",
@@ -31423,17 +30840,17 @@ var init_println_debug = __esm({
31423
30840
  aiSpecific: true,
31424
30841
  description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
31425
30842
  create(_context) {
31426
- return { threshold: DEFAULT_THRESHOLD3 };
30843
+ return { threshold: DEFAULT_THRESHOLD };
31427
30844
  },
31428
30845
  analyze(context, facts) {
31429
30846
  const issues = [];
31430
30847
  const source = facts.v2?._source;
31431
30848
  if (!source) return issues;
31432
- if (!/\.kt$/i.test(facts.filePath)) return issues;
30849
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31433
30850
  const matches = [];
31434
30851
  let m;
31435
- PRINTLN_REGEX2.lastIndex = 0;
31436
- while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
30852
+ PRINTLN_REGEX.lastIndex = 0;
30853
+ while ((m = PRINTLN_REGEX.exec(source)) !== null) {
31437
30854
  matches.push(m.index);
31438
30855
  }
31439
30856
  if (matches.length <= context.threshold) return issues;
@@ -31459,12 +30876,12 @@ var init_println_debug = __esm({
31459
30876
  });
31460
30877
 
31461
30878
  // src/rules/kotlin/string-concat-loop.ts
31462
- var STRING_CONCAT_REGEX2, kotlinStringConcatLoopRule;
31463
- var init_string_concat_loop2 = __esm({
30879
+ var STRING_CONCAT_REGEX, kotlinStringConcatLoopRule;
30880
+ var init_string_concat_loop = __esm({
31464
30881
  "src/rules/kotlin/string-concat-loop.ts"() {
31465
30882
  "use strict";
31466
30883
  init_rule();
31467
- STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
30884
+ STRING_CONCAT_REGEX = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
31468
30885
  kotlinStringConcatLoopRule = createRule({
31469
30886
  id: "kotlin/string-concat-loop",
31470
30887
  category: "perf",
@@ -31478,11 +30895,11 @@ var init_string_concat_loop2 = __esm({
31478
30895
  const issues = [];
31479
30896
  const source = facts.v2?._source;
31480
30897
  if (!source) return issues;
31481
- if (!/\.kt$/i.test(facts.filePath)) return issues;
30898
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31482
30899
  if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
31483
30900
  let m;
31484
- STRING_CONCAT_REGEX2.lastIndex = 0;
31485
- while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
30901
+ STRING_CONCAT_REGEX.lastIndex = 0;
30902
+ while ((m = STRING_CONCAT_REGEX.exec(source)) !== null) {
31486
30903
  const line = source.slice(0, m.index).split("\n").length;
31487
30904
  const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
31488
30905
  if (/\.append\s*\(/.test(lineText)) continue;
@@ -36700,6 +36117,15 @@ function parseSource(source, filePath) {
36700
36117
  case "go":
36701
36118
  case "rs":
36702
36119
  case "java":
36120
+ // v0.28.0: Kotlin files get the same parseBlankModule path as
36121
+ // Java (v0.24.5). All 5 `kotlin/*` rules are regex-based and
36122
+ // gate themselves on `/\.kts?$/i.test(filePath)` inside their
36123
+ // `analyze()`. The tree-sitter Kotlin integration is a larger
36124
+ // lift; the v0.27.0 methodology paper confirmed era-confounding
36125
+ // is the dominant signal anyway, so a regex-only Kotlin pass is
36126
+ // sufficient for the v9 calibration goal.
36127
+ case "kt":
36128
+ case "kts":
36703
36129
  return parseBlankModule(source);
36704
36130
  default:
36705
36131
  return parseWithSwc(source, filePath);
@@ -41128,13 +40554,13 @@ var init_implicitly_unwrapped_optional = __esm({
41128
40554
  });
41129
40555
 
41130
40556
  // src/rules/swift/print-debug.ts
41131
- var PRINT_REGEX, DEFAULT_THRESHOLD4, swiftPrintDebugRule;
40557
+ var PRINT_REGEX, DEFAULT_THRESHOLD2, swiftPrintDebugRule;
41132
40558
  var init_print_debug = __esm({
41133
40559
  "src/rules/swift/print-debug.ts"() {
41134
40560
  "use strict";
41135
40561
  init_rule();
41136
40562
  PRINT_REGEX = /\bprint\s*\(/g;
41137
- DEFAULT_THRESHOLD4 = 1;
40563
+ DEFAULT_THRESHOLD2 = 1;
41138
40564
  swiftPrintDebugRule = createRule({
41139
40565
  id: "swift/print-debug",
41140
40566
  category: "typo",
@@ -41142,7 +40568,7 @@ var init_print_debug = __esm({
41142
40568
  aiSpecific: true,
41143
40569
  description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
41144
40570
  create(_context) {
41145
- return { threshold: DEFAULT_THRESHOLD4 };
40571
+ return { threshold: DEFAULT_THRESHOLD2 };
41146
40572
  },
41147
40573
  analyze(context, facts) {
41148
40574
  const issues = [];
@@ -46892,22 +46318,11 @@ var init_builtins = __esm({
46892
46318
  init_error_wrap_without_context();
46893
46319
  init_nil_slice_vs_empty();
46894
46320
  init_struct_tag_inconsistency();
46895
- init_arraylist_vs_linkedlist();
46896
- init_builder_overuse();
46897
- init_empty_catch_block();
46898
- init_immutable_collection_preference();
46899
- init_legacy_date_api();
46900
- init_optional_overuse();
46901
- init_raw_type_overuse();
46902
- init_stream_overuse();
46903
- init_string_concat_loop();
46904
- init_system_out_println();
46905
- init_verbose_javadoc();
46906
46321
  init_coroutine_global_scope();
46907
46322
  init_data_class_defaults_overuse();
46908
46323
  init_object_singleton_misuse();
46909
46324
  init_println_debug();
46910
- init_string_concat_loop2();
46325
+ init_string_concat_loop();
46911
46326
  init_gap_monopoly();
46912
46327
  init_math_element_uniformity();
46913
46328
  init_math_grid_uniformity();
@@ -47032,17 +46447,6 @@ var init_builtins = __esm({
47032
46447
  goErrorWrapWithoutContextRule,
47033
46448
  goNilSliceVsEmptyRule,
47034
46449
  goStructTagInconsistencyRule,
47035
- javaArraylistVsLinkedlistRule,
47036
- javaBuilderOveruseRule,
47037
- javaEmptyCatchBlockRule,
47038
- javaImmutableCollectionPreferenceRule,
47039
- javaLegacyDateApiRule,
47040
- javaOptionalOveruseRule,
47041
- javaRawTypeOveruseRule,
47042
- javaStreamOveruseRule,
47043
- javaStringConcatLoopRule,
47044
- javaSystemOutPrintlnRule,
47045
- javaVerboseJavadocRule,
47046
46450
  kotlinCoroutineGlobalScopeRule,
47047
46451
  kotlinDataClassDefaultsOveruseRule,
47048
46452
  kotlinObjectSingletonMisuseRule,
@@ -50212,224 +49616,6 @@ var init_signal_strength = __esm({
50212
49616
  _v8Lift: 1,
50213
49617
  defaultOff: true
50214
49618
  },
50215
- "java/arraylist-vs-linkedlist": {
50216
- recall: 35e-4,
50217
- fpRate: 94e-4,
50218
- ratio: 0.37,
50219
- precision: 0.0446,
50220
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50221
- verdict: "DORMANT",
50222
- _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).",
50223
- aiSpecific: true,
50224
- _v7Verdict: "DORMANT",
50225
- _v7Lift: 1,
50226
- _v7Recall: 0,
50227
- _v7FpRate: 0,
50228
- _v7Precision: 0,
50229
- _v8Verdict: "DORMANT",
50230
- _v8Lift: 1,
50231
- _v9Verdict: "DORMANT",
50232
- _v9Lift: 4.74,
50233
- _v9Recall: 35e-4,
50234
- _v9FpRate: 94e-4,
50235
- _v9Precision: 0.0446,
50236
- defaultOff: true
50237
- },
50238
- "java/empty-catch-block": {
50239
- recall: 8e-3,
50240
- fpRate: 0.1118,
50241
- ratio: 0.07,
50242
- precision: 89e-4,
50243
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50244
- verdict: "DORMANT",
50245
- _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.",
50246
- aiSpecific: true,
50247
- _v7Verdict: "DORMANT",
50248
- _v7Lift: 1,
50249
- _v7Recall: 0,
50250
- _v7FpRate: 0,
50251
- _v7Precision: 0,
50252
- _v8Verdict: "DORMANT",
50253
- _v8Lift: 1,
50254
- _v9Verdict: "DORMANT",
50255
- _v9Lift: 0.08,
50256
- _v9Recall: 8e-3,
50257
- _v9FpRate: 0.1118,
50258
- _v9Precision: 89e-4,
50259
- defaultOff: true
50260
- },
50261
- "java/legacy-date-api": {
50262
- recall: 0.0285,
50263
- fpRate: 0.0511,
50264
- ratio: 0.56,
50265
- precision: 0.0657,
50266
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50267
- verdict: "DORMANT",
50268
- _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.",
50269
- aiSpecific: true,
50270
- _v7Verdict: "DORMANT",
50271
- _v7Lift: 1,
50272
- _v7Recall: 0,
50273
- _v7FpRate: 0,
50274
- _v7Precision: 0,
50275
- _v8Verdict: "DORMANT",
50276
- _v8Lift: 1,
50277
- _v9Verdict: "DORMANT",
50278
- _v9Lift: 1.29,
50279
- _v9Recall: 0.0285,
50280
- _v9FpRate: 0.0511,
50281
- _v9Precision: 0.0657,
50282
- defaultOff: true
50283
- },
50284
- "java/verbose-javadoc": {
50285
- recall: 0,
50286
- fpRate: 0,
50287
- ratio: 1,
50288
- precision: 0,
50289
- lastCalibratedAt: "2026-07-03T00:00:00Z",
50290
- verdict: "DORMANT",
50291
- _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.",
50292
- aiSpecific: true,
50293
- _v9Verdict: "DORMANT",
50294
- _v9Lift: 1,
50295
- _v9Recall: 0,
50296
- _v9FpRate: 0,
50297
- _v9Precision: 0,
50298
- defaultOff: true
50299
- },
50300
- "java/optional-overuse": {
50301
- recall: 0,
50302
- fpRate: 0,
50303
- ratio: 1,
50304
- precision: 0,
50305
- lastCalibratedAt: "2026-07-03T00:00:00Z",
50306
- verdict: "DORMANT",
50307
- _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.",
50308
- aiSpecific: true,
50309
- _v9Verdict: "DORMANT",
50310
- _v9Lift: 1,
50311
- _v9Recall: 0,
50312
- _v9FpRate: 0,
50313
- _v9Precision: 0,
50314
- defaultOff: true
50315
- },
50316
- "java/immutable-collection-preference": {
50317
- recall: 0,
50318
- fpRate: 0,
50319
- ratio: 1,
50320
- precision: 0,
50321
- lastCalibratedAt: "2026-07-03T00:00:00Z",
50322
- verdict: "DORMANT",
50323
- _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.",
50324
- aiSpecific: true,
50325
- _v9Verdict: "DORMANT",
50326
- _v9Lift: 1,
50327
- _v9Recall: 0,
50328
- _v9FpRate: 0,
50329
- _v9Precision: 0,
50330
- defaultOff: true
50331
- },
50332
- "java/builder-overuse": {
50333
- recall: 0,
50334
- fpRate: 0,
50335
- ratio: 1,
50336
- precision: 0,
50337
- lastCalibratedAt: "2026-07-03T00:00:00Z",
50338
- verdict: "DORMANT",
50339
- _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.",
50340
- aiSpecific: true,
50341
- _v9Verdict: "DORMANT",
50342
- _v9Lift: 1,
50343
- _v9Recall: 0,
50344
- _v9FpRate: 0,
50345
- _v9Precision: 0,
50346
- defaultOff: true
50347
- },
50348
- "java/stream-overuse": {
50349
- recall: 0,
50350
- fpRate: 0,
50351
- ratio: 1,
50352
- precision: 0,
50353
- lastCalibratedAt: "2026-07-03T00:00:00Z",
50354
- verdict: "DORMANT",
50355
- _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.",
50356
- aiSpecific: true,
50357
- _v9Verdict: "DORMANT",
50358
- _v9Lift: 1,
50359
- _v9Recall: 0,
50360
- _v9FpRate: 0,
50361
- _v9Precision: 0,
50362
- defaultOff: true
50363
- },
50364
- "java/raw-type-overuse": {
50365
- recall: 0.1093,
50366
- fpRate: 0.2041,
50367
- ratio: 0.54,
50368
- precision: 0.0632,
50369
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50370
- verdict: "DORMANT",
50371
- _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.",
50372
- aiSpecific: true,
50373
- _v7Verdict: "DORMANT",
50374
- _v7Lift: 1,
50375
- _v7Recall: 0,
50376
- _v7FpRate: 0,
50377
- _v7Precision: 0,
50378
- _v8Verdict: "DORMANT",
50379
- _v8Lift: 1,
50380
- _v9Verdict: "DORMANT",
50381
- _v9Lift: 0.31,
50382
- _v9Recall: 0.1093,
50383
- _v9FpRate: 0.2041,
50384
- _v9Precision: 0.0632,
50385
- defaultOff: true
50386
- },
50387
- "java/string-concat-loop": {
50388
- recall: 46e-4,
50389
- fpRate: 0.0358,
50390
- ratio: 0.13,
50391
- precision: 0.0158,
50392
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50393
- verdict: "DORMANT",
50394
- _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.",
50395
- aiSpecific: true,
50396
- _v7Verdict: "DORMANT",
50397
- _v7Lift: 1,
50398
- _v7Recall: 0,
50399
- _v7FpRate: 0,
50400
- _v7Precision: 0,
50401
- _v8Verdict: "DORMANT",
50402
- _v8Lift: 1,
50403
- _v9Verdict: "DORMANT",
50404
- _v9Lift: 0.44,
50405
- _v9Recall: 46e-4,
50406
- _v9FpRate: 0.0358,
50407
- _v9Precision: 0.0158,
50408
- defaultOff: true
50409
- },
50410
- "java/system-out-println": {
50411
- recall: 0.1503,
50412
- fpRate: 0.2533,
50413
- ratio: 0.59,
50414
- precision: 0.0695,
50415
- lastCalibratedAt: "2026-07-02T19:34:00Z",
50416
- verdict: "DORMANT",
50417
- _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.",
50418
- aiSpecific: true,
50419
- _v7Verdict: "DORMANT",
50420
- _v7Lift: 1,
50421
- _v7Recall: 0,
50422
- _v7FpRate: 0,
50423
- _v7Precision: 0,
50424
- _v8Verdict: "DORMANT",
50425
- _v8Lift: 1,
50426
- _v9Verdict: "DORMANT",
50427
- _v9Lift: 0.27,
50428
- _v9Recall: 0.1503,
50429
- _v9FpRate: 0.2533,
50430
- _v9Precision: 0.0695,
50431
- defaultOff: true
50432
- },
50433
49619
  "layout/forced-layout": {
50434
49620
  recall: 0,
50435
49621
  fpRate: 0,
@@ -51674,13 +50860,13 @@ var init_signal_strength = __esm({
51674
50860
  defaultOff: true
51675
50861
  },
51676
50862
  "kotlin/data-class-defaults-overuse": {
51677
- recall: 0,
51678
- fpRate: 0,
51679
- ratio: 1,
51680
- precision: 0,
51681
- lastCalibratedAt: "2026-07-02T00:00:00Z",
50863
+ recall: 469e-5,
50864
+ fpRate: 222e-5,
50865
+ ratio: 2.11,
50866
+ precision: 0.1429,
50867
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51682
50868
  verdict: "DORMANT",
51683
- _calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
50869
+ _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.",
51684
50870
  aiSpecific: true,
51685
50871
  _v7Verdict: "DORMANT",
51686
50872
  _v7Lift: 1,
@@ -51689,16 +50875,21 @@ var init_signal_strength = __esm({
51689
50875
  _v7Precision: 0,
51690
50876
  _v8Verdict: "DORMANT",
51691
50877
  _v8Lift: 1,
50878
+ _v9Verdict: "DORMANT",
50879
+ _v9Lift: 2.11,
50880
+ _v9Recall: 469e-5,
50881
+ _v9FpRate: 222e-5,
50882
+ _v9Precision: 0.1429,
51692
50883
  defaultOff: true
51693
50884
  },
51694
50885
  "kotlin/coroutine-global-scope": {
51695
- recall: 0,
51696
- fpRate: 0,
51697
- ratio: 1,
51698
- precision: 0,
51699
- lastCalibratedAt: "2026-07-02T00:00:00Z",
50886
+ recall: 469e-5,
50887
+ fpRate: 0.05004,
50888
+ ratio: 0.09,
50889
+ precision: 735e-5,
50890
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51700
50891
  verdict: "DORMANT",
51701
- _calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
50892
+ _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.",
51702
50893
  aiSpecific: true,
51703
50894
  _v7Verdict: "DORMANT",
51704
50895
  _v7Lift: 1,
@@ -51707,16 +50898,21 @@ var init_signal_strength = __esm({
51707
50898
  _v7Precision: 0,
51708
50899
  _v8Verdict: "DORMANT",
51709
50900
  _v8Lift: 1,
50901
+ _v9Verdict: "DORMANT",
50902
+ _v9Lift: 0.09,
50903
+ _v9Recall: 469e-5,
50904
+ _v9FpRate: 0.05004,
50905
+ _v9Precision: 735e-5,
51710
50906
  defaultOff: true
51711
50907
  },
51712
50908
  "kotlin/println-debug": {
51713
- recall: 0,
51714
- fpRate: 0,
51715
- ratio: 1,
51716
- precision: 0,
51717
- lastCalibratedAt: "2026-07-02T00:00:00Z",
50909
+ recall: 0.07042,
50910
+ fpRate: 0.11379,
50911
+ ratio: 0.62,
50912
+ precision: 0.04659,
50913
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51718
50914
  verdict: "DORMANT",
51719
- _calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
50915
+ _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.",
51720
50916
  aiSpecific: true,
51721
50917
  _v7Verdict: "DORMANT",
51722
50918
  _v7Lift: 1,
@@ -51725,16 +50921,21 @@ var init_signal_strength = __esm({
51725
50921
  _v7Precision: 0,
51726
50922
  _v8Verdict: "DORMANT",
51727
50923
  _v8Lift: 1,
50924
+ _v9Verdict: "DORMANT",
50925
+ _v9Lift: 0.62,
50926
+ _v9Recall: 0.07042,
50927
+ _v9FpRate: 0.11379,
50928
+ _v9Precision: 0.04659,
51728
50929
  defaultOff: true
51729
50930
  },
51730
50931
  "kotlin/object-singleton-misuse": {
51731
- recall: 0,
51732
- fpRate: 0,
51733
- ratio: 1,
51734
- precision: 0,
51735
- lastCalibratedAt: "2026-07-02T00:00:00Z",
50932
+ recall: 469e-5,
50933
+ fpRate: 371e-5,
50934
+ ratio: 1.27,
50935
+ precision: 0.09091,
50936
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51736
50937
  verdict: "DORMANT",
51737
- _calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
50938
+ _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).",
51738
50939
  aiSpecific: true,
51739
50940
  _v7Verdict: "DORMANT",
51740
50941
  _v7Lift: 1,
@@ -51743,16 +50944,21 @@ var init_signal_strength = __esm({
51743
50944
  _v7Precision: 0,
51744
50945
  _v8Verdict: "DORMANT",
51745
50946
  _v8Lift: 1,
50947
+ _v9Verdict: "DORMANT",
50948
+ _v9Lift: 1.27,
50949
+ _v9Recall: 469e-5,
50950
+ _v9FpRate: 371e-5,
50951
+ _v9Precision: 0.09091,
51746
50952
  defaultOff: true
51747
50953
  },
51748
50954
  "kotlin/string-concat-loop": {
51749
- recall: 0,
51750
- fpRate: 0,
51751
- ratio: 1,
51752
- precision: 0,
51753
- lastCalibratedAt: "2026-07-02T00:00:00Z",
50955
+ recall: 939e-5,
50956
+ fpRate: 519e-5,
50957
+ ratio: 1.81,
50958
+ precision: 0.125,
50959
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51754
50960
  verdict: "DORMANT",
51755
- _calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
50961
+ _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.",
51756
50962
  aiSpecific: true,
51757
50963
  _v7Verdict: "DORMANT",
51758
50964
  _v7Lift: 1,
@@ -51761,6 +50967,11 @@ var init_signal_strength = __esm({
51761
50967
  _v7Precision: 0,
51762
50968
  _v8Verdict: "DORMANT",
51763
50969
  _v8Lift: 1,
50970
+ _v9Verdict: "DORMANT",
50971
+ _v9Lift: 1.81,
50972
+ _v9Recall: 939e-5,
50973
+ _v9FpRate: 519e-5,
50974
+ _v9Precision: 0.125,
51764
50975
  defaultOff: true
51765
50976
  },
51766
50977
  "swift/force-unwrap": {
@@ -52021,8 +51232,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
52021
51232
  const ext = (0, import_node_path12.extname)(filePath).toLowerCase();
52022
51233
  const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
52023
51234
  ".swift",
52024
- ".kt",
52025
- ".kts",
52026
51235
  ".dart",
52027
51236
  ".cpp",
52028
51237
  ".cc",
@@ -59971,18 +59180,6 @@ var RULE_HINTS = {
59971
59180
  "dup/near-duplicate": "Refactor to a shared helper. Type-2 near-duplicate: same identifiers + structure, different whitespace/comments. AI-iterated code regenerates the same function with cosmetic edits. (v0.23.0 \u2014 MinHash + LSH, k=5, thr 0.7.)",
59972
59181
  // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
59973
59182
  "dup/structural-clone": "Refactor to a shared helper. Type-3 clone: same shape as another file after identifier canonicalization, with renames and/or added/removed statements. AI agents and copy-paste both produce this. Extract the shared part. (v0.24.0.)",
59974
- // v0.20.0 — Java-specific rules
59975
- "java/system-out-println": "Replace with `private static final Logger log = LoggerFactory.getLogger(...);` then `log.info(...)`. AI agents default to println because of textbook examples; real Java code uses a logger.",
59976
- "java/empty-catch-block": 'Log the exception (`log.error("...", e)`), re-throw it, or both. Empty catch blocks hide bugs. Common in AI-generated code that wants to look defensive.',
59977
- "java/arraylist-vs-linkedlist": "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 recommends ArrayList unless you need a Deque.",
59978
- "java/legacy-date-api": "Replace `java.util.Date` / `Calendar` with `java.time` (`LocalDate`, `LocalDateTime`, `Instant`, `ZonedDateTime`). java.time is immutable and thread-safe. AI agents default to the legacy API.",
59979
- "java/raw-type-overuse": "Replace raw `List` / `Map` / `Set` with the parameterized form. Raw types disable generic type checking (Effective Java, Item 23). AI agents default to raw types when unsure of the correct generic parameters.",
59980
- "java/string-concat-loop": "Declare a `StringBuilder` outside the loop: `StringBuilder sb = new StringBuilder(); sb.append(...);` then `return sb.toString();` after. String concat in a loop is O(n\xB2).",
59981
- "java/verbose-javadoc": "Skip Javadoc on trivial methods (getters, setters, builders). AI over-documents; real Java limits Javadoc to public API surface. (v0.26.0 \u2014 DORMANT.)",
59982
- "java/optional-overuse": "Use Objects.requireNonNull(x) for null checks; reserve Optional for return values. AI chains .ofNullable().orElseThrow() where null checks suffice. (v0.26.0 \u2014 DORMANT.)",
59983
- "java/immutable-collection-preference": "Prefer mutable (new ArrayList<>()) when the collection will be modified. AI defaults to List.of/Map.of/Set.of; real Java uses mutable. (v0.26.0 \u2014 DORMANT.)",
59984
- "java/builder-overuse": "Use a plain constructor for classes with \u2264 3 fields. AI defaults to @Builder; Builder adds Lombok dep and a Builder inner class per class. (v0.26.0 \u2014 DORMANT.)",
59985
- "java/stream-overuse": "Prefer a for-loop for simple transformations. AI chains Stream API for everything; for small collections a for-loop is faster. (v0.26.0 \u2014 DORMANT.)",
59986
59183
  // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
59987
59184
  "kotlin/data-class-defaults-overuse": "Audit which fields are genuinely optional. Real Kotlin uses defaults sparingly (1-2 callbacks, the rest required). v0.24.0 \u2014 DORMANT until v9 calibration.",
59988
59185
  "kotlin/coroutine-global-scope": "Replace with `viewModelScope.launch { ... }`, `lifecycleScope.launch { ... }`, or `coroutineScope { ... }`. GlobalScope bypasses cancellation. (v0.24.0 \u2014 DORMANT.)",