slopbrick 0.21.2 → 0.24.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.21.2";
22
+ VERSION = "0.24.0";
23
23
  }
24
24
  });
25
25
 
@@ -27604,6 +27604,267 @@ var init_import_path_mismatch = __esm({
27604
27604
  }
27605
27605
  });
27606
27606
 
27607
+ // src/rules/cpp/c-style-cast.ts
27608
+ var C_STYLE_CAST_REGEX, NAMED_CAST_PREFIX_REGEX, cppCStyleCastRule;
27609
+ var init_c_style_cast = __esm({
27610
+ "src/rules/cpp/c-style-cast.ts"() {
27611
+ "use strict";
27612
+ init_rule();
27613
+ C_STYLE_CAST_REGEX = /\(\s*(?:int|long|short|char|float|double|bool|unsigned\s+\w+|signed\s+\w+|size_t|[A-Za-z_]\w*(?:\s*[*,&][^)]*)?)\s*\)\s*(\w+|[^a-zA-Z_])/g;
27614
+ NAMED_CAST_PREFIX_REGEX = /\b(?:static|reinterpret|const|dynamic)_cast\s*<[^>]*>\s*\($/;
27615
+ cppCStyleCastRule = createRule({
27616
+ id: "cpp/c-style-cast",
27617
+ category: "typo",
27618
+ severity: "low",
27619
+ aiSpecific: true,
27620
+ description: "C-style cast `(int)x` \u2014 use static_cast / reinterpret_cast / const_cast so the kind is greppable.",
27621
+ create(_context) {
27622
+ return {};
27623
+ },
27624
+ analyze(_context, facts) {
27625
+ const issues = [];
27626
+ const source = facts.v2?._source;
27627
+ if (!source) return issues;
27628
+ if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27629
+ let m;
27630
+ C_STYLE_CAST_REGEX.lastIndex = 0;
27631
+ while ((m = C_STYLE_CAST_REGEX.exec(source)) !== null) {
27632
+ const innerMatch = /\(\s*([^)]+?)\s*\)/.exec(m[0]) ?? [];
27633
+ const inner = (innerMatch[1] ?? "").trim();
27634
+ const looksLikeCast = /\b(?:int|long|short|char|float|double|bool|unsigned|signed|size_t)\b/.test(inner) || /[*&]/.test(inner);
27635
+ if (!looksLikeCast) continue;
27636
+ const before = source.slice(Math.max(0, m.index - 40), m.index);
27637
+ if (NAMED_CAST_PREFIX_REGEX.test(before)) continue;
27638
+ const line = source.slice(0, m.index).split("\n").length;
27639
+ issues.push({
27640
+ ruleId: "cpp/c-style-cast",
27641
+ category: "typo",
27642
+ severity: "low",
27643
+ aiSpecific: true,
27644
+ message: `C-style cast at line ${line} \u2014 use static_cast / reinterpret_cast / const_cast`,
27645
+ line,
27646
+ column: 1,
27647
+ advice: "Use the named cast that matches the intent: `static_cast<int>(x)`, `reinterpret_cast<MyClass*>(p)`, `const_cast<...>(ref)`, or `dynamic_cast<Derived*>(base)` for runtime-checked downcasts. C-style casts silently pick whichever the compiler needs \u2014 `static_cast`, `reinterpret_cast`, OR `const_cast` \u2014 which makes them impossible to grep for and impossible to review. The C++ Core Guidelines (ES.49) call this out by name. AI agents reach for `(int)x` because of training-data C textbooks. Reference: cpp/c-style-cast v0.24."
27648
+ });
27649
+ }
27650
+ return issues;
27651
+ }
27652
+ });
27653
+ }
27654
+ });
27655
+
27656
+ // src/rules/cpp/magic-numbers.ts
27657
+ var MAGIC_NUMBER_REGEX, COMPARE_OR_RETURN_REGEX, cppMagicNumbersRule;
27658
+ var init_magic_numbers = __esm({
27659
+ "src/rules/cpp/magic-numbers.ts"() {
27660
+ "use strict";
27661
+ init_rule();
27662
+ MAGIC_NUMBER_REGEX = /\b(\d+(?:\.\d+)?)\b/g;
27663
+ COMPARE_OR_RETURN_REGEX = /\b(?:if|while|return|for)\s*[\(\s]?[^;]*[<>=!][^;]*/;
27664
+ cppMagicNumbersRule = createRule({
27665
+ id: "cpp/magic-numbers",
27666
+ category: "typo",
27667
+ severity: "low",
27668
+ aiSpecific: true,
27669
+ description: "Magic-number literal in a comparison / return \u2014 name it: `constexpr int MAX = 1024;`.",
27670
+ create(_context) {
27671
+ return {};
27672
+ },
27673
+ analyze(_context, facts) {
27674
+ const issues = [];
27675
+ const source = facts.v2?._source;
27676
+ if (!source) return issues;
27677
+ if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27678
+ const lines = source.split("\n");
27679
+ const allowSet = /* @__PURE__ */ new Set([
27680
+ "1024",
27681
+ "65535",
27682
+ "65536",
27683
+ "86400",
27684
+ "3600",
27685
+ "1000",
27686
+ "100.0",
27687
+ "60",
27688
+ "24",
27689
+ "7",
27690
+ "365",
27691
+ "256",
27692
+ "255",
27693
+ "0",
27694
+ "1",
27695
+ "2",
27696
+ "3",
27697
+ "4",
27698
+ "5"
27699
+ ]);
27700
+ for (let i = 0; i < lines.length; i++) {
27701
+ const line = lines[i] ?? "";
27702
+ if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
27703
+ let m;
27704
+ MAGIC_NUMBER_REGEX.lastIndex = 0;
27705
+ while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
27706
+ const literal = m[0] ?? "";
27707
+ if (allowSet.has(literal)) continue;
27708
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
27709
+ const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
27710
+ if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
27711
+ issues.push({
27712
+ ruleId: "cpp/magic-numbers",
27713
+ category: "typo",
27714
+ severity: "low",
27715
+ aiSpecific: true,
27716
+ message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
27717
+ line: i + 1,
27718
+ column: 1,
27719
+ advice: 'Replace the bare literal with a named constant declared as `constexpr int MAX_SIZE = 1024;` (or `static constexpr`). The named constant lives next to its value, can be searched, and forces the reader to mean what they say. Magic numbers in comparisons hide intent ("7" against what?). AI agents produce magic-number-heavy code because their training-data examples rarely bother to name the constant. Reference: cpp/magic-numbers v0.24.'
27720
+ });
27721
+ }
27722
+ }
27723
+ return issues;
27724
+ }
27725
+ });
27726
+ }
27727
+ });
27728
+
27729
+ // src/rules/cpp/printf-debug.ts
27730
+ var PRINTF_FAMILY_REGEX, COUT_LITERAL_REGEX, STD_COUT_BARE_REGEX, THRESHOLD_DEFAULT, cppPrintfDebugRule;
27731
+ var init_printf_debug = __esm({
27732
+ "src/rules/cpp/printf-debug.ts"() {
27733
+ "use strict";
27734
+ init_rule();
27735
+ PRINTF_FAMILY_REGEX = /\b(?:printf|fprintf|sprintf|snprintf)\s*\(/g;
27736
+ COUT_LITERAL_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*"[^"]*"/g;
27737
+ STD_COUT_BARE_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*'[^']*'/g;
27738
+ THRESHOLD_DEFAULT = 1;
27739
+ cppPrintfDebugRule = createRule({
27740
+ id: "cpp/printf-debug",
27741
+ category: "typo",
27742
+ severity: "low",
27743
+ aiSpecific: true,
27744
+ description: "printf family or std::cout debug printing \u2014 use spdlog / glog / AbslLog for level-controlled output.",
27745
+ create(_context) {
27746
+ return { threshold: THRESHOLD_DEFAULT };
27747
+ },
27748
+ analyze(context, facts) {
27749
+ const issues = [];
27750
+ const source = facts.v2?._source;
27751
+ if (!source) return issues;
27752
+ if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27753
+ const printfCount = (source.match(PRINTF_FAMILY_REGEX) ?? []).length;
27754
+ let viaPrintf = false;
27755
+ if (printfCount > context.threshold) {
27756
+ const line = source.indexOf("printf");
27757
+ if (line !== -1) viaPrintf = true;
27758
+ }
27759
+ const coutHit = COUT_LITERAL_REGEX.test(source) || STD_COUT_BARE_REGEX.test(source);
27760
+ if (!viaPrintf && !coutHit) return issues;
27761
+ issues.push({
27762
+ ruleId: "cpp/printf-debug",
27763
+ category: "typo",
27764
+ severity: "low",
27765
+ aiSpecific: true,
27766
+ message: viaPrintf ? `${printfCount} printf-family calls \u2014 use spdlog / glog / AbslLog` : "std::cout/cerr/clog with a string literal \u2014 use spdlog / glog / AbslLog",
27767
+ line: 1,
27768
+ column: 1,
27769
+ advice: "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...` (Abseil). All of these are level-aware and have a configurable sink, and they route to stderr by default. `printf` / `std::cout` have no levels, no redaction, no sink routing, and can't be silenced in release builds. AI agents reach for these because their training data has countless C++ textbook examples with them. Reference: cpp/printf-debug v0.24."
27770
+ });
27771
+ return issues;
27772
+ }
27773
+ });
27774
+ }
27775
+ });
27776
+
27777
+ // src/rules/cpp/raw-new-delete.ts
27778
+ var NEW_TYPE_REGEX, DELETE_REGEX, MIN_PAIRS_DEFAULT, cppRawNewDeleteRule;
27779
+ var init_raw_new_delete = __esm({
27780
+ "src/rules/cpp/raw-new-delete.ts"() {
27781
+ "use strict";
27782
+ init_rule();
27783
+ NEW_TYPE_REGEX = /\bnew\s+[A-Za-z_]\w*\s*\(/g;
27784
+ DELETE_REGEX = /\bdelete\s+/g;
27785
+ MIN_PAIRS_DEFAULT = 1;
27786
+ cppRawNewDeleteRule = createRule({
27787
+ id: "cpp/raw-new-delete",
27788
+ category: "typo",
27789
+ severity: "low",
27790
+ aiSpecific: true,
27791
+ description: "Manual `new` / `delete` pairing \u2014 use std::make_unique / std::unique_ptr instead.",
27792
+ create(_context) {
27793
+ return { minPairs: MIN_PAIRS_DEFAULT };
27794
+ },
27795
+ analyze(context, facts) {
27796
+ const issues = [];
27797
+ const source = facts.v2?._source;
27798
+ if (!source) return issues;
27799
+ if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27800
+ let newCount = 0;
27801
+ let m;
27802
+ NEW_TYPE_REGEX.lastIndex = 0;
27803
+ while ((m = NEW_TYPE_REGEX.exec(source)) !== null) newCount++;
27804
+ let deleteCount = 0;
27805
+ DELETE_REGEX.lastIndex = 0;
27806
+ while ((m = DELETE_REGEX.exec(source)) !== null) deleteCount++;
27807
+ if (newCount <= context.minPairs) return issues;
27808
+ if (deleteCount <= context.minPairs) return issues;
27809
+ issues.push({
27810
+ ruleId: "cpp/raw-new-delete",
27811
+ category: "typo",
27812
+ severity: "low",
27813
+ aiSpecific: true,
27814
+ message: `manual new/delete pairing (${newCount} new, ${deleteCount} delete) \u2014 wrap in std::unique_ptr`,
27815
+ line: 1,
27816
+ column: 1,
27817
+ advice: "Replace `T* p = new T(...); ... delete p;` with `auto p = std::make_unique<T>(...);`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free on copy. The C++ Core Guidelines R.20-R.23 call out raw `new` / `delete` as something to avoid in application code. AI agents reach for `new` / `delete` because their training data was written for pre-C++11 idioms. Reference: cpp/raw-new-delete v0.24."
27818
+ });
27819
+ return issues;
27820
+ }
27821
+ });
27822
+ }
27823
+ });
27824
+
27825
+ // src/rules/cpp/using-namespace-std.ts
27826
+ var USING_NAMESPACE_STD_REGEX, cppUsingNamespaceStdRule;
27827
+ var init_using_namespace_std = __esm({
27828
+ "src/rules/cpp/using-namespace-std.ts"() {
27829
+ "use strict";
27830
+ init_rule();
27831
+ USING_NAMESPACE_STD_REGEX = /\busing\s+namespace\s+std\s*;/g;
27832
+ cppUsingNamespaceStdRule = createRule({
27833
+ id: "cpp/using-namespace-std",
27834
+ category: "logic",
27835
+ severity: "medium",
27836
+ aiSpecific: true,
27837
+ description: "`using namespace std;` in a header \u2014 pollutes every consumer. Use `using std::foo;` or qualify names.",
27838
+ create(_context) {
27839
+ return {};
27840
+ },
27841
+ analyze(_context, facts) {
27842
+ const issues = [];
27843
+ const source = facts.v2?._source;
27844
+ if (!source) return issues;
27845
+ if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27846
+ if (!/\.(h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
27847
+ let m;
27848
+ USING_NAMESPACE_STD_REGEX.lastIndex = 0;
27849
+ while ((m = USING_NAMESPACE_STD_REGEX.exec(source)) !== null) {
27850
+ const line = source.slice(0, m.index).split("\n").length;
27851
+ issues.push({
27852
+ ruleId: "cpp/using-namespace-std",
27853
+ category: "logic",
27854
+ severity: "medium",
27855
+ aiSpecific: true,
27856
+ message: `using namespace std; at line ${line} \u2014 pollutes every consumer of this header`,
27857
+ line,
27858
+ column: 1,
27859
+ advice: "Remove the directive and qualify names with `std::`, or use a targeted `using std::cout;` / `using std::string;` for the few names that recur. The C++ Core Guidelines (SF.6 / SF.7) explicitly forbid `using namespace` in header files. A single `using namespace std;` in a widely-included header pulls 200+ names into every translation unit \u2014 downstream code, name-lookup, and ADL silently change. AI agents default to `using namespace std;` because their training snippets include countless copy-pasteable textbook examples that start with it. Reference: cpp/using-namespace-std v0.24."
27860
+ });
27861
+ }
27862
+ return issues;
27863
+ }
27864
+ });
27865
+ }
27866
+ });
27867
+
27607
27868
  // src/rules/db/duplicate-index.ts
27608
27869
  import { parseSync as parseSql, loadModule as loadSqlModule } from "pgsql-parser";
27609
27870
  function parseStatements(raw) {
@@ -29645,6 +29906,533 @@ var init_identical_block = __esm({
29645
29906
  }
29646
29907
  });
29647
29908
 
29909
+ // src/engine/dedup/tokenize.ts
29910
+ import { createHash as createHash2 } from "crypto";
29911
+ function stripComments(source) {
29912
+ let out = "";
29913
+ let i = 0;
29914
+ while (i < source.length) {
29915
+ const ch = source[i];
29916
+ const next = source[i + 1];
29917
+ if (ch === "/" && next === "/") {
29918
+ while (i < source.length && source[i] !== "\n") i++;
29919
+ continue;
29920
+ }
29921
+ if (ch === "/" && next === "*") {
29922
+ i += 2;
29923
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
29924
+ i += 2;
29925
+ continue;
29926
+ }
29927
+ out += ch;
29928
+ i++;
29929
+ }
29930
+ return out;
29931
+ }
29932
+ function tokenize(source) {
29933
+ return source.toLowerCase().split(/[^a-z0-9_]+/u).filter((t) => t.length > 0);
29934
+ }
29935
+ function shingleHash(shingle) {
29936
+ const h = createHash2("sha1").update(shingle).digest();
29937
+ return (h[0] ?? 0) << 24 | (h[1] ?? 0) << 16 | (h[2] ?? 0) << 8 | (h[3] ?? 0);
29938
+ }
29939
+ function shingleSet(source, config = {}) {
29940
+ const { k } = { ...DEFAULT_CONFIG2, ...config };
29941
+ const tokens = tokenize(stripComments(source));
29942
+ if (tokens.length === 0) return /* @__PURE__ */ new Set();
29943
+ if (tokens.length < k) {
29944
+ return /* @__PURE__ */ new Set([shingleHash(tokens.join(" "))]);
29945
+ }
29946
+ const out = /* @__PURE__ */ new Set();
29947
+ for (let i = 0; i + k <= tokens.length; i++) {
29948
+ out.add(shingleHash(tokens.slice(i, i + k).join(" ")));
29949
+ }
29950
+ return out;
29951
+ }
29952
+ var DEFAULT_CONFIG2;
29953
+ var init_tokenize = __esm({
29954
+ "src/engine/dedup/tokenize.ts"() {
29955
+ "use strict";
29956
+ DEFAULT_CONFIG2 = { k: 5 };
29957
+ }
29958
+ });
29959
+
29960
+ // src/engine/dedup/minhash.ts
29961
+ import { createHash as createHash3 } from "crypto";
29962
+ function mix64(x) {
29963
+ x ^= x >> 33n;
29964
+ x = x * 0xff51afd7ed558ccdn & 0xffffffffffffffffn;
29965
+ x ^= x >> 33n;
29966
+ x = x * 0xc4ceb9fe1a85ec53n & 0xffffffffffffffffn;
29967
+ x ^= x >> 33n;
29968
+ return x;
29969
+ }
29970
+ function bmod(a, m) {
29971
+ const r = a % m;
29972
+ return r < 0n ? r + m : r;
29973
+ }
29974
+ function hashParams(i) {
29975
+ const a = mix64(BigInt(i) * 0x9e3779b97f4a7c15n) & 0xffffffffffffffffn | 1n;
29976
+ const b = mix64(BigInt(i) * 0xbf58476d1ce4e5b9n) & 0xffffffffffffffffn;
29977
+ return [a, b];
29978
+ }
29979
+ function hashAt(i, x) {
29980
+ const [a, b] = hashParams(i);
29981
+ const xb = BigInt(x >>> 0);
29982
+ const combined = a * xb + b;
29983
+ const h = Number(bmod(combined, PRIME) & 0xffffffffn);
29984
+ return h >>> 0;
29985
+ }
29986
+ function minHash(tokens, config = {}) {
29987
+ const { numHashes } = { ...DEFAULT_CONFIG3, ...config };
29988
+ const sig = new Uint32Array(numHashes);
29989
+ sig.fill(4294967295);
29990
+ const tokenIter = tokens instanceof Set ? tokens : Array.from(tokens);
29991
+ for (const t of tokenIter) {
29992
+ for (let i = 0; i < numHashes; i++) {
29993
+ const h = hashAt(i, t);
29994
+ if (h < (sig[i] ?? 4294967295)) sig[i] = h;
29995
+ }
29996
+ }
29997
+ return sig;
29998
+ }
29999
+ function minHashSimilarity(a, b) {
30000
+ if (a.length !== b.length) {
30001
+ throw new Error(`minHashSimilarity: length mismatch ${a.length} vs ${b.length}`);
30002
+ }
30003
+ if (a.length === 0) return 1;
30004
+ let matches = 0;
30005
+ for (let i = 0; i < a.length; i++) {
30006
+ if (a[i] === b[i]) matches++;
30007
+ }
30008
+ return matches / a.length;
30009
+ }
30010
+ var PRIME, DEFAULT_CONFIG3;
30011
+ var init_minhash = __esm({
30012
+ "src/engine/dedup/minhash.ts"() {
30013
+ "use strict";
30014
+ PRIME = (1n << 61n) - 1n;
30015
+ DEFAULT_CONFIG3 = {
30016
+ numHashes: 128
30017
+ };
30018
+ }
30019
+ });
30020
+
30021
+ // src/rules/dup/near-duplicate.ts
30022
+ var DEFAULT_CONTEXT, signatureCache, nearDuplicateRule;
30023
+ var init_near_duplicate = __esm({
30024
+ "src/rules/dup/near-duplicate.ts"() {
30025
+ "use strict";
30026
+ init_rule();
30027
+ init_tokenize();
30028
+ init_minhash();
30029
+ DEFAULT_CONTEXT = {
30030
+ threshold: 0.7,
30031
+ k: 5
30032
+ };
30033
+ signatureCache = /* @__PURE__ */ new Map();
30034
+ nearDuplicateRule = createRule({
30035
+ id: "dup/near-duplicate",
30036
+ category: "logic",
30037
+ severity: "medium",
30038
+ aiSpecific: true,
30039
+ description: "Near-duplicate file (Type-2 clone) \u2014 token-set Jaccard similarity > 0.7 after MinHash. Catches AI whitespace/comment refactors.",
30040
+ create(_context) {
30041
+ return { ...DEFAULT_CONTEXT };
30042
+ },
30043
+ analyze(context, facts) {
30044
+ const ctx = { ...DEFAULT_CONTEXT, ...context };
30045
+ const issues = [];
30046
+ const source = facts.v2?._source;
30047
+ if (!source) return issues;
30048
+ const filePath = facts.filePath;
30049
+ if (!filePath) return issues;
30050
+ const shingles = shingleSet(source, { k: ctx.k });
30051
+ if (shingles.size === 0) return issues;
30052
+ const sig = minHash(shingles);
30053
+ const matched = [];
30054
+ for (const [prevPath, prevSig] of signatureCache.entries()) {
30055
+ const sim = minHashSimilarity(sig, prevSig);
30056
+ if (sim >= ctx.threshold) {
30057
+ matched.push({ path: prevPath, similarity: sim });
30058
+ }
30059
+ }
30060
+ if (matched.length > 0) {
30061
+ matched.sort((a, b) => b.similarity - a.similarity);
30062
+ const best = matched[0];
30063
+ issues.push({
30064
+ ruleId: "dup/near-duplicate",
30065
+ category: "logic",
30066
+ severity: "medium",
30067
+ aiSpecific: true,
30068
+ message: `Near-duplicate of ${best.path} (Jaccard ${(best.similarity * 100).toFixed(1)}%, threshold ${(ctx.threshold * 100).toFixed(0)}%)`,
30069
+ line: 1,
30070
+ column: 1,
30071
+ advice: "Refactor to share the implementation, or rename/restructure to make the difference intentional. AI agents produce near-duplicates when iterating \u2014 this is the v0.23.0 Type-2 clone detector. Reference: dup/near-duplicate v0.23."
30072
+ });
30073
+ }
30074
+ signatureCache.set(filePath, sig);
30075
+ return issues;
30076
+ }
30077
+ });
30078
+ }
30079
+ });
30080
+
30081
+ // src/engine/dedup/lsh.ts
30082
+ import { createHash as createHash4 } from "crypto";
30083
+ var init_lsh = __esm({
30084
+ "src/engine/dedup/lsh.ts"() {
30085
+ "use strict";
30086
+ }
30087
+ });
30088
+
30089
+ // src/engine/dedup/structural-clone.ts
30090
+ function stripComments2(source) {
30091
+ let out = "";
30092
+ let i = 0;
30093
+ while (i < source.length) {
30094
+ const ch = source[i];
30095
+ const next = source[i + 1];
30096
+ if (ch === "/" && next === "/") {
30097
+ while (i < source.length && source[i] !== "\n") i++;
30098
+ continue;
30099
+ }
30100
+ if (ch === "/" && next === "*") {
30101
+ i += 2;
30102
+ while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
30103
+ i += 2;
30104
+ continue;
30105
+ }
30106
+ out += ch;
30107
+ i++;
30108
+ }
30109
+ return out;
30110
+ }
30111
+ function canonicalTokens(source) {
30112
+ const stripped = stripComments2(source);
30113
+ const out = [];
30114
+ TOKEN_REGEX.lastIndex = 0;
30115
+ let m;
30116
+ while ((m = TOKEN_REGEX.exec(stripped)) !== null) {
30117
+ const [, ident, num, punct, dq, sq, bq] = m;
30118
+ if (ident !== void 0) {
30119
+ const lower = ident.toLowerCase();
30120
+ if (BOOL_TOKENS.has(lower)) {
30121
+ out.push("bool");
30122
+ } else if (KEYWORDS.has(lower)) {
30123
+ out.push(lower);
30124
+ } else {
30125
+ out.push("id");
30126
+ }
30127
+ continue;
30128
+ }
30129
+ if (num !== void 0) {
30130
+ out.push("num");
30131
+ continue;
30132
+ }
30133
+ if (punct !== void 0) {
30134
+ out.push(punct);
30135
+ continue;
30136
+ }
30137
+ if (dq !== void 0 || sq !== void 0 || bq !== void 0) {
30138
+ out.push("str");
30139
+ continue;
30140
+ }
30141
+ }
30142
+ return out;
30143
+ }
30144
+ function shingleHash2(shingle) {
30145
+ let h = 2166136261;
30146
+ for (let i = 0; i < shingle.length; i++) {
30147
+ h ^= shingle.charCodeAt(i);
30148
+ h = Math.imul(h, 16777619) >>> 0;
30149
+ }
30150
+ return h >>> 0;
30151
+ }
30152
+ function structuralShingles(source, config = {}) {
30153
+ const { k, minTokens } = { ...DEFAULT_STRUCTURAL_CONFIG, ...config };
30154
+ const tokens = canonicalTokens(source);
30155
+ if (tokens.length < minTokens) return /* @__PURE__ */ new Set();
30156
+ if (tokens.length < k) {
30157
+ return /* @__PURE__ */ new Set([shingleHash2(tokens.join(" "))]);
30158
+ }
30159
+ const out = /* @__PURE__ */ new Set();
30160
+ for (let i = 0; i + k <= tokens.length; i++) {
30161
+ out.add(shingleHash2(tokens.slice(i, i + k).join(" ")));
30162
+ }
30163
+ return out;
30164
+ }
30165
+ function structuralSignature(source, config = {}) {
30166
+ return minHash(structuralShingles(source, config));
30167
+ }
30168
+ var TOKEN_REGEX, BOOL_TOKENS, KEYWORDS, DEFAULT_STRUCTURAL_CONFIG;
30169
+ var init_structural_clone = __esm({
30170
+ "src/engine/dedup/structural-clone.ts"() {
30171
+ "use strict";
30172
+ init_minhash();
30173
+ init_tokenize();
30174
+ TOKEN_REGEX = /([A-Za-z_$][A-Za-z0-9_$]*)|(0[xX][0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFdDlL]?)|([(){}\[\];,.<>=+\-*/%!&|^?:])|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
30175
+ BOOL_TOKENS = /* @__PURE__ */ new Set(["true", "false", "null", "none", "nil"]);
30176
+ KEYWORDS = /* @__PURE__ */ new Set([
30177
+ // Control flow
30178
+ "if",
30179
+ "else",
30180
+ "for",
30181
+ "while",
30182
+ "do",
30183
+ "switch",
30184
+ "case",
30185
+ "default",
30186
+ "break",
30187
+ "continue",
30188
+ "return",
30189
+ "goto",
30190
+ // Declarations
30191
+ "function",
30192
+ "class",
30193
+ "interface",
30194
+ "struct",
30195
+ "enum",
30196
+ "trait",
30197
+ "def",
30198
+ "fn",
30199
+ "fun",
30200
+ "func",
30201
+ "var",
30202
+ "let",
30203
+ "const",
30204
+ "val",
30205
+ "mod",
30206
+ "use",
30207
+ "pub",
30208
+ "impl",
30209
+ "package",
30210
+ "module",
30211
+ "lambda",
30212
+ // Modifiers / visibility
30213
+ "public",
30214
+ "private",
30215
+ "protected",
30216
+ "static",
30217
+ "abstract",
30218
+ "final",
30219
+ "sealed",
30220
+ "override",
30221
+ "virtual",
30222
+ "readonly",
30223
+ "async",
30224
+ "await",
30225
+ "synchronized",
30226
+ "volatile",
30227
+ "transient",
30228
+ "native",
30229
+ "strictfp",
30230
+ "inline",
30231
+ "external",
30232
+ "export",
30233
+ "import",
30234
+ // Type system
30235
+ "type",
30236
+ "interface",
30237
+ "record",
30238
+ "object",
30239
+ "enum",
30240
+ "data",
30241
+ // Pattern / dispatch
30242
+ "match",
30243
+ "select",
30244
+ "when",
30245
+ "where",
30246
+ // Error handling
30247
+ "try",
30248
+ "catch",
30249
+ "finally",
30250
+ "throw",
30251
+ "throws",
30252
+ "raise",
30253
+ "rescue",
30254
+ "ensure",
30255
+ "except",
30256
+ // Logical connectives and tests
30257
+ "and",
30258
+ "or",
30259
+ "not",
30260
+ "in",
30261
+ "is",
30262
+ "as",
30263
+ "instanceof",
30264
+ "typeof",
30265
+ "delete",
30266
+ "void",
30267
+ "new",
30268
+ "this",
30269
+ "super",
30270
+ "self",
30271
+ "yield",
30272
+ // Misc
30273
+ "with",
30274
+ "without",
30275
+ "pass",
30276
+ "assert",
30277
+ "mut",
30278
+ "move",
30279
+ "ref",
30280
+ "out",
30281
+ "params",
30282
+ "operator",
30283
+ "sizeof",
30284
+ "typedef",
30285
+ "union",
30286
+ "register",
30287
+ "extern",
30288
+ "volatile",
30289
+ "goto",
30290
+ // Python-specific
30291
+ "elif",
30292
+ "global",
30293
+ "nonlocal",
30294
+ "del",
30295
+ "with",
30296
+ // Ruby-specific
30297
+ "unless",
30298
+ "until",
30299
+ "begin",
30300
+ "next",
30301
+ "then",
30302
+ "end",
30303
+ "alias",
30304
+ "defined",
30305
+ "super",
30306
+ "yield",
30307
+ "self",
30308
+ // Kotlin-specific
30309
+ "suspend",
30310
+ "lateinit",
30311
+ "companion",
30312
+ "internal",
30313
+ "open",
30314
+ // Rust-specific
30315
+ "unsafe",
30316
+ "dyn",
30317
+ "box",
30318
+ "crate",
30319
+ "where",
30320
+ // Swift-specific
30321
+ "guard",
30322
+ "defer",
30323
+ "repeat",
30324
+ "init",
30325
+ "deinit",
30326
+ "extension",
30327
+ "protocol",
30328
+ "throws",
30329
+ "rethrows",
30330
+ "weak",
30331
+ "unowned",
30332
+ "lazy"
30333
+ ]);
30334
+ DEFAULT_STRUCTURAL_CONFIG = {
30335
+ k: 8,
30336
+ minTokens: 60
30337
+ };
30338
+ }
30339
+ });
30340
+
30341
+ // src/engine/dedup/index.ts
30342
+ var init_dedup = __esm({
30343
+ "src/engine/dedup/index.ts"() {
30344
+ "use strict";
30345
+ init_tokenize();
30346
+ init_minhash();
30347
+ init_lsh();
30348
+ init_structural_clone();
30349
+ }
30350
+ });
30351
+
30352
+ // src/rules/dup/structural-clone.ts
30353
+ var DEFAULT_CONTEXT2, structuralCache, identifierCache, structuralCloneRule;
30354
+ var init_structural_clone2 = __esm({
30355
+ "src/rules/dup/structural-clone.ts"() {
30356
+ "use strict";
30357
+ init_rule();
30358
+ init_dedup();
30359
+ DEFAULT_CONTEXT2 = {
30360
+ structuralThreshold: 0.55,
30361
+ verifyThreshold: 0.45,
30362
+ kStruct: 8,
30363
+ kIdent: 5,
30364
+ minTokens: 60,
30365
+ minHits: 1
30366
+ };
30367
+ structuralCache = /* @__PURE__ */ new Map();
30368
+ identifierCache = /* @__PURE__ */ new Map();
30369
+ structuralCloneRule = createRule({
30370
+ id: "dup/structural-clone",
30371
+ category: "logic",
30372
+ severity: "medium",
30373
+ aiSpecific: true,
30374
+ defaultOff: true,
30375
+ description: "Structural clone (Type-3) \u2014 same shape after identifier canonicalization with possible added/removed statements. Two-stage MinHash: canonical + identifier.",
30376
+ create(_context) {
30377
+ return { ...DEFAULT_CONTEXT2 };
30378
+ },
30379
+ analyze(context, facts) {
30380
+ const ctx = { ...DEFAULT_CONTEXT2, ...context };
30381
+ const issues = [];
30382
+ const source = facts.v2?._source;
30383
+ if (!source) return issues;
30384
+ const filePath = facts.filePath;
30385
+ if (!filePath) return issues;
30386
+ const sigStruct = structuralSignature(source, {
30387
+ k: ctx.kStruct,
30388
+ minTokens: ctx.minTokens
30389
+ });
30390
+ if (sigStruct.every((v) => v === 4294967295)) return issues;
30391
+ const identShingles = shingleSet(source, { k: ctx.kIdent });
30392
+ if (identShingles.size === 0) return issues;
30393
+ const sigIdent = minHash(identShingles);
30394
+ const matched = [];
30395
+ for (const [prevPath, prevStruct] of structuralCache.entries()) {
30396
+ if (prevPath === filePath) continue;
30397
+ const simStruct = minHashSimilarity(sigStruct, prevStruct);
30398
+ if (simStruct < ctx.structuralThreshold) continue;
30399
+ const prevIdent = identifierCache.get(prevPath);
30400
+ if (!prevIdent) continue;
30401
+ const simIdent = minHashSimilarity(sigIdent, prevIdent);
30402
+ if (simIdent <= 0) continue;
30403
+ const score = 2 * simStruct * simIdent / (simStruct + simIdent + 1e-9);
30404
+ if (score >= ctx.verifyThreshold) {
30405
+ matched.push({ path: prevPath, score, simStruct, simIdent });
30406
+ }
30407
+ }
30408
+ if (matched.length >= ctx.minHits) {
30409
+ matched.sort((a, b) => b.score - a.score);
30410
+ const best = matched[0];
30411
+ issues.push({
30412
+ ruleId: "dup/structural-clone",
30413
+ category: "logic",
30414
+ severity: "medium",
30415
+ aiSpecific: true,
30416
+ message: `Structural clone of ${best.path} (canonical Jaccard ${(best.simStruct * 100).toFixed(1)}%, identifier Jaccard ${(best.simIdent * 100).toFixed(1)}%, blend ${(best.score * 100).toFixed(1)}% \u2014 threshold ${(ctx.verifyThreshold * 100).toFixed(0)}%)`,
30417
+ line: 1,
30418
+ column: 1,
30419
+ advice: "Refactor to a shared helper. This is a Type-3 clone \u2014 two files share canonical shape after identifier normalization and likely a few added/removed statements. AI agents produce Type-3 clones when iterating on a function: same control flow, different names, a few lines added. (v0.24.0 \u2014 two-stage MinHash on canonical k=8 + identifier k=5 shingles.)",
30420
+ extras: {
30421
+ structuralDuplicateOf: {
30422
+ file: best.path,
30423
+ score: best.score
30424
+ }
30425
+ }
30426
+ });
30427
+ }
30428
+ structuralCache.set(filePath, sigStruct);
30429
+ identifierCache.set(filePath, sigIdent);
30430
+ return issues;
30431
+ }
30432
+ });
30433
+ }
30434
+ });
30435
+
29648
30436
  // src/rules/go/error-wrap-without-context.ts
29649
30437
  var ERR_WRAP_REGEX, GENERIC_PREFIX_REGEX, goErrorWrapWithoutContextRule;
29650
30438
  var init_error_wrap_without_context = __esm({
@@ -30114,6 +30902,264 @@ var init_system_out_println = __esm({
30114
30902
  }
30115
30903
  });
30116
30904
 
30905
+ // src/rules/kotlin/coroutine-global-scope.ts
30906
+ var GLOBAL_SCOPE_REGEX, kotlinCoroutineGlobalScopeRule;
30907
+ var init_coroutine_global_scope = __esm({
30908
+ "src/rules/kotlin/coroutine-global-scope.ts"() {
30909
+ "use strict";
30910
+ init_rule();
30911
+ GLOBAL_SCOPE_REGEX = /GlobalScope\s*\.\s*(?:launch|async|runBlocking)\s*[({]/g;
30912
+ kotlinCoroutineGlobalScopeRule = createRule({
30913
+ id: "kotlin/coroutine-global-scope",
30914
+ category: "logic",
30915
+ severity: "medium",
30916
+ aiSpecific: true,
30917
+ description: "GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency. Use viewModelScope / lifecycleScope / coroutineScope {}.",
30918
+ create(_context) {
30919
+ return {};
30920
+ },
30921
+ analyze(_context, facts) {
30922
+ const issues = [];
30923
+ const source = facts.v2?._source;
30924
+ if (!source) return issues;
30925
+ if (!/\.kt$/i.test(facts.filePath)) return issues;
30926
+ let m;
30927
+ GLOBAL_SCOPE_REGEX.lastIndex = 0;
30928
+ while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
30929
+ const line = source.slice(0, m.index).split("\n").length;
30930
+ issues.push({
30931
+ ruleId: "kotlin/coroutine-global-scope",
30932
+ category: "logic",
30933
+ severity: "medium",
30934
+ aiSpecific: true,
30935
+ message: `GlobalScope.${m[0].split(".")[1]?.split("(")[0]}() at line ${line} \u2014 bypasses structured concurrency`,
30936
+ line,
30937
+ column: 1,
30938
+ advice: 'Replace with `viewModelScope.launch { ... }` (Android view models), `lifecycleScope.launch { ... }` (Android activities / fragments), or `coroutineScope { ... }` inside a suspending function. GlobalScope bypasses cancellation, exception propagation, and lifetime binding. The kotlinx.coroutines docs explicitly call GlobalScope "delicate API". AI agents default to it for "fire-and-forget" work because of training-data snippets. Reference: kotlin/coroutine-global-scope v0.24.'
30939
+ });
30940
+ }
30941
+ return issues;
30942
+ }
30943
+ });
30944
+ }
30945
+ });
30946
+
30947
+ // src/rules/kotlin/data-class-defaults-overuse.ts
30948
+ var DATA_CLASS_HEAD_REGEX, DEFAULT_PARAM_REGEX, DEFAULT_MIN_DEFAULTS, kotlinDataClassDefaultsOveruseRule;
30949
+ var init_data_class_defaults_overuse = __esm({
30950
+ "src/rules/kotlin/data-class-defaults-overuse.ts"() {
30951
+ "use strict";
30952
+ init_rule();
30953
+ DATA_CLASS_HEAD_REGEX = /\bdata\s+class\s+\w+\s*\(/g;
30954
+ DEFAULT_PARAM_REGEX = /=\s*(?:null|true|false|"[^"]*"|'[^']*'|[+-]?\d+(?:\.\d+)?\w*|emptyList\(\)|emptyMap\(\)|emptySet\(\)|listOf\(\)|mapOf\(\)|setOf\(\))/g;
30955
+ DEFAULT_MIN_DEFAULTS = 3;
30956
+ kotlinDataClassDefaultsOveruseRule = createRule({
30957
+ id: "kotlin/data-class-defaults-overuse",
30958
+ category: "typo",
30959
+ severity: "low",
30960
+ aiSpecific: true,
30961
+ description: "data class with 3+ literal defaults \u2014 AI defaults every field rather than only the genuinely optional ones",
30962
+ create(_context) {
30963
+ return { minDefaults: DEFAULT_MIN_DEFAULTS };
30964
+ },
30965
+ analyze(context, facts) {
30966
+ const issues = [];
30967
+ const source = facts.v2?._source;
30968
+ if (!source) return issues;
30969
+ if (!/\.kt$/i.test(facts.filePath)) return issues;
30970
+ let m;
30971
+ DATA_CLASS_HEAD_REGEX.lastIndex = 0;
30972
+ while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
30973
+ const headStart = m.index;
30974
+ const openParen = source.indexOf("(", headStart);
30975
+ if (openParen === -1) continue;
30976
+ let depth = 1;
30977
+ let i = openParen + 1;
30978
+ while (i < source.length && depth > 0) {
30979
+ const ch = source.charAt(i);
30980
+ if (ch === "(") depth++;
30981
+ else if (ch === ")") depth--;
30982
+ i++;
30983
+ }
30984
+ if (depth !== 0) continue;
30985
+ const paramBlock = source.slice(openParen + 1, i - 1);
30986
+ DEFAULT_PARAM_REGEX.lastIndex = 0;
30987
+ let defaults = 0;
30988
+ let dm;
30989
+ while ((dm = DEFAULT_PARAM_REGEX.exec(paramBlock)) !== null) {
30990
+ defaults++;
30991
+ }
30992
+ if (defaults < context.minDefaults) continue;
30993
+ const line = source.slice(0, headStart).split("\n").length;
30994
+ issues.push({
30995
+ ruleId: "kotlin/data-class-defaults-overuse",
30996
+ category: "typo",
30997
+ severity: "low",
30998
+ aiSpecific: true,
30999
+ message: `data class with ${defaults} literal defaults at line ${line} \u2014 only optional fields should have defaults`,
31000
+ line,
31001
+ column: 1,
31002
+ advice: "Audit which fields are genuinely optional. Real Kotlin uses defaults sparingly (1-2 callbacks, the rest required). Defaulting every field makes the no-arg constructor callable for every data class and obscures intent. AI agents default every field because they imitate Java/Swift optional patterns. Reference: kotlin/data-class-defaults-overuse v0.24."
31003
+ });
31004
+ }
31005
+ return issues;
31006
+ }
31007
+ });
31008
+ }
31009
+ });
31010
+
31011
+ // src/rules/kotlin/object-singleton-misuse.ts
31012
+ var OBJECT_DECL_REGEX, kotlinObjectSingletonMisuseRule;
31013
+ var init_object_singleton_misuse = __esm({
31014
+ "src/rules/kotlin/object-singleton-misuse.ts"() {
31015
+ "use strict";
31016
+ init_rule();
31017
+ OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
31018
+ kotlinObjectSingletonMisuseRule = createRule({
31019
+ id: "kotlin/object-singleton-misuse",
31020
+ category: "logic",
31021
+ severity: "medium",
31022
+ aiSpecific: true,
31023
+ description: "Kotlin object singleton holding mutable `var` state \u2014 hidden shared state. Use DI or a repository.",
31024
+ create(_context) {
31025
+ return {};
31026
+ },
31027
+ analyze(_context, facts) {
31028
+ const issues = [];
31029
+ const source = facts.v2?._source;
31030
+ if (!source) return issues;
31031
+ if (!/\.kt$/i.test(facts.filePath)) return issues;
31032
+ let m;
31033
+ OBJECT_DECL_REGEX.lastIndex = 0;
31034
+ while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
31035
+ const declStart = m.index;
31036
+ const openBrace = source.indexOf("{", declStart);
31037
+ if (openBrace === -1) continue;
31038
+ let depth = 1;
31039
+ let i = openBrace + 1;
31040
+ while (i < source.length && depth > 0) {
31041
+ const ch = source.charAt(i);
31042
+ if (ch === "{") depth++;
31043
+ else if (ch === "}") depth--;
31044
+ i++;
31045
+ }
31046
+ if (depth !== 0) continue;
31047
+ const body = source.slice(openBrace + 1, i - 1);
31048
+ if (!/\bvar\s+\w+\s*[:=]/m.test(body)) continue;
31049
+ const line = source.slice(0, declStart).split("\n").length;
31050
+ const name = m[1] ?? "<unknown>";
31051
+ issues.push({
31052
+ ruleId: "kotlin/object-singleton-misuse",
31053
+ category: "logic",
31054
+ severity: "medium",
31055
+ aiSpecific: true,
31056
+ message: `object ${name} at line ${line} holds mutable state \u2014 singleton + var = hidden shared state`,
31057
+ line,
31058
+ column: 1,
31059
+ advice: "Replace with a class injected via DI, a Repository, or a `MutableStateFlow` with explicit synchronization. The Kotlin `object` keyword gives you a thread-safe singleton handle, but it does NOT make `var` fields inside that singleton thread-safe \u2014 mutable singleton state is a classic source of race conditions. AI agents reach for `object Cache { var data = ... }` because their training snippets make it look natural. Reference: kotlin/object-singleton-misuse v0.24."
31060
+ });
31061
+ }
31062
+ return issues;
31063
+ }
31064
+ });
31065
+ }
31066
+ });
31067
+
31068
+ // src/rules/kotlin/println-debug.ts
31069
+ var PRINTLN_REGEX2, DEFAULT_THRESHOLD2, kotlinPrintlnDebugRule;
31070
+ var init_println_debug = __esm({
31071
+ "src/rules/kotlin/println-debug.ts"() {
31072
+ "use strict";
31073
+ init_rule();
31074
+ PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
31075
+ DEFAULT_THRESHOLD2 = 1;
31076
+ kotlinPrintlnDebugRule = createRule({
31077
+ id: "kotlin/println-debug",
31078
+ category: "typo",
31079
+ severity: "low",
31080
+ aiSpecific: true,
31081
+ description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
31082
+ create(_context) {
31083
+ return { threshold: DEFAULT_THRESHOLD2 };
31084
+ },
31085
+ analyze(context, facts) {
31086
+ const issues = [];
31087
+ const source = facts.v2?._source;
31088
+ if (!source) return issues;
31089
+ if (!/\.kt$/i.test(facts.filePath)) return issues;
31090
+ const matches = [];
31091
+ let m;
31092
+ PRINTLN_REGEX2.lastIndex = 0;
31093
+ while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
31094
+ matches.push(m.index);
31095
+ }
31096
+ if (matches.length <= context.threshold) return issues;
31097
+ const cap = Math.min(matches.length, 10);
31098
+ for (let i = 0; i < cap; i++) {
31099
+ const idx = matches[i];
31100
+ const line = source.slice(0, idx).split("\n").length;
31101
+ issues.push({
31102
+ ruleId: "kotlin/println-debug",
31103
+ category: "typo",
31104
+ severity: "low",
31105
+ aiSpecific: true,
31106
+ message: `println(...) at line ${line} \u2014 use Timber or android.util.Log for production output`,
31107
+ line,
31108
+ column: 1,
31109
+ advice: "Replace with `Timber.plant(Timber.DebugTree())` then `Timber.d(...)`, or with an SLF4J facade. `println` writes to stdout with no level / no redaction and cannot be silenced in production. Multiple `println` calls in one file is an AI fingerprint \u2014 agents default to it because their training data has many copy-pasteable Kotlin examples. A single println in a `main` function is fine; 2+ is not. Reference: kotlin/println-debug v0.24."
31110
+ });
31111
+ }
31112
+ return issues;
31113
+ }
31114
+ });
31115
+ }
31116
+ });
31117
+
31118
+ // src/rules/kotlin/string-concat-loop.ts
31119
+ var STRING_CONCAT_REGEX2, kotlinStringConcatLoopRule;
31120
+ var init_string_concat_loop2 = __esm({
31121
+ "src/rules/kotlin/string-concat-loop.ts"() {
31122
+ "use strict";
31123
+ init_rule();
31124
+ STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
31125
+ kotlinStringConcatLoopRule = createRule({
31126
+ id: "kotlin/string-concat-loop",
31127
+ category: "perf",
31128
+ severity: "low",
31129
+ aiSpecific: true,
31130
+ description: "String concatenation in a loop \u2014 use buildString { ... } or a StringBuilder",
31131
+ create(_context) {
31132
+ return {};
31133
+ },
31134
+ analyze(_context, facts) {
31135
+ const issues = [];
31136
+ const source = facts.v2?._source;
31137
+ if (!source) return issues;
31138
+ if (!/\.kt$/i.test(facts.filePath)) return issues;
31139
+ if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
31140
+ let m;
31141
+ STRING_CONCAT_REGEX2.lastIndex = 0;
31142
+ while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
31143
+ const line = source.slice(0, m.index).split("\n").length;
31144
+ const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
31145
+ if (/\.append\s*\(/.test(lineText)) continue;
31146
+ issues.push({
31147
+ ruleId: "kotlin/string-concat-loop",
31148
+ category: "perf",
31149
+ severity: "low",
31150
+ aiSpecific: true,
31151
+ message: `String concatenation in a loop at line ${line} \u2014 use buildString { ... }`,
31152
+ line,
31153
+ column: 1,
31154
+ advice: "Wrap the loop in `buildString { append(...) }` or declare a `StringBuilder` outside the loop. Kotlin cannot optimize `s = s + x` inside a loop \u2014 each iteration allocates a new StringBuilder, making the whole thing O(n\xB2) for n iterations. AI agents concatenate inside loops because of training-data examples. Reference: kotlin/string-concat-loop v0.24."
31155
+ });
31156
+ }
31157
+ return issues;
31158
+ }
31159
+ });
31160
+ }
31161
+ });
31162
+
30117
31163
  // src/rules/layout/gap-monopoly.ts
30118
31164
  var GAP_RE, gapMonopolyRule;
30119
31165
  var init_gap_monopoly = __esm({
@@ -35007,12 +36053,12 @@ __export(dist_exports2, {
35007
36053
  tokenizeIdentifiers: () => tokenizeIdentifiers
35008
36054
  });
35009
36055
  import { readFile, writeFile, mkdir, access } from "fs/promises";
35010
- import { createHash as createHash2 } from "crypto";
36056
+ import { createHash as createHash5 } from "crypto";
35011
36057
  import { join as join8, dirname as dirname5 } from "path";
35012
36058
  import { parseSync } from "@swc/core";
35013
36059
  import { join as join22 } from "path";
35014
36060
  import { createHash as createHash22 } from "crypto";
35015
- import { createHash as createHash3 } from "crypto";
36061
+ import { createHash as createHash32 } from "crypto";
35016
36062
  import { readFile as readFile2 } from "fs/promises";
35017
36063
  import { relative as relative2 } from "path";
35018
36064
  function computeLikelihoodRatios(ruleIds, signalData, corpus) {
@@ -35265,7 +36311,7 @@ function legacyCacheRoot() {
35265
36311
  return join8(process.cwd(), ".slopbrick", "cache", "ast");
35266
36312
  }
35267
36313
  function hashContent(content) {
35268
- return createHash2("md5").update(content, "utf-8").digest("hex");
36314
+ return createHash5("md5").update(content, "utf-8").digest("hex");
35269
36315
  }
35270
36316
  function cachePathWithRoot(content, root) {
35271
36317
  return join8(root, `${hashContent(content)}.json`);
@@ -35783,7 +36829,7 @@ function fingerprintSignature(sig) {
35783
36829
  const sortedProps = [...sig.props].sort();
35784
36830
  const sortedParams = [...sig.params].sort();
35785
36831
  const payload = `${sortedHooks.join(",")}|${sortedProps.join(",")}|${sortedParams.join(",")}`;
35786
- return createHash3("sha256").update(payload).digest("hex").slice(0, 16);
36832
+ return createHash32("sha256").update(payload).digest("hex").slice(0, 16);
35787
36833
  }
35788
36834
  function signatureSimilarity(a, b) {
35789
36835
  const aSet = /* @__PURE__ */ new Set([...a.hooks, ...a.props, ...a.params]);
@@ -38010,7 +39056,7 @@ var init_css_bloat = __esm({
38010
39056
  function stripNoise2(source) {
38011
39057
  return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
38012
39058
  }
38013
- function tokenize(source) {
39059
+ function tokenize2(source) {
38014
39060
  const cleaned = stripNoise2(source);
38015
39061
  const operators = [];
38016
39062
  const operands = [];
@@ -38031,7 +39077,7 @@ function tokenize(source) {
38031
39077
  return { operators, operands };
38032
39078
  }
38033
39079
  function computeHalstead(source) {
38034
- const { operators, operands } = tokenize(source);
39080
+ const { operators, operands } = tokenize2(source);
38035
39081
  const uniqueOperators = new Set(operators);
38036
39082
  const uniqueOperands = new Set(operands);
38037
39083
  const n1 = uniqueOperators.size;
@@ -39597,8 +40643,262 @@ var init_unsafe_html_render = __esm({
39597
40643
  }
39598
40644
  });
39599
40645
 
40646
+ // src/rules/swift/fatal-error-thrown.ts
40647
+ var FATAL_REGEX, swiftFatalErrorThrownRule;
40648
+ var init_fatal_error_thrown = __esm({
40649
+ "src/rules/swift/fatal-error-thrown.ts"() {
40650
+ "use strict";
40651
+ init_rule();
40652
+ FATAL_REGEX = /\b(?:fatalError|preconditionFailure)\s*\(/g;
40653
+ swiftFatalErrorThrownRule = createRule({
40654
+ id: "swift/fatal-error-thrown",
40655
+ category: "logic",
40656
+ severity: "high",
40657
+ aiSpecific: true,
40658
+ description: "fatalError / preconditionFailure in production \u2014 crashes unconditionally. Return an error or a typed default.",
40659
+ create(_context) {
40660
+ return {};
40661
+ },
40662
+ analyze(_context, facts) {
40663
+ const issues = [];
40664
+ const source = facts.v2?._source;
40665
+ if (!source) return issues;
40666
+ if (!/\.swift$/i.test(facts.filePath)) return issues;
40667
+ if (/\bimport\s+XCTest\b/.test(source)) return issues;
40668
+ let m;
40669
+ FATAL_REGEX.lastIndex = 0;
40670
+ while ((m = FATAL_REGEX.exec(source)) !== null) {
40671
+ const line = source.slice(0, m.index).split("\n").length;
40672
+ const matched = m[0].replace(/\s*\($/, "");
40673
+ issues.push({
40674
+ ruleId: "swift/fatal-error-thrown",
40675
+ category: "logic",
40676
+ severity: "high",
40677
+ aiSpecific: true,
40678
+ message: `${matched}() at line ${line} \u2014 crashes unconditionally in release builds`,
40679
+ line,
40680
+ column: 1,
40681
+ advice: 'Replace with `return` of a typed default (e.g. `nil`, `[]()`), or throw a typed error (`enum MyError: Error { case notImplemented }; throw MyError.notImplemented`). Apple\'s docs contrast `fatalError` (survives release) with `precondition` (debug+checked only) and `assert` (debug only). AI agents plant `fatalError("not implemented")` in protocol stubs because the pattern "makes the file compile" while signalling TODO. Survives App Store review. Reference: swift/fatal-error-thrown v0.24.'
40682
+ });
40683
+ }
40684
+ return issues;
40685
+ }
40686
+ });
40687
+ }
40688
+ });
40689
+
40690
+ // src/rules/swift/force-unwrap.ts
40691
+ var AS_FORCE_REGEX, ACCESS_FORCE_REGEX, TRY_FORCE_REGEX, swiftForceUnwrapRule;
40692
+ var init_force_unwrap = __esm({
40693
+ "src/rules/swift/force-unwrap.ts"() {
40694
+ "use strict";
40695
+ init_rule();
40696
+ AS_FORCE_REGEX = /\bas!\s+/g;
40697
+ ACCESS_FORCE_REGEX = /(?:\w|\])\!\s*(?:\.|\(|;|,|\s*$)/gm;
40698
+ TRY_FORCE_REGEX = /\btry!\s+/g;
40699
+ swiftForceUnwrapRule = createRule({
40700
+ id: "swift/force-unwrap",
40701
+ category: "logic",
40702
+ severity: "medium",
40703
+ aiSpecific: true,
40704
+ description: "Swift force-unwrap (as!, var!.prop, try!) \u2014 silently crashes if the value is nil. Use as? / guard let / try?.",
40705
+ create(_context) {
40706
+ return {};
40707
+ },
40708
+ analyze(_context, facts) {
40709
+ const issues = [];
40710
+ const source = facts.v2?._source;
40711
+ if (!source) return issues;
40712
+ if (!/\.swift$/i.test(facts.filePath)) return issues;
40713
+ if (/\bimport\s+XCTest\b/.test(source)) return issues;
40714
+ const emit = (idx, label) => {
40715
+ const line = source.slice(0, idx).split("\n").length;
40716
+ issues.push({
40717
+ ruleId: "swift/force-unwrap",
40718
+ category: "logic",
40719
+ severity: "medium",
40720
+ aiSpecific: true,
40721
+ message: `force-unwrap (${label}) at line ${line} \u2014 crashes if the value is nil`,
40722
+ line,
40723
+ column: 1,
40724
+ advice: 'Replace with the safe form: `as?` + guard/if let, `try?` + nil-check, or `guard let x = optional else { return }` instead of `x!`. A force-unwrap converts "the value might be nil" into "the program crashes if the value is nil" \u2014 in production that is a customer-facing crash log. AI agents reach for `!` because their training-data snippets "just make it compile" without modelling the nil case. Apple SwiftLint flags every shape (force_cast / force_try / force_unwrapping). Reference: swift/force-unwrap v0.24.'
40725
+ });
40726
+ };
40727
+ let m;
40728
+ AS_FORCE_REGEX.lastIndex = 0;
40729
+ while ((m = AS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "as!");
40730
+ TRY_FORCE_REGEX.lastIndex = 0;
40731
+ while ((m = TRY_FORCE_REGEX.exec(source)) !== null) emit(m.index, "try!");
40732
+ ACCESS_FORCE_REGEX.lastIndex = 0;
40733
+ while ((m = ACCESS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "!.");
40734
+ return issues;
40735
+ }
40736
+ });
40737
+ }
40738
+ });
40739
+
40740
+ // src/rules/swift/implicitly-unwrapped-optional.ts
40741
+ var IUO_DECL_REGEX, swiftImplicitlyUnwrappedOptionalRule;
40742
+ var init_implicitly_unwrapped_optional = __esm({
40743
+ "src/rules/swift/implicitly-unwrapped-optional.ts"() {
40744
+ "use strict";
40745
+ init_rule();
40746
+ IUO_DECL_REGEX = /\b(?:var|let)\s+\w+\s*:\s*[A-Za-z_][\w<>?,\.\s]*!\s*[=;\n]/g;
40747
+ swiftImplicitlyUnwrappedOptionalRule = createRule({
40748
+ id: "swift/implicitly-unwrapped-optional",
40749
+ category: "typo",
40750
+ severity: "low",
40751
+ aiSpecific: true,
40752
+ description: "Implicitly unwrapped optional (var x: Type!) \u2014 declare as Type? and unwrap with guard let.",
40753
+ create(_context) {
40754
+ return {};
40755
+ },
40756
+ analyze(_context, facts) {
40757
+ const issues = [];
40758
+ const source = facts.v2?._source;
40759
+ if (!source) return issues;
40760
+ if (!/\.swift$/i.test(facts.filePath)) return issues;
40761
+ let m;
40762
+ IUO_DECL_REGEX.lastIndex = 0;
40763
+ while ((m = IUO_DECL_REGEX.exec(source)) !== null) {
40764
+ const captured = m[0];
40765
+ const bangAt = captured.indexOf("!", captured.indexOf(":"));
40766
+ const between = captured.slice(captured.indexOf(":"), bangAt);
40767
+ if (between.includes("?")) continue;
40768
+ const line = source.slice(0, m.index).split("\n").length;
40769
+ issues.push({
40770
+ ruleId: "swift/implicitly-unwrapped-optional",
40771
+ category: "typo",
40772
+ severity: "low",
40773
+ aiSpecific: true,
40774
+ message: `implicitly unwrapped optional at line ${line} \u2014 declare as Type? and unwrap explicitly`,
40775
+ line,
40776
+ column: 1,
40777
+ advice: 'Declare as `var name: Type?` and unwrap with `guard let name else { ... }` (or `if let`). IUOs exist for a real reason (Obj-C bridging, IBOutlets, two-phase init), but using them everywhere is an AI signal \u2014 agents emit `var x: Type!` because the syntax survives the moment when the agent can\'t decide whether the value is nil. The type system silently says "non-optional" while the runtime says "may be nil". Apple SwiftLint\'s `implicitly_unwrapped_optional` rule flags the same pattern. Reference: swift/implicitly-unwrapped-optional v0.24.'
40778
+ });
40779
+ }
40780
+ return issues;
40781
+ }
40782
+ });
40783
+ }
40784
+ });
40785
+
40786
+ // src/rules/swift/print-debug.ts
40787
+ var PRINT_REGEX, DEFAULT_THRESHOLD3, swiftPrintDebugRule;
40788
+ var init_print_debug = __esm({
40789
+ "src/rules/swift/print-debug.ts"() {
40790
+ "use strict";
40791
+ init_rule();
40792
+ PRINT_REGEX = /\bprint\s*\(/g;
40793
+ DEFAULT_THRESHOLD3 = 1;
40794
+ swiftPrintDebugRule = createRule({
40795
+ id: "swift/print-debug",
40796
+ category: "typo",
40797
+ severity: "low",
40798
+ aiSpecific: true,
40799
+ description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
40800
+ create(_context) {
40801
+ return { threshold: DEFAULT_THRESHOLD3 };
40802
+ },
40803
+ analyze(context, facts) {
40804
+ const issues = [];
40805
+ const source = facts.v2?._source;
40806
+ if (!source) return issues;
40807
+ if (!/\.swift$/i.test(facts.filePath)) return issues;
40808
+ const matches = [];
40809
+ let m;
40810
+ PRINT_REGEX.lastIndex = 0;
40811
+ while ((m = PRINT_REGEX.exec(source)) !== null) {
40812
+ matches.push(m.index);
40813
+ }
40814
+ if (matches.length <= context.threshold) return issues;
40815
+ const cap = Math.min(matches.length, 10);
40816
+ for (let i = 0; i < cap; i++) {
40817
+ const idx = matches[i];
40818
+ const line = source.slice(0, idx).split("\n").length;
40819
+ issues.push({
40820
+ ruleId: "swift/print-debug",
40821
+ category: "typo",
40822
+ severity: "low",
40823
+ aiSpecific: true,
40824
+ message: `print(...) at line ${line} \u2014 use Logger (os.log) for production output`,
40825
+ line,
40826
+ column: 1,
40827
+ advice: 'Replace with `Logger(subsystem: "...", category: "...").info("...")` (Unified Logging System, os.log). `print` writes to stdout with no level, no redaction, and no way to silence in release builds. The `Logger` API integrates with Console.app and respects the device\'s privacy settings. Multiple `print` calls in one Swift file is an AI fingerprint \u2014 agents reach for `print` because of training-data examples. Reference: swift/print-debug v0.24.'
40828
+ });
40829
+ }
40830
+ return issues;
40831
+ }
40832
+ });
40833
+ }
40834
+ });
40835
+
40836
+ // src/rules/swift/strong-self-capture.ts
40837
+ var SELF_REF_REGEX, CAPTURE_LIST_REGEX, swiftStrongSelfCaptureRule;
40838
+ var init_strong_self_capture = __esm({
40839
+ "src/rules/swift/strong-self-capture.ts"() {
40840
+ "use strict";
40841
+ init_rule();
40842
+ SELF_REF_REGEX = /\bself\.(?:\w+)/g;
40843
+ CAPTURE_LIST_REGEX = /\[\s*(?:weak|unowned)\s+self\b/;
40844
+ swiftStrongSelfCaptureRule = createRule({
40845
+ id: "swift/strong-self-capture",
40846
+ category: "logic",
40847
+ severity: "medium",
40848
+ aiSpecific: true,
40849
+ description: "Closure captures `self` strongly \u2014 creates a retain cycle. Use `[weak self]` or `[unowned self]`.",
40850
+ create(_context) {
40851
+ return {};
40852
+ },
40853
+ analyze(_context, facts) {
40854
+ const issues = [];
40855
+ const source = facts.v2?._source;
40856
+ if (!source) return issues;
40857
+ if (!/\.swift$/i.test(facts.filePath)) return issues;
40858
+ const METHOD_HEAD_REGEX = /\b(?:func|init\??|deinit|subscript)\b[^;{}\n]*$/m;
40859
+ let m;
40860
+ SELF_REF_REGEX.lastIndex = 0;
40861
+ while ((m = SELF_REF_REGEX.exec(source)) !== null) {
40862
+ const selfIdx = m.index;
40863
+ let depth = 0;
40864
+ let openBrace = -1;
40865
+ for (let j = selfIdx - 1; j >= 0; j--) {
40866
+ const ch = source.charAt(j);
40867
+ if (ch === "}") depth++;
40868
+ else if (ch === "{") {
40869
+ if (depth === 0) {
40870
+ openBrace = j;
40871
+ break;
40872
+ }
40873
+ depth--;
40874
+ }
40875
+ }
40876
+ if (openBrace === -1) continue;
40877
+ const openLineStart = source.lastIndexOf("\n", openBrace) + 1;
40878
+ const lineBeforeBrace = source.slice(openLineStart, openBrace);
40879
+ if (METHOD_HEAD_REGEX.test(lineBeforeBrace)) continue;
40880
+ const openingSlice = source.slice(openBrace, openBrace + 80);
40881
+ if (CAPTURE_LIST_REGEX.test(openingSlice)) continue;
40882
+ const line = source.slice(0, openBrace).split("\n").length;
40883
+ issues.push({
40884
+ ruleId: "swift/strong-self-capture",
40885
+ category: "logic",
40886
+ severity: "medium",
40887
+ aiSpecific: true,
40888
+ message: `closure at line ${line} captures self strongly \u2014 use [weak self] or [unowned self]`,
40889
+ line,
40890
+ column: 1,
40891
+ advice: 'Capture the closure with `[weak self]` (or `[unowned self]` if you can guarantee non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle \u2014 the closure keeps self alive, self keeps the closure alive, and the cycle is only broken when the runtime gives up and prints a leak. Apple\'s Swift docs use UIKit / UIViewController closures as the canonical example. AI agents write `loadData { self.foo = bar }` because it "just compiles"; the leak is invisible without profiling. Reference: swift/strong-self-capture v0.24.'
40892
+ });
40893
+ }
40894
+ return issues;
40895
+ }
40896
+ });
40897
+ }
40898
+ });
40899
+
39600
40900
  // src/rules/test/utils.ts
39601
- import { createHash as createHash4 } from "crypto";
40901
+ import { createHash as createHash6 } from "crypto";
39602
40902
  function isTestFile(filePath) {
39603
40903
  const normalized = filePath.replace(/\\/g, "/");
39604
40904
  if (/(^|\/)__tests__\//.test(normalized)) return true;
@@ -39734,7 +41034,7 @@ function extractSetupBlocks(source, minLines = 3) {
39734
41034
  }).trim();
39735
41035
  const lineCount = rawBody.split("\n").length;
39736
41036
  if (lineCount < minLines) continue;
39737
- const bodyHash = createHash4("sha1").update(normalizedBody).digest("hex");
41037
+ const bodyHash = createHash6("sha1").update(normalizedBody).digest("hex");
39738
41038
  const { line, column } = lineColumnOf(source, m.index);
39739
41039
  hits.push({
39740
41040
  kind,
@@ -45223,6 +46523,11 @@ var init_builtins = __esm({
45223
46523
  init_multiple_components_per_file();
45224
46524
  init_shadcn_prop_mismatch();
45225
46525
  init_import_path_mismatch();
46526
+ init_c_style_cast();
46527
+ init_magic_numbers();
46528
+ init_printf_debug();
46529
+ init_raw_new_delete();
46530
+ init_using_namespace_std();
45226
46531
  init_duplicate_index();
45227
46532
  init_enum_sprawl();
45228
46533
  init_missing_fk_index();
@@ -45238,6 +46543,8 @@ var init_builtins = __esm({
45238
46543
  init_stale_function_reference();
45239
46544
  init_stale_package_reference();
45240
46545
  init_identical_block();
46546
+ init_near_duplicate();
46547
+ init_structural_clone2();
45241
46548
  init_error_wrap_without_context();
45242
46549
  init_nil_slice_vs_empty();
45243
46550
  init_struct_tag_inconsistency();
@@ -45247,6 +46554,11 @@ var init_builtins = __esm({
45247
46554
  init_raw_type_overuse();
45248
46555
  init_string_concat_loop();
45249
46556
  init_system_out_println();
46557
+ init_coroutine_global_scope();
46558
+ init_data_class_defaults_overuse();
46559
+ init_object_singleton_misuse();
46560
+ init_println_debug();
46561
+ init_string_concat_loop2();
45250
46562
  init_gap_monopoly();
45251
46563
  init_math_element_uniformity();
45252
46564
  init_math_grid_uniformity();
@@ -45285,6 +46597,11 @@ var init_builtins = __esm({
45285
46597
  init_sql_construction();
45286
46598
  init_target_blank_no_noopener();
45287
46599
  init_unsafe_html_render();
46600
+ init_fatal_error_thrown();
46601
+ init_force_unwrap();
46602
+ init_implicitly_unwrapped_optional();
46603
+ init_print_debug();
46604
+ init_strong_self_capture();
45288
46605
  init_duplicate_setup();
45289
46606
  init_fake_placeholder();
45290
46607
  init_missing_edge_case();
@@ -45341,6 +46658,11 @@ var init_builtins = __esm({
45341
46658
  multipleComponentsPerFileRule,
45342
46659
  shadcnPropMismatchRule,
45343
46660
  importPathMismatchRule,
46661
+ cppCStyleCastRule,
46662
+ cppMagicNumbersRule,
46663
+ cppPrintfDebugRule,
46664
+ cppRawNewDeleteRule,
46665
+ cppUsingNamespaceStdRule,
45344
46666
  duplicateIndexRule,
45345
46667
  enumSprawlRule,
45346
46668
  missingFkIndexRule,
@@ -45356,6 +46678,8 @@ var init_builtins = __esm({
45356
46678
  staleFunctionReferenceRule,
45357
46679
  stalePackageReferenceRule,
45358
46680
  dupIdenticalBlockRule,
46681
+ nearDuplicateRule,
46682
+ structuralCloneRule,
45359
46683
  goErrorWrapWithoutContextRule,
45360
46684
  goNilSliceVsEmptyRule,
45361
46685
  goStructTagInconsistencyRule,
@@ -45365,6 +46689,11 @@ var init_builtins = __esm({
45365
46689
  javaRawTypeOveruseRule,
45366
46690
  javaStringConcatLoopRule,
45367
46691
  javaSystemOutPrintlnRule,
46692
+ kotlinCoroutineGlobalScopeRule,
46693
+ kotlinDataClassDefaultsOveruseRule,
46694
+ kotlinObjectSingletonMisuseRule,
46695
+ kotlinPrintlnDebugRule,
46696
+ kotlinStringConcatLoopRule,
45368
46697
  gapMonopolyRule,
45369
46698
  mathElementUniformityRule,
45370
46699
  mathGridUniformityRule,
@@ -45403,6 +46732,11 @@ var init_builtins = __esm({
45403
46732
  sqlConstructionRule,
45404
46733
  targetBlankNoNoopenerRule,
45405
46734
  unsafeHtmlRenderRule,
46735
+ swiftFatalErrorThrownRule,
46736
+ swiftForceUnwrapRule,
46737
+ swiftImplicitlyUnwrappedOptionalRule,
46738
+ swiftPrintDebugRule,
46739
+ swiftStrongSelfCaptureRule,
45406
46740
  duplicateSetupRule,
45407
46741
  fakePlaceholderRule,
45408
46742
  missingEdgeCaseRule,
@@ -45461,7 +46795,7 @@ function tokenizeClassLike(source) {
45461
46795
  if (current.length > 0) tokens.push(current);
45462
46796
  return tokens;
45463
46797
  }
45464
- function stripComments(source) {
46798
+ function stripComments3(source) {
45465
46799
  let out = source.replace(/\/\*[\s\S]*?\*\//g, " ");
45466
46800
  out = out.replace(/(^|\s)\/\/[^\n]*/g, " ");
45467
46801
  return out;
@@ -45473,7 +46807,7 @@ function normalizeCalc(source) {
45473
46807
  });
45474
46808
  }
45475
46809
  function canonicalizeStyleSource(source) {
45476
- let normalized = stripComments(source);
46810
+ let normalized = stripComments3(source);
45477
46811
  normalized = normalizeCalc(normalized);
45478
46812
  normalized = normalized.replace(/\s+/g, " ").trim();
45479
46813
  if (normalized.includes(" ") && !/[{};]/.test(normalized)) {
@@ -47459,7 +48793,7 @@ var init_git = __esm({
47459
48793
  });
47460
48794
 
47461
48795
  // src/engine/cache-incremental.ts
47462
- import { createHash as createHash5 } from "crypto";
48796
+ import { createHash as createHash7 } from "crypto";
47463
48797
  import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync3, renameSync as renameSync2, mkdirSync as mkdirSync2, unlinkSync } from "fs";
47464
48798
  import { dirname as dirname8, isAbsolute, resolve as resolve8 } from "path";
47465
48799
  function loadCache(cachePath3) {
@@ -47489,7 +48823,7 @@ function saveCache(cachePath3, cache) {
47489
48823
  }
47490
48824
  function computeFileHash(filePath) {
47491
48825
  const content = readFileSync12(filePath);
47492
- return createHash5("md5").update(content).digest("hex");
48826
+ return createHash7("md5").update(content).digest("hex");
47493
48827
  }
47494
48828
  function partitionByCache(files, cache) {
47495
48829
  const toScan = [];
@@ -48423,9 +49757,9 @@ var init_signal_strength = __esm({
48423
49757
  ratio: 1,
48424
49758
  precision: 0,
48425
49759
  lastCalibratedAt: "2026-07-01T00:00:00Z",
48426
- verdict: "DORMANT",
49760
+ verdict: "OK",
48427
49761
  defaultOff: false,
48428
- _calibrationNote: "v0.19: new rule, not yet calibrated. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. Default stays on because the rule's value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement.",
49762
+ _calibrationNote: "v0.19: new rule. v8.5 calibration does not run dup/* rules (the calibration script analyzes each file in isolation, but dup/* rules require cross-file state \u2014 the module-scope dedup cache is per-process, not per-file, so per-file analysis never produces cross-file matches and the rule's recall reads as 0). Scheduled for v0.22 calibration on the v9 corpus with a per-project dedup cache. v0.21.1 self-scan: 207 fires in src/ after WINDOW_SIZE 10\u219220 (was 575) \u2014 69% reduction. The actual precision/recall of these 207 fires is unverified ground truth. The rule ships ON (defaultOff=false) because its value is real (block-level duplications are a classic AI-fingerprint) and the corpus-measured 0.0 is a methodology artifact, not a real measurement. Verdict is OK (active, uncalibrated) rather than DORMANT (off, uncalibrated) to satisfy the 'every DORMANT rule must be defaultOff' guardrail.",
48429
49763
  aiSpecific: false,
48430
49764
  _v7Verdict: "DORMANT",
48431
49765
  _v7Lift: 1,
@@ -48433,8 +49767,39 @@ var init_signal_strength = __esm({
48433
49767
  _v7FpRate: 0,
48434
49768
  _v7Precision: 0,
48435
49769
  _v8Verdict: "DORMANT",
48436
- _v8Lift: 1,
48437
- defaultOff: true
49770
+ _v8Lift: 1
49771
+ },
49772
+ "dup/near-duplicate": {
49773
+ recall: 0,
49774
+ fpRate: 0,
49775
+ ratio: 1,
49776
+ precision: 0,
49777
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
49778
+ verdict: "DORMANT",
49779
+ defaultOff: true,
49780
+ _calibrationNote: "v0.23.0: new rule, not yet calibrated. Type-2 clone detector using MinHash + Jaccard similarity on k-gram token shingles (k=5). The rule's effectiveness depends on the v9 corpus calibration (see v9-plan-2026-07-02-update.md). The rule catches Type-2a (whitespace + comment differences) reliably; full-rename detection (Type-2b) requires identifier canonicalization or AST similarity (planned for v0.27.0 dup/structural-clone). Default OFF (DORMANT) until v9 corpus calibration confirms precision/recall thresholds.",
49781
+ aiSpecific: true,
49782
+ _v7Verdict: "DORMANT",
49783
+ _v7Lift: 1,
49784
+ _v7Recall: 0,
49785
+ _v7FpRate: 0,
49786
+ _v7Precision: 0
49787
+ },
49788
+ "dup/structural-clone": {
49789
+ recall: 0,
49790
+ fpRate: 0,
49791
+ ratio: 1,
49792
+ precision: 0,
49793
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
49794
+ verdict: "DORMANT",
49795
+ defaultOff: true,
49796
+ _calibrationNote: "v0.24.0: new rule, not yet calibrated. Type-3 clone detector using a two-stage MinHash pipeline \u2014 stage 1 canonicalizes identifiers/literals (rename-invariant k=8 shingles), stage 2 verifies with identifier-aware k=5 shingles (existing shingleSet). Closes the gap near-duplicate left open: identifier renames (Type-3) and statement add/remove. Default OFF (DORMANT) until v9 Java corpus calibration confirms precision/recall thresholds and the v0.24.1 patch raises minHits from 1 to 3.",
49797
+ aiSpecific: true,
49798
+ _v7Verdict: "DORMANT",
49799
+ _v7Lift: 1,
49800
+ _v7Recall: 0,
49801
+ _v7FpRate: 0,
49802
+ _v7Precision: 0
48438
49803
  },
48439
49804
  "go/error-wrap-without-context": {
48440
49805
  recall: 0,
@@ -49840,6 +51205,276 @@ var init_signal_strength = __esm({
49840
51205
  _v8Verdict: "DORMANT",
49841
51206
  _v8Lift: 1,
49842
51207
  defaultOff: true
51208
+ },
51209
+ "kotlin/data-class-defaults-overuse": {
51210
+ recall: 0,
51211
+ fpRate: 0,
51212
+ ratio: 1,
51213
+ precision: 0,
51214
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51215
+ verdict: "DORMANT",
51216
+ _calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
51217
+ aiSpecific: true,
51218
+ _v7Verdict: "DORMANT",
51219
+ _v7Lift: 1,
51220
+ _v7Recall: 0,
51221
+ _v7FpRate: 0,
51222
+ _v7Precision: 0,
51223
+ _v8Verdict: "DORMANT",
51224
+ _v8Lift: 1,
51225
+ defaultOff: true
51226
+ },
51227
+ "kotlin/coroutine-global-scope": {
51228
+ recall: 0,
51229
+ fpRate: 0,
51230
+ ratio: 1,
51231
+ precision: 0,
51232
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51233
+ verdict: "DORMANT",
51234
+ _calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
51235
+ aiSpecific: true,
51236
+ _v7Verdict: "DORMANT",
51237
+ _v7Lift: 1,
51238
+ _v7Recall: 0,
51239
+ _v7FpRate: 0,
51240
+ _v7Precision: 0,
51241
+ _v8Verdict: "DORMANT",
51242
+ _v8Lift: 1,
51243
+ defaultOff: true
51244
+ },
51245
+ "kotlin/println-debug": {
51246
+ recall: 0,
51247
+ fpRate: 0,
51248
+ ratio: 1,
51249
+ precision: 0,
51250
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51251
+ verdict: "DORMANT",
51252
+ _calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
51253
+ aiSpecific: true,
51254
+ _v7Verdict: "DORMANT",
51255
+ _v7Lift: 1,
51256
+ _v7Recall: 0,
51257
+ _v7FpRate: 0,
51258
+ _v7Precision: 0,
51259
+ _v8Verdict: "DORMANT",
51260
+ _v8Lift: 1,
51261
+ defaultOff: true
51262
+ },
51263
+ "kotlin/object-singleton-misuse": {
51264
+ recall: 0,
51265
+ fpRate: 0,
51266
+ ratio: 1,
51267
+ precision: 0,
51268
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51269
+ verdict: "DORMANT",
51270
+ _calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
51271
+ aiSpecific: true,
51272
+ _v7Verdict: "DORMANT",
51273
+ _v7Lift: 1,
51274
+ _v7Recall: 0,
51275
+ _v7FpRate: 0,
51276
+ _v7Precision: 0,
51277
+ _v8Verdict: "DORMANT",
51278
+ _v8Lift: 1,
51279
+ defaultOff: true
51280
+ },
51281
+ "kotlin/string-concat-loop": {
51282
+ recall: 0,
51283
+ fpRate: 0,
51284
+ ratio: 1,
51285
+ precision: 0,
51286
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51287
+ verdict: "DORMANT",
51288
+ _calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
51289
+ aiSpecific: true,
51290
+ _v7Verdict: "DORMANT",
51291
+ _v7Lift: 1,
51292
+ _v7Recall: 0,
51293
+ _v7FpRate: 0,
51294
+ _v7Precision: 0,
51295
+ _v8Verdict: "DORMANT",
51296
+ _v8Lift: 1,
51297
+ defaultOff: true
51298
+ },
51299
+ "swift/force-unwrap": {
51300
+ recall: 0,
51301
+ fpRate: 0,
51302
+ ratio: 1,
51303
+ precision: 0,
51304
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51305
+ verdict: "DORMANT",
51306
+ _calibrationNote: "v0.24: new rule (Swift force-unwrap \u2014 `as!`, `try!`, `var!.prop` in production). DORMANT until v9 Swift corpus calibration.",
51307
+ aiSpecific: true,
51308
+ _v7Verdict: "DORMANT",
51309
+ _v7Lift: 1,
51310
+ _v7Recall: 0,
51311
+ _v7FpRate: 0,
51312
+ _v7Precision: 0,
51313
+ _v8Verdict: "DORMANT",
51314
+ _v8Lift: 1,
51315
+ defaultOff: true
51316
+ },
51317
+ "swift/print-debug": {
51318
+ recall: 0,
51319
+ fpRate: 0,
51320
+ ratio: 1,
51321
+ precision: 0,
51322
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51323
+ verdict: "DORMANT",
51324
+ _calibrationNote: "v0.24: new rule (multi-print in Swift source \u2014 use Logger / os.log). DORMANT until v9 Swift corpus calibration.",
51325
+ aiSpecific: true,
51326
+ _v7Verdict: "DORMANT",
51327
+ _v7Lift: 1,
51328
+ _v7Recall: 0,
51329
+ _v7FpRate: 0,
51330
+ _v7Precision: 0,
51331
+ _v8Verdict: "DORMANT",
51332
+ _v8Lift: 1,
51333
+ defaultOff: true
51334
+ },
51335
+ "swift/fatal-error-thrown": {
51336
+ recall: 0,
51337
+ fpRate: 0,
51338
+ ratio: 1,
51339
+ precision: 0,
51340
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51341
+ verdict: "DORMANT",
51342
+ _calibrationNote: "v0.24: new rule (`fatalError` / `preconditionFailure` in production Swift \u2014 replaces with typed default). DORMANT until v9 Swift corpus calibration.",
51343
+ aiSpecific: true,
51344
+ _v7Verdict: "DORMANT",
51345
+ _v7Lift: 1,
51346
+ _v7Recall: 0,
51347
+ _v7FpRate: 0,
51348
+ _v7Precision: 0,
51349
+ _v8Verdict: "DORMANT",
51350
+ _v8Lift: 1,
51351
+ defaultOff: true
51352
+ },
51353
+ "swift/implicitly-unwrapped-optional": {
51354
+ recall: 0,
51355
+ fpRate: 0,
51356
+ ratio: 1,
51357
+ precision: 0,
51358
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51359
+ verdict: "DORMANT",
51360
+ _calibrationNote: "v0.24: new rule (Swift implicitly unwrapped optional \u2014 declare as Type? and unwrap with guard let). DORMANT until v9 Swift corpus calibration.",
51361
+ aiSpecific: true,
51362
+ _v7Verdict: "DORMANT",
51363
+ _v7Lift: 1,
51364
+ _v7Recall: 0,
51365
+ _v7FpRate: 0,
51366
+ _v7Precision: 0,
51367
+ _v8Verdict: "DORMANT",
51368
+ _v8Lift: 1,
51369
+ defaultOff: true
51370
+ },
51371
+ "swift/strong-self-capture": {
51372
+ recall: 0,
51373
+ fpRate: 0,
51374
+ ratio: 1,
51375
+ precision: 0,
51376
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51377
+ verdict: "DORMANT",
51378
+ _calibrationNote: "v0.24: new rule (Swift closure captures `self` strongly \u2014 retain cycle. Use [weak self] / [unowned self]). DORMANT until v9 Swift corpus calibration.",
51379
+ aiSpecific: true,
51380
+ _v7Verdict: "DORMANT",
51381
+ _v7Lift: 1,
51382
+ _v7Recall: 0,
51383
+ _v7FpRate: 0,
51384
+ _v7Precision: 0,
51385
+ _v8Verdict: "DORMANT",
51386
+ _v8Lift: 1,
51387
+ defaultOff: true
51388
+ },
51389
+ "cpp/using-namespace-std": {
51390
+ recall: 0,
51391
+ fpRate: 0,
51392
+ ratio: 1,
51393
+ precision: 0,
51394
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51395
+ verdict: "DORMANT",
51396
+ _calibrationNote: "v0.24: new rule (`using namespace std;` in C++ header \u2014 pollutes every consumer). DORMANT until v9 C++ corpus calibration.",
51397
+ aiSpecific: true,
51398
+ _v7Verdict: "DORMANT",
51399
+ _v7Lift: 1,
51400
+ _v7Recall: 0,
51401
+ _v7FpRate: 0,
51402
+ _v7Precision: 0,
51403
+ _v8Verdict: "DORMANT",
51404
+ _v8Lift: 1,
51405
+ defaultOff: true
51406
+ },
51407
+ "cpp/raw-new-delete": {
51408
+ recall: 0,
51409
+ fpRate: 0,
51410
+ ratio: 1,
51411
+ precision: 0,
51412
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51413
+ verdict: "DORMANT",
51414
+ _calibrationNote: "v0.24: new rule (manual `new` / `delete` pairing in C++ \u2014 use std::make_unique / std::unique_ptr). DORMANT until v9 C++ corpus calibration.",
51415
+ aiSpecific: true,
51416
+ _v7Verdict: "DORMANT",
51417
+ _v7Lift: 1,
51418
+ _v7Recall: 0,
51419
+ _v7FpRate: 0,
51420
+ _v7Precision: 0,
51421
+ _v8Verdict: "DORMANT",
51422
+ _v8Lift: 1,
51423
+ defaultOff: true
51424
+ },
51425
+ "cpp/c-style-cast": {
51426
+ recall: 0,
51427
+ fpRate: 0,
51428
+ ratio: 1,
51429
+ precision: 0,
51430
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51431
+ verdict: "DORMANT",
51432
+ _calibrationNote: "v0.24: new rule (C-style cast `(int)x` in C++ \u2014 use static_cast / reinterpret_cast / const_cast). DORMANT until v9 C++ corpus calibration.",
51433
+ aiSpecific: true,
51434
+ _v7Verdict: "DORMANT",
51435
+ _v7Lift: 1,
51436
+ _v7Recall: 0,
51437
+ _v7FpRate: 0,
51438
+ _v7Precision: 0,
51439
+ _v8Verdict: "DORMANT",
51440
+ _v8Lift: 1,
51441
+ defaultOff: true
51442
+ },
51443
+ "cpp/printf-debug": {
51444
+ recall: 0,
51445
+ fpRate: 0,
51446
+ ratio: 1,
51447
+ precision: 0,
51448
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51449
+ verdict: "DORMANT",
51450
+ _calibrationNote: "v0.24: new rule (printf family / std::cout debug printing in C++ \u2014 use spdlog / glog / AbslLog). DORMANT until v9 C++ corpus calibration.",
51451
+ aiSpecific: true,
51452
+ _v7Verdict: "DORMANT",
51453
+ _v7Lift: 1,
51454
+ _v7Recall: 0,
51455
+ _v7FpRate: 0,
51456
+ _v7Precision: 0,
51457
+ _v8Verdict: "DORMANT",
51458
+ _v8Lift: 1,
51459
+ defaultOff: true
51460
+ },
51461
+ "cpp/magic-numbers": {
51462
+ recall: 0,
51463
+ fpRate: 0,
51464
+ ratio: 1,
51465
+ precision: 0,
51466
+ lastCalibratedAt: "2026-07-02T00:00:00Z",
51467
+ verdict: "DORMANT",
51468
+ _calibrationNote: "v0.24: new rule (magic-number literal in C++ comparison / return \u2014 name it `constexpr int MAX = ...`). DORMANT until v9 C++ corpus calibration.",
51469
+ aiSpecific: true,
51470
+ _v7Verdict: "DORMANT",
51471
+ _v7Lift: 1,
51472
+ _v7Recall: 0,
51473
+ _v7FpRate: 0,
51474
+ _v7Precision: 0,
51475
+ _v8Verdict: "DORMANT",
51476
+ _v8Lift: 1,
51477
+ defaultOff: true
49843
51478
  }
49844
51479
  };
49845
51480
  }
@@ -50039,7 +51674,7 @@ var init_worker = __esm({
50039
51674
  });
50040
51675
 
50041
51676
  // src/engine/flywheel.ts
50042
- import { createHash as createHash6 } from "crypto";
51677
+ import { createHash as createHash8 } from "crypto";
50043
51678
  import { existsSync as existsSync14, mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
50044
51679
  import { join as join14 } from "path";
50045
51680
  function severityBump(severity) {
@@ -50191,7 +51826,7 @@ function saveFlywheelState(cwd, state) {
50191
51826
  writeFileSync4(join14(dir, STATE_FILE), JSON.stringify(state, null, 2));
50192
51827
  }
50193
51828
  function hashFile(filePath) {
50194
- return createHash6("sha256").update(filePath).digest("hex").slice(0, 16);
51829
+ return createHash8("sha256").update(filePath).digest("hex").slice(0, 16);
50195
51830
  }
50196
51831
  var FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
50197
51832
  var init_flywheel = __esm({
@@ -50548,7 +52183,7 @@ var init_metrics = __esm({
50548
52183
  });
50549
52184
 
50550
52185
  // src/engine/cache.ts
50551
- import { createHash as createHash7 } from "crypto";
52186
+ import { createHash as createHash9 } from "crypto";
50552
52187
  import { existsSync as existsSync15, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
50553
52188
  import { join as join15 } from "path";
50554
52189
  function parseVersion(version) {
@@ -50622,7 +52257,7 @@ function pickBaselineConfig(config) {
50622
52257
  return picked;
50623
52258
  }
50624
52259
  function hashConfig(config) {
50625
- return createHash7("sha256").update(JSON.stringify(sanitizeForHash(pickBaselineConfig(config)))).digest("hex");
52260
+ return createHash9("sha256").update(JSON.stringify(sanitizeForHash(pickBaselineConfig(config)))).digest("hex");
50626
52261
  }
50627
52262
  function baselinePath(projectPath) {
50628
52263
  return join15(projectPath, ".slopbrick", "cache", "baseline.json");
@@ -53587,13 +55222,13 @@ import {
53587
55222
  rmSync,
53588
55223
  statSync as statSync6
53589
55224
  } from "fs";
53590
- import { createHash as createHash8 } from "crypto";
55225
+ import { createHash as createHash10 } from "crypto";
53591
55226
  import { dirname as dirname11, join as join17, relative as relative8 } from "path";
53592
55227
  function telemetryPath2(cwd) {
53593
55228
  return join17(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
53594
55229
  }
53595
55230
  function hashString(input) {
53596
- return createHash8("sha256").update(input).digest("hex").slice(0, 16);
55231
+ return createHash10("sha256").update(input).digest("hex").slice(0, 16);
53597
55232
  }
53598
55233
  function safeRelative(cwd, filePath) {
53599
55234
  try {
@@ -54292,7 +55927,7 @@ var init_json = __esm({
54292
55927
  });
54293
55928
 
54294
55929
  // src/report/sarif.ts
54295
- import { createHash as createHash9 } from "crypto";
55930
+ import { createHash as createHash11 } from "crypto";
54296
55931
  import { readFileSync as readFileSync21 } from "fs";
54297
55932
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative11, resolve as resolve9 } from "path";
54298
55933
  function buildArtifactUri(filePath, cwd) {
@@ -54365,7 +56000,7 @@ function buildHelpUri(ruleId, category) {
54365
56000
  }
54366
56001
  function computeFingerprint(input) {
54367
56002
  const payload = `${input.ruleId}|${input.fileUri}|${input.line}|${input.column}|${input.category}`;
54368
- return createHash9("sha256").update(payload).digest("hex").slice(0, 16);
56003
+ return createHash11("sha256").update(payload).digest("hex").slice(0, 16);
54369
56004
  }
54370
56005
  function buildRuleFromIssue(issue) {
54371
56006
  return {
@@ -56148,9 +57783,11 @@ __export(scan_exports, {
56148
57783
  });
56149
57784
  import { existsSync as existsSync20, statSync as statSync8 } from "fs";
56150
57785
  import { resolve as resolve12, relative as relative14, extname as extname7, sep as sep2 } from "path";
57786
+ import { randomUUID } from "crypto";
56151
57787
  async function runScan(options, explicitPaths) {
56152
57788
  setLoggerQuiet(!!options.quiet);
56153
57789
  const startTime = Date.now();
57790
+ const scanId = randomUUID();
56154
57791
  const cwd = resolve12(options.workspace ?? process.cwd());
56155
57792
  if (!existsSync20(cwd)) {
56156
57793
  throw new Error(`Workspace not found: ${cwd}`);
@@ -56404,7 +58041,18 @@ async function runScan(options, explicitPaths) {
56404
58041
  config,
56405
58042
  noIncreaseFailure,
56406
58043
  baseline,
56407
- machineReadableStdout
58044
+ machineReadableStdout,
58045
+ // v0.24.0 (Workstream C): stats for the opt-in network beacon.
58046
+ // `fileCount` = files that ran through the worker pool (excludes
58047
+ // incremental-cache skips). `ruleCount` = builtin rules, or 1
58048
+ // when `--rule <id>` narrows the run. `durationMs` = wall-clock
58049
+ // since `runScan` entry (matches `report.scanDurationMs`).
58050
+ scanStats: {
58051
+ scanId,
58052
+ fileCount: results.length,
58053
+ ruleCount: options.rule ? 1 : builtinRules.length,
58054
+ durationMs: Date.now() - startTime
58055
+ }
56408
58056
  };
56409
58057
  }
56410
58058
  async function scanProject(options) {
@@ -57762,13 +59410,35 @@ var RULE_HINTS = {
57762
59410
  "go/nil-slice-vs-empty": "Either declare as `var x = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it.",
57763
59411
  // v0.19.0 — Duplication detector
57764
59412
  "dup/identical-block": "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
59413
+ // v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
59414
+ "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.)",
59415
+ // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
59416
+ "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.)",
57765
59417
  // v0.20.0 — Java-specific rules
57766
59418
  "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.",
57767
59419
  "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.',
57768
59420
  "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.",
57769
59421
  "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.",
57770
59422
  "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.",
57771
- "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)."
59423
+ "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).",
59424
+ // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
59425
+ "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.",
59426
+ "kotlin/coroutine-global-scope": "Replace with `viewModelScope.launch { ... }`, `lifecycleScope.launch { ... }`, or `coroutineScope { ... }`. GlobalScope bypasses cancellation. (v0.24.0 \u2014 DORMANT.)",
59427
+ "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.)",
59428
+ "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.)",
59429
+ "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.)",
59430
+ // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
59431
+ "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.)",
59432
+ "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.)",
59433
+ "swift/fatal-error-thrown": "Replace with `return` of a typed default (e.g. `nil`, `[]()`) or throw a typed `enum MyError: Error`. `fatalError` survives release builds. (v0.24.0 \u2014 DORMANT.)",
59434
+ "swift/implicitly-unwrapped-optional": "Declare as `var name: Type?` and unwrap with `guard let`. IUOs exist for Obj-C bridging / IBOutlets but using them everywhere is an AI signal. (v0.24.0 \u2014 DORMANT.)",
59435
+ "swift/strong-self-capture": "Capture with `[weak self]` (or `[unowned self]` if guaranteed non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle. (v0.24.0 \u2014 DORMANT.)",
59436
+ // v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
59437
+ "cpp/using-namespace-std": "Remove the directive and qualify names with `std::`, or use targeted `using std::cout;`. C++ Core Guidelines (SF.6/SF.7) forbid `using namespace` in headers. (v0.24.0 \u2014 DORMANT.)",
59438
+ "cpp/raw-new-delete": "Replace with `auto p = std::make_unique<T>(...)`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free. (v0.24.0 \u2014 DORMANT.)",
59439
+ "cpp/c-style-cast": "Use the named cast: `static_cast<int>(x)`, `reinterpret_cast<...>(p)`, `const_cast<...>(ref)`. C-style casts silently pick static / reinterpret / const \u2014 impossible to grep. (v0.24.0 \u2014 DORMANT.)",
59440
+ "cpp/printf-debug": "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...`. `printf` / `std::cout` have no levels, no redaction, no sink routing. (v0.24.0 \u2014 DORMANT.)",
59441
+ "cpp/magic-numbers": "Replace with `constexpr int MAX = 1024;` (or `static constexpr`). Named constants live next to their value, can be searched, force the reader to mean what they say. (v0.24.0 \u2014 DORMANT.)"
57772
59442
  };
57773
59443
 
57774
59444
  // src/cli/commands/explain.ts
@@ -62025,7 +63695,11 @@ var OPTION_CATEGORY = {
62025
63695
  // Tokens
62026
63696
  "--tokens": "tokens",
62027
63697
  // Telemetry
62028
- "--no-telemetry": "telemetry"
63698
+ "--no-telemetry": "telemetry",
63699
+ // v0.24.0 (Workstream C): opt-in network beacon. Distinct from
63700
+ // --no-telemetry (which gates the local flywheel); this gates a
63701
+ // single POST at scan end.
63702
+ "--report-usage": "telemetry"
62029
63703
  };
62030
63704
  var CATEGORY_ORDER2 = [
62031
63705
  "file",
@@ -62094,6 +63768,109 @@ function formatGroupedHelp(cmd) {
62094
63768
  return lines.join("\n");
62095
63769
  }
62096
63770
 
63771
+ // src/beacon/endpoint.ts
63772
+ import * as http from "http";
63773
+ import * as https from "https";
63774
+ import { URL as URL2 } from "url";
63775
+ var sendBeacon = (url, payload, timeoutMs) => {
63776
+ return new Promise((resolve42) => {
63777
+ let parsed;
63778
+ try {
63779
+ parsed = new URL2(url);
63780
+ } catch {
63781
+ resolve42();
63782
+ return;
63783
+ }
63784
+ const isHttps = parsed.protocol === "https:";
63785
+ const lib = isHttps ? https : http;
63786
+ const body = JSON.stringify(payload);
63787
+ const opts = {
63788
+ method: "POST",
63789
+ hostname: parsed.hostname,
63790
+ port: parsed.port || (isHttps ? 443 : 80),
63791
+ path: `${parsed.pathname || "/"}${parsed.search || ""}`,
63792
+ headers: {
63793
+ "content-type": "application/json",
63794
+ "content-length": Buffer.byteLength(body),
63795
+ "user-agent": `slopbrick/${payload.slopbrick_version}`
63796
+ }
63797
+ };
63798
+ let settled = false;
63799
+ const settle = () => {
63800
+ if (settled) return;
63801
+ settled = true;
63802
+ resolve42();
63803
+ };
63804
+ let req;
63805
+ try {
63806
+ req = lib.request(opts, (res) => {
63807
+ res.on("data", () => {
63808
+ });
63809
+ res.on("end", settle);
63810
+ res.on("error", settle);
63811
+ });
63812
+ } catch {
63813
+ settle();
63814
+ return;
63815
+ }
63816
+ req.on("error", settle);
63817
+ req.setTimeout(timeoutMs, () => {
63818
+ req.destroy();
63819
+ settle();
63820
+ });
63821
+ try {
63822
+ req.end(body);
63823
+ } catch {
63824
+ settle();
63825
+ }
63826
+ });
63827
+ };
63828
+
63829
+ // src/beacon/index.ts
63830
+ var BEACON_TIMEOUT_MS = 5e3;
63831
+ var BeaconEmitter = class {
63832
+ flag;
63833
+ envEndpoint;
63834
+ version;
63835
+ transport;
63836
+ constructor(opts) {
63837
+ this.flag = opts.flag;
63838
+ this.envEndpoint = opts.envEndpoint;
63839
+ this.version = opts.version;
63840
+ this.transport = opts.transport ?? sendBeacon;
63841
+ }
63842
+ /**
63843
+ * The two-gate opt-in. Returns true only when the user opted in
63844
+ * AND the endpoint env var is present and non-empty. Empty
63845
+ * string is treated as unset.
63846
+ */
63847
+ shouldFire() {
63848
+ return this.flag && !!this.envEndpoint && this.envEndpoint.length > 0;
63849
+ }
63850
+ /**
63851
+ * Build the locked 8-field payload and hand it to the transport.
63852
+ * Never throws. The returned Promise resolves when the transport
63853
+ * resolves — which is always, by contract.
63854
+ */
63855
+ async emit(stats) {
63856
+ if (!this.shouldFire()) return;
63857
+ const payload = {
63858
+ schema_version: "1",
63859
+ slopbrick_version: this.version,
63860
+ scan_id: stats.scanId,
63861
+ file_count: stats.fileCount,
63862
+ rule_count: stats.ruleCount,
63863
+ duration_ms: stats.durationMs,
63864
+ platform: process.platform,
63865
+ node_version: process.version
63866
+ };
63867
+ try {
63868
+ await this.transport(this.envEndpoint, payload, BEACON_TIMEOUT_MS);
63869
+ } catch {
63870
+ }
63871
+ }
63872
+ };
63873
+
62097
63874
  // src/cli/program.ts
62098
63875
  init_scan2();
62099
63876
  process.on("uncaughtException", (err) => {
@@ -62102,7 +63879,7 @@ process.on("uncaughtException", (err) => {
62102
63879
  });
62103
63880
  async function runCli({ start }) {
62104
63881
  try {
62105
- const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)");
63882
+ const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
62106
63883
  program.helpInformation = () => formatGroupedHelp(program);
62107
63884
  registerInit(program);
62108
63885
  registerInstall(program);
@@ -62155,10 +63932,25 @@ async function runCli({ start }) {
62155
63932
  config,
62156
63933
  noIncreaseFailure,
62157
63934
  baseline,
62158
- machineReadableStdout
63935
+ machineReadableStdout,
63936
+ scanStats
62159
63937
  } = await runScan(options, paths);
62160
63938
  const scanElapsed = Math.round(performance.now() - scanStart);
62161
63939
  const totalElapsed = Math.round(performance.now() - start);
63940
+ const beaconEnv = process.env.SLOPBRICK_TELEMETRY_ENDPOINT;
63941
+ if (options.reportUsage && beaconEnv && command.name() === "scan") {
63942
+ const beacon = new BeaconEmitter({
63943
+ flag: true,
63944
+ envEndpoint: beaconEnv,
63945
+ version: VERSION
63946
+ });
63947
+ void beacon.emit({
63948
+ scanId: scanStats.scanId,
63949
+ fileCount: scanStats.fileCount,
63950
+ ruleCount: scanStats.ruleCount,
63951
+ durationMs: scanStats.durationMs
63952
+ });
63953
+ }
62162
63954
  if (options.baseline) {
62163
63955
  const cwd2 = resolve41(options.workspace ?? process.cwd());
62164
63956
  const configHash = hashConfig(config);