slopbrick 0.28.0 → 0.29.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.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.28.0";
22
+ VERSION = "0.29.0";
23
23
  }
24
24
  });
25
25
 
@@ -30749,6 +30749,115 @@ var init_data_class_defaults_overuse = __esm({
30749
30749
  }
30750
30750
  });
30751
30751
 
30752
+ // src/rules/kotlin/force-unwrap.ts
30753
+ var FORCE_UNWRAP_REGEX, kotlinForceUnwrapRule;
30754
+ var init_force_unwrap = __esm({
30755
+ "src/rules/kotlin/force-unwrap.ts"() {
30756
+ "use strict";
30757
+ init_rule();
30758
+ FORCE_UNWRAP_REGEX = /!!(?=\s*[.\)}\};,\n\[])/g;
30759
+ kotlinForceUnwrapRule = createRule({
30760
+ id: "kotlin/force-unwrap",
30761
+ category: "logic",
30762
+ severity: "medium",
30763
+ aiSpecific: false,
30764
+ description: "!! force-unwrap \u2014 use ?. (safe call) or a proper null check",
30765
+ create(_context) {
30766
+ return {};
30767
+ },
30768
+ analyze(_context, facts) {
30769
+ const issues = [];
30770
+ const source = facts.v2?._source;
30771
+ if (!source) return issues;
30772
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
30773
+ let m;
30774
+ FORCE_UNWRAP_REGEX.lastIndex = 0;
30775
+ while ((m = FORCE_UNWRAP_REGEX.exec(source)) !== null) {
30776
+ const line = source.slice(0, m.index).split("\n").length;
30777
+ const lineStart = source.lastIndexOf("\n", m.index) + 1;
30778
+ const lineText = source.slice(lineStart, m.index);
30779
+ const quoteCount = (lineText.match(/"/g) || []).length;
30780
+ if (quoteCount % 2 === 1) continue;
30781
+ issues.push({
30782
+ ruleId: "kotlin/force-unwrap",
30783
+ category: "logic",
30784
+ severity: "medium",
30785
+ aiSpecific: false,
30786
+ message: `!! force-unwrap at line ${line}`,
30787
+ line,
30788
+ column: m.index - lineStart + 1,
30789
+ advice: "Use ?. (safe call) with ?: (Elvis) for a default, or a proper when/if check. !! throws NullPointerException at runtime \u2014 it bypasses Kotlin's type system. Reference: kotlin/force-unwrap v0.29."
30790
+ });
30791
+ }
30792
+ return issues;
30793
+ }
30794
+ });
30795
+ }
30796
+ });
30797
+
30798
+ // src/rules/kotlin/hardcoded-credential.ts
30799
+ var KEY_REGEX, VALUE_REGEX, FALSE_POSITIVES, kotlinHardcodedCredentialRule;
30800
+ var init_hardcoded_credential = __esm({
30801
+ "src/rules/kotlin/hardcoded-credential.ts"() {
30802
+ "use strict";
30803
+ init_rule();
30804
+ KEY_REGEX = /\b(api[_-]?key|apikey|secret|token|password|auth|access[_-]?key|client[_-]?secret|private[_-]?key)\b\s*[=:]/i;
30805
+ VALUE_REGEX = /["']([A-Za-z0-9_\-+/=.@*!]{16,})["']/;
30806
+ FALSE_POSITIVES = /* @__PURE__ */ new Set([
30807
+ "passwordless",
30808
+ "tokenize",
30809
+ "tokenizer",
30810
+ "authorizationrequired",
30811
+ "authenticated",
30812
+ "authenticatortoken",
30813
+ "authtoken",
30814
+ "placeholder",
30815
+ // common in test fixtures
30816
+ "changeme"
30817
+ // placeholder, but still suspicious — kept for review
30818
+ ]);
30819
+ kotlinHardcodedCredentialRule = createRule({
30820
+ id: "kotlin/hardcoded-credential",
30821
+ category: "security",
30822
+ severity: "high",
30823
+ aiSpecific: false,
30824
+ description: "Hardcoded credential literal \u2014 use env vars or a secrets manager",
30825
+ create(_context) {
30826
+ return {};
30827
+ },
30828
+ analyze(_context, facts) {
30829
+ const issues = [];
30830
+ const source = facts.v2?._source;
30831
+ if (!source) return issues;
30832
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
30833
+ const lines = source.split("\n");
30834
+ for (let i = 0; i < lines.length; i++) {
30835
+ const line = lines[i];
30836
+ if (!KEY_REGEX.test(line)) continue;
30837
+ const valueMatch = VALUE_REGEX.exec(line);
30838
+ if (!valueMatch) continue;
30839
+ const value = valueMatch[1];
30840
+ if (FALSE_POSITIVES.has(value.toLowerCase())) continue;
30841
+ if (!/[a-zA-Z]/.test(value) || !/[0-9]/.test(value)) continue;
30842
+ if (value.startsWith("$")) continue;
30843
+ if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) continue;
30844
+ issues.push({
30845
+ ruleId: "kotlin/hardcoded-credential",
30846
+ category: "security",
30847
+ severity: "high",
30848
+ aiSpecific: false,
30849
+ message: `Hardcoded credential at line ${i + 1}`,
30850
+ line: i + 1,
30851
+ column: 1,
30852
+ advice: "Move the credential to an environment variable, a .env file that is .gitignore'd, or a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager). Hardcoded credentials are the #1 source of secret leaks on GitHub. Reference: kotlin/hardcoded-credential v0.29 (OWASP A07:2021 \u2014 Identification and Authentication Failures)."
30853
+ });
30854
+ }
30855
+ return issues;
30856
+ }
30857
+ });
30858
+ }
30859
+ });
30860
+
30752
30861
  // src/rules/kotlin/object-singleton-misuse.ts
30753
30862
  var OBJECT_DECL_REGEX, kotlinObjectSingletonMisuseRule;
30754
30863
  var init_object_singleton_misuse = __esm({
@@ -30806,13 +30915,58 @@ var init_object_singleton_misuse = __esm({
30806
30915
  }
30807
30916
  });
30808
30917
 
30918
+ // src/rules/kotlin/println-as-log.ts
30919
+ var PRINTLN_REGEX, REAL_LOGGING_IMPORT_REGEX, kotlinPrintlnAsLogRule;
30920
+ var init_println_as_log = __esm({
30921
+ "src/rules/kotlin/println-as-log.ts"() {
30922
+ "use strict";
30923
+ init_rule();
30924
+ PRINTLN_REGEX = /\bprintln\s*\(/g;
30925
+ REAL_LOGGING_IMPORT_REGEX = /\bimport\s+(?:android\.util\.Log|org\.slf4j\.|io\.github\.oshai\.kotlinlogging|kotlin\.logging|co\.touchlab\.kermit|com\.github\.ajalt\.timber|org\.apache\.logging\.log4j)/;
30926
+ kotlinPrintlnAsLogRule = createRule({
30927
+ id: "kotlin/println-as-log",
30928
+ category: "logic",
30929
+ severity: "low",
30930
+ aiSpecific: false,
30931
+ description: "println() used for logging \u2014 use slf4j, kermit, or android.util.Log",
30932
+ create(_context) {
30933
+ return {};
30934
+ },
30935
+ analyze(_context, facts) {
30936
+ const issues = [];
30937
+ const source = facts.v2?._source;
30938
+ if (!source) return issues;
30939
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
30940
+ if (/\/test\//i.test(facts.filePath) || /\.test\.kts?$/i.test(facts.filePath)) return issues;
30941
+ if (REAL_LOGGING_IMPORT_REGEX.test(source)) return issues;
30942
+ let m;
30943
+ PRINTLN_REGEX.lastIndex = 0;
30944
+ while ((m = PRINTLN_REGEX.exec(source)) !== null) {
30945
+ const line = source.slice(0, m.index).split("\n").length;
30946
+ issues.push({
30947
+ ruleId: "kotlin/println-as-log",
30948
+ category: "logic",
30949
+ severity: "low",
30950
+ aiSpecific: false,
30951
+ message: `println() as logger at line ${line}`,
30952
+ line,
30953
+ column: 1,
30954
+ advice: "Use a real logging library: slf4j (JVM), android.util.Log (Android), Timber (Android), kermit (multiplatform), or kotlin-logging. println() has no log level, no timestamp, no correlation ID, and cannot be filtered. Reference: kotlin/println-as-log v0.29."
30955
+ });
30956
+ }
30957
+ return issues;
30958
+ }
30959
+ });
30960
+ }
30961
+ });
30962
+
30809
30963
  // src/rules/kotlin/println-debug.ts
30810
- var PRINTLN_REGEX, DEFAULT_THRESHOLD, kotlinPrintlnDebugRule;
30964
+ var PRINTLN_REGEX2, DEFAULT_THRESHOLD, kotlinPrintlnDebugRule;
30811
30965
  var init_println_debug = __esm({
30812
30966
  "src/rules/kotlin/println-debug.ts"() {
30813
30967
  "use strict";
30814
30968
  init_rule();
30815
- PRINTLN_REGEX = /^\s*println\s*\(/gm;
30969
+ PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
30816
30970
  DEFAULT_THRESHOLD = 1;
30817
30971
  kotlinPrintlnDebugRule = createRule({
30818
30972
  id: "kotlin/println-debug",
@@ -30830,8 +30984,8 @@ var init_println_debug = __esm({
30830
30984
  if (!/\.kts?$/i.test(facts.filePath)) return issues;
30831
30985
  const matches = [];
30832
30986
  let m;
30833
- PRINTLN_REGEX.lastIndex = 0;
30834
- while ((m = PRINTLN_REGEX.exec(source)) !== null) {
30987
+ PRINTLN_REGEX2.lastIndex = 0;
30988
+ while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
30835
30989
  matches.push(m.index);
30836
30990
  }
30837
30991
  if (matches.length <= context.threshold) return issues;
@@ -30856,6 +31010,95 @@ var init_println_debug = __esm({
30856
31010
  }
30857
31011
  });
30858
31012
 
31013
+ // src/rules/kotlin/runblocking-misuse.ts
31014
+ var RUN_BLOCKING_REGEX, kotlinRunBlockingMisuseRule;
31015
+ var init_runblocking_misuse = __esm({
31016
+ "src/rules/kotlin/runblocking-misuse.ts"() {
31017
+ "use strict";
31018
+ init_rule();
31019
+ RUN_BLOCKING_REGEX = /\brunBlocking\s*[({]/g;
31020
+ kotlinRunBlockingMisuseRule = createRule({
31021
+ id: "kotlin/runblocking-misuse",
31022
+ category: "perf",
31023
+ severity: "medium",
31024
+ aiSpecific: false,
31025
+ description: "runBlocking { ... } \u2014 blocks the calling thread; use coroutineScope {} or call suspend fun directly",
31026
+ create(_context) {
31027
+ return {};
31028
+ },
31029
+ analyze(_context, facts) {
31030
+ const issues = [];
31031
+ const source = facts.v2?._source;
31032
+ if (!source) return issues;
31033
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31034
+ if (/\bfun\s+main\s*\(/.test(source)) return issues;
31035
+ let m;
31036
+ RUN_BLOCKING_REGEX.lastIndex = 0;
31037
+ while ((m = RUN_BLOCKING_REGEX.exec(source)) !== null) {
31038
+ const line = source.slice(0, m.index).split("\n").length;
31039
+ issues.push({
31040
+ ruleId: "kotlin/runblocking-misuse",
31041
+ category: "perf",
31042
+ severity: "medium",
31043
+ aiSpecific: false,
31044
+ message: `runBlocking { ... } at line ${line}`,
31045
+ line,
31046
+ column: 1,
31047
+ advice: 'runBlocking blocks the calling thread, defeating the purpose of coroutines. Use coroutineScope { } for structured concurrency, or call the suspend function directly. The Kotlin coroutines docs: runBlocking "should rarely (if ever) be used outside of main()". Reference: kotlin/runblocking-misuse v0.29.'
31048
+ });
31049
+ }
31050
+ return issues;
31051
+ }
31052
+ });
31053
+ }
31054
+ });
31055
+
31056
+ // src/rules/kotlin/sql-string-concat.ts
31057
+ var SQL_KEYWORD_REGEX, UNSAFE_REGEX, SAFE_REGEX, kotlinSqlStringConcatRule;
31058
+ var init_sql_string_concat = __esm({
31059
+ "src/rules/kotlin/sql-string-concat.ts"() {
31060
+ "use strict";
31061
+ init_rule();
31062
+ SQL_KEYWORD_REGEX = /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|CREATE\s+TABLE|DROP\s+TABLE|ALTER\s+TABLE)\b/i;
31063
+ UNSAFE_REGEX = /(?:\+|\$\{)/;
31064
+ SAFE_REGEX = /(?:PreparedStatement|setParameter|setString|setInt|setLong|bind|:name|:\\?\\?|\?\\s*,)/;
31065
+ kotlinSqlStringConcatRule = createRule({
31066
+ id: "kotlin/sql-string-concat",
31067
+ category: "security",
31068
+ severity: "high",
31069
+ aiSpecific: false,
31070
+ description: "SQL query built via string concat / template \u2014 use PreparedStatement or setParameter()",
31071
+ create(_context) {
31072
+ return {};
31073
+ },
31074
+ analyze(_context, facts) {
31075
+ const issues = [];
31076
+ const source = facts.v2?._source;
31077
+ if (!source) return issues;
31078
+ if (!/\.kts?$/i.test(facts.filePath)) return issues;
31079
+ const lines = source.split("\n");
31080
+ for (let i = 0; i < lines.length; i++) {
31081
+ const line = lines[i];
31082
+ if (!SQL_KEYWORD_REGEX.test(line)) continue;
31083
+ if (!UNSAFE_REGEX.test(line)) continue;
31084
+ if (SAFE_REGEX.test(line)) continue;
31085
+ issues.push({
31086
+ ruleId: "kotlin/sql-string-concat",
31087
+ category: "security",
31088
+ severity: "high",
31089
+ aiSpecific: false,
31090
+ message: `SQL query built via string concat/template at line ${i + 1}`,
31091
+ line: i + 1,
31092
+ column: 1,
31093
+ advice: 'Use a PreparedStatement (JDBC), setParameter() (Exposed), or an ORM (Room, jOOQ). String concatenation or template interpolation into a SQL query is the canonical SQL-injection pattern \u2014 even "trusted" inputs (signed JWT, internal config) can be influenced by an attacker. Reference: kotlin/sql-string-concat v0.29 (OWASP A03:2021).'
31094
+ });
31095
+ }
31096
+ return issues;
31097
+ }
31098
+ });
31099
+ }
31100
+ });
31101
+
30859
31102
  // src/rules/kotlin/string-concat-loop.ts
30860
31103
  var STRING_CONCAT_REGEX, kotlinStringConcatLoopRule;
30861
31104
  var init_string_concat_loop = __esm({
@@ -40440,7 +40683,7 @@ var init_fatal_error_thrown = __esm({
40440
40683
 
40441
40684
  // src/rules/swift/force-unwrap.ts
40442
40685
  var AS_FORCE_REGEX, ACCESS_FORCE_REGEX, TRY_FORCE_REGEX, swiftForceUnwrapRule;
40443
- var init_force_unwrap = __esm({
40686
+ var init_force_unwrap2 = __esm({
40444
40687
  "src/rules/swift/force-unwrap.ts"() {
40445
40688
  "use strict";
40446
40689
  init_rule();
@@ -46301,8 +46544,13 @@ var init_builtins = __esm({
46301
46544
  init_struct_tag_inconsistency();
46302
46545
  init_coroutine_global_scope();
46303
46546
  init_data_class_defaults_overuse();
46547
+ init_force_unwrap();
46548
+ init_hardcoded_credential();
46304
46549
  init_object_singleton_misuse();
46550
+ init_println_as_log();
46305
46551
  init_println_debug();
46552
+ init_runblocking_misuse();
46553
+ init_sql_string_concat();
46306
46554
  init_string_concat_loop();
46307
46555
  init_gap_monopoly();
46308
46556
  init_math_element_uniformity();
@@ -46343,7 +46591,7 @@ var init_builtins = __esm({
46343
46591
  init_target_blank_no_noopener();
46344
46592
  init_unsafe_html_render();
46345
46593
  init_fatal_error_thrown();
46346
- init_force_unwrap();
46594
+ init_force_unwrap2();
46347
46595
  init_implicitly_unwrapped_optional();
46348
46596
  init_print_debug();
46349
46597
  init_strong_self_capture();
@@ -46430,8 +46678,13 @@ var init_builtins = __esm({
46430
46678
  goStructTagInconsistencyRule,
46431
46679
  kotlinCoroutineGlobalScopeRule,
46432
46680
  kotlinDataClassDefaultsOveruseRule,
46681
+ kotlinForceUnwrapRule,
46682
+ kotlinHardcodedCredentialRule,
46433
46683
  kotlinObjectSingletonMisuseRule,
46684
+ kotlinPrintlnAsLogRule,
46434
46685
  kotlinPrintlnDebugRule,
46686
+ kotlinRunBlockingMisuseRule,
46687
+ kotlinSqlStringConcatRule,
46435
46688
  kotlinStringConcatLoopRule,
46436
46689
  gapMonopolyRule,
46437
46690
  mathElementUniformityRule,
@@ -50952,6 +51205,86 @@ var init_signal_strength = __esm({
50952
51205
  _v9Precision: 0.125,
50953
51206
  defaultOff: true
50954
51207
  },
51208
+ "kotlin/sql-string-concat": {
51209
+ recall: 469e-5,
51210
+ fpRate: 63e-4,
51211
+ ratio: 0.75,
51212
+ precision: 0.0556,
51213
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51214
+ verdict: "DORMANT",
51215
+ _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.75 (DORMANT). 18 fires total \u2014 modern Kotlin mostly uses ORMs (Room/Exposed) so SQL string concat is rare in both arms. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: rule is loadable but invisible by default until v0.30 re-calibration with a larger pos arm.",
51216
+ aiSpecific: false,
51217
+ _v9Verdict: "DORMANT",
51218
+ _v9Lift: 0.75,
51219
+ _v9Recall: 469e-5,
51220
+ _v9FpRate: 63e-4,
51221
+ _v9Precision: 0.0556,
51222
+ defaultOff: true
51223
+ },
51224
+ "kotlin/hardcoded-credential": {
51225
+ recall: 0,
51226
+ fpRate: 0,
51227
+ ratio: 0,
51228
+ precision: 0,
51229
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51230
+ verdict: "DORMANT",
51231
+ _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). 0 fires. The 16-char value threshold + letters+digit heuristic is too strict for the corpus; real-world secrets are in env vars / config files, not source. INSUFFICIENT_DATA: needs different corpus (CI configs, .env samples). defaultOff: loadable but invisible by default.",
51232
+ aiSpecific: false,
51233
+ _v9Verdict: "DORMANT",
51234
+ _v9Lift: 0,
51235
+ _v9Recall: 0,
51236
+ _v9FpRate: 0,
51237
+ _v9Precision: 0,
51238
+ defaultOff: true
51239
+ },
51240
+ "kotlin/runblocking-misuse": {
51241
+ recall: 0.10798,
51242
+ fpRate: 0.21534,
51243
+ ratio: 0.5,
51244
+ precision: 0.0381,
51245
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51246
+ verdict: "DORMANT",
51247
+ _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.50 \u2014 fires 2x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin coroutines used runBlocking more; modern Kotlin uses coroutineScope. Same direction as kotlin/coroutine-global-scope (0.09). defaultOff: loadable but invisible by default until v0.30 re-calibration.",
51248
+ aiSpecific: false,
51249
+ _v9Verdict: "DORMANT",
51250
+ _v9Lift: 0.5,
51251
+ _v9Recall: 0.10798,
51252
+ _v9FpRate: 0.21534,
51253
+ _v9Precision: 0.0381,
51254
+ defaultOff: true
51255
+ },
51256
+ "kotlin/println-as-log": {
51257
+ recall: 0.08451,
51258
+ fpRate: 0.04596,
51259
+ ratio: 1.84,
51260
+ precision: 0.1268,
51261
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51262
+ verdict: "OK",
51263
+ _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=1.84 (\u22651.5) \u2014 first rule in this entire session with a positive direction! Fires 1.84x more on post-2024 AI/demos than pre-2022 production. Precision=12.7% (below 50% USEFUL threshold); verdict=OK. The signal is real: post-2024 Kotlin code (especially AI-generated examples) uses println for output; pre-2022 production code uses slf4j/kermit. INSUFFICIENT_DATA: pos arm only 213 files. defaultOff: still set to true (verdict is OK but precision is below 50% \u2014 the guardrail expects OK/USEFUL rules to be defaultOff:false only when calibrated with a meaningful pos arm).",
51264
+ aiSpecific: false,
51265
+ _v9Verdict: "OK",
51266
+ _v9Lift: 1.84,
51267
+ _v9Recall: 0.08451,
51268
+ _v9FpRate: 0.04596,
51269
+ _v9Precision: 0.1268,
51270
+ defaultOff: true
51271
+ },
51272
+ "kotlin/force-unwrap": {
51273
+ recall: 0.11737,
51274
+ fpRate: 0.33284,
51275
+ ratio: 0.35,
51276
+ precision: 0.0271,
51277
+ lastCalibratedAt: "2026-07-03T00:00:00Z",
51278
+ verdict: "DORMANT",
51279
+ _calibrationNote: "v0.29: v9 Kotlin calibration (2698 neg, 213 pos). ratio=0.35 \u2014 fires 3x more on pre-2022 (neg) than post-2024 (pos). Era-confounded: pre-2022 Kotlin code uses !! freely; modern Kotlin relies on ?./?: and Result<T> wrappers. Stronger era signal than runblocking-misuse. defaultOff: loadable but invisible by default.",
51280
+ aiSpecific: false,
51281
+ _v9Verdict: "DORMANT",
51282
+ _v9Lift: 0.35,
51283
+ _v9Recall: 0.11737,
51284
+ _v9FpRate: 0.33284,
51285
+ _v9Precision: 0.0271,
51286
+ defaultOff: true
51287
+ },
50955
51288
  "swift/force-unwrap": {
50956
51289
  recall: 0,
50957
51290
  fpRate: 0,
@@ -59085,6 +59418,12 @@ var RULE_HINTS = {
59085
59418
  "kotlin/println-debug": "Replace with `Timber.d(...)` or `android.util.Log`. `println` writes to stdout with no level, no redaction, can't be silenced in release builds. (v0.24.0 \u2014 DORMANT.)",
59086
59419
  "kotlin/object-singleton-misuse": "Replace with a class injected via DI, a Repository, or `MutableStateFlow`. The `object` keyword gives a thread-safe singleton handle, NOT thread-safe `var` fields. (v0.24.0 \u2014 DORMANT.)",
59087
59420
  "kotlin/string-concat-loop": "Wrap the loop in `buildString { append(...) }` or hoist a `StringBuilder`. Kotlin can't optimize `s = s + x` in a loop \u2014 O(n\xB2). (v0.24.0 \u2014 DORMANT.)",
59421
+ // v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
59422
+ "kotlin/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter() (Exposed), or an ORM (Room, jOOQ). String concat or template interpolation into a SQL query is the canonical SQL-injection pattern. OWASP A03:2021. (v0.29.0 \u2014 DORMANT.)",
59423
+ "kotlin/hardcoded-credential": "Move the credential to an env var, a .env file (gitignored), or a secrets manager (Vault, AWS Secrets Manager). Hardcoded creds are the #1 secret-leak vector. OWASP A07:2021. (v0.29.0 \u2014 DORMANT.)",
59424
+ "kotlin/runblocking-misuse": "Use coroutineScope { } for structured concurrency, or call the suspend function directly. runBlocking blocks the calling thread and is rarely correct outside main(). (v0.29.0 \u2014 DORMANT.)",
59425
+ "kotlin/println-as-log": "Use slf4j (JVM), android.util.Log (Android), Timber, kermit, or kotlin-logging. println has no log level, no timestamp, no correlation ID. (v0.29.0 \u2014 OK; first positive-signal rule in v9 history, ratio 1.84.)",
59426
+ "kotlin/force-unwrap": "Use ?. (safe call) with ?: (Elvis) for a default, or a proper when/if check. !! throws NullPointerException at runtime and bypasses Kotlin's type system. (v0.29.0 \u2014 DORMANT, era-confounded.)",
59088
59427
  // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
59089
59428
  "swift/force-unwrap": "Replace with the safe form: `as?` + guard/if let, `try?`, or `guard let x = optional else { return }`. `!` crashes unconditionally in release. (v0.24.0 \u2014 DORMANT.)",
59090
59429
  "swift/print-debug": "Replace with `Logger(subsystem:..., category:...).info(...)` (os.log). `print` writes to stdout with no level and no redaction. (v0.24.0 \u2014 DORMANT.)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopbrick",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 95+ rules in 15 categories, computes 4 scores (aiSlopScore: lower=cleaner, engineeringHygiene, security, repositoryHealth composite), and persists the structure for AI agents and CI.",
5
5
  "type": "module",
6
6
  "bin": {