slopbrick 0.21.2 → 0.25.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/README.md +72 -0
- package/dist/engine/worker.cjs +1521 -12
- package/dist/engine/worker.js +1522 -13
- package/dist/index.cjs +1927 -90
- package/dist/index.d.cts +35 -1
- package/dist/index.d.ts +35 -1
- package/dist/index.js +1953 -117
- package/package.json +5 -2
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.
|
|
22
|
+
VERSION = "0.25.0";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -309,6 +309,38 @@ var init_defaults = __esm({
|
|
|
309
309
|
*/
|
|
310
310
|
testIntelligence: {
|
|
311
311
|
missingEdgeCase: false
|
|
312
|
+
},
|
|
313
|
+
/**
|
|
314
|
+
* v0.25.0: self-scan exclude paths. Defaults cover the three paths
|
|
315
|
+
* that are always false positives when scanning the slopbrick repo
|
|
316
|
+
* itself:
|
|
317
|
+
*
|
|
318
|
+
* - `src/rules/**` — rule definitions contain example patterns
|
|
319
|
+
* the rules themselves detect (self-fire). E.g. a
|
|
320
|
+
* `security/sql-construction` rule's source file has a SQL
|
|
321
|
+
* concat string literal that fires its own regex.
|
|
322
|
+
* - `tests/fixtures/**` — test fixtures contain intentional bad
|
|
323
|
+
* code that the rules must fire on to be useful (each fixture
|
|
324
|
+
* is a positive test case). Scanning them in a self-scan
|
|
325
|
+
* produces ~70 false-positive "issues" that are really just
|
|
326
|
+
* test data.
|
|
327
|
+
* - `tests/rules/**` — rule test files contain expected-issue
|
|
328
|
+
* assertions, also meta-code.
|
|
329
|
+
*
|
|
330
|
+
* Three patterns, ~70 issues removed per self-scan. Combined with
|
|
331
|
+
* the v0.25.0 graded security cap, this restores the v9 plan's
|
|
332
|
+
* "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
|
|
333
|
+
* self-scan FPs collapsing the score to 0).
|
|
334
|
+
*
|
|
335
|
+
* Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
|
|
336
|
+
* to opt out and scan every file (legacy behavior).
|
|
337
|
+
*/
|
|
338
|
+
selfScan: {
|
|
339
|
+
excludePaths: [
|
|
340
|
+
"src/rules/**",
|
|
341
|
+
"tests/fixtures/**",
|
|
342
|
+
"tests/rules/**"
|
|
343
|
+
]
|
|
312
344
|
}
|
|
313
345
|
};
|
|
314
346
|
}
|
|
@@ -27604,6 +27636,267 @@ var init_import_path_mismatch = __esm({
|
|
|
27604
27636
|
}
|
|
27605
27637
|
});
|
|
27606
27638
|
|
|
27639
|
+
// src/rules/cpp/c-style-cast.ts
|
|
27640
|
+
var C_STYLE_CAST_REGEX, NAMED_CAST_PREFIX_REGEX, cppCStyleCastRule;
|
|
27641
|
+
var init_c_style_cast = __esm({
|
|
27642
|
+
"src/rules/cpp/c-style-cast.ts"() {
|
|
27643
|
+
"use strict";
|
|
27644
|
+
init_rule();
|
|
27645
|
+
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;
|
|
27646
|
+
NAMED_CAST_PREFIX_REGEX = /\b(?:static|reinterpret|const|dynamic)_cast\s*<[^>]*>\s*\($/;
|
|
27647
|
+
cppCStyleCastRule = createRule({
|
|
27648
|
+
id: "cpp/c-style-cast",
|
|
27649
|
+
category: "typo",
|
|
27650
|
+
severity: "low",
|
|
27651
|
+
aiSpecific: true,
|
|
27652
|
+
description: "C-style cast `(int)x` \u2014 use static_cast / reinterpret_cast / const_cast so the kind is greppable.",
|
|
27653
|
+
create(_context) {
|
|
27654
|
+
return {};
|
|
27655
|
+
},
|
|
27656
|
+
analyze(_context, facts) {
|
|
27657
|
+
const issues = [];
|
|
27658
|
+
const source = facts.v2?._source;
|
|
27659
|
+
if (!source) return issues;
|
|
27660
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27661
|
+
let m;
|
|
27662
|
+
C_STYLE_CAST_REGEX.lastIndex = 0;
|
|
27663
|
+
while ((m = C_STYLE_CAST_REGEX.exec(source)) !== null) {
|
|
27664
|
+
const innerMatch = /\(\s*([^)]+?)\s*\)/.exec(m[0]) ?? [];
|
|
27665
|
+
const inner = (innerMatch[1] ?? "").trim();
|
|
27666
|
+
const looksLikeCast = /\b(?:int|long|short|char|float|double|bool|unsigned|signed|size_t)\b/.test(inner) || /[*&]/.test(inner);
|
|
27667
|
+
if (!looksLikeCast) continue;
|
|
27668
|
+
const before = source.slice(Math.max(0, m.index - 40), m.index);
|
|
27669
|
+
if (NAMED_CAST_PREFIX_REGEX.test(before)) continue;
|
|
27670
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
27671
|
+
issues.push({
|
|
27672
|
+
ruleId: "cpp/c-style-cast",
|
|
27673
|
+
category: "typo",
|
|
27674
|
+
severity: "low",
|
|
27675
|
+
aiSpecific: true,
|
|
27676
|
+
message: `C-style cast at line ${line} \u2014 use static_cast / reinterpret_cast / const_cast`,
|
|
27677
|
+
line,
|
|
27678
|
+
column: 1,
|
|
27679
|
+
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."
|
|
27680
|
+
});
|
|
27681
|
+
}
|
|
27682
|
+
return issues;
|
|
27683
|
+
}
|
|
27684
|
+
});
|
|
27685
|
+
}
|
|
27686
|
+
});
|
|
27687
|
+
|
|
27688
|
+
// src/rules/cpp/magic-numbers.ts
|
|
27689
|
+
var MAGIC_NUMBER_REGEX, COMPARE_OR_RETURN_REGEX, cppMagicNumbersRule;
|
|
27690
|
+
var init_magic_numbers = __esm({
|
|
27691
|
+
"src/rules/cpp/magic-numbers.ts"() {
|
|
27692
|
+
"use strict";
|
|
27693
|
+
init_rule();
|
|
27694
|
+
MAGIC_NUMBER_REGEX = /\b(\d+(?:\.\d+)?)\b/g;
|
|
27695
|
+
COMPARE_OR_RETURN_REGEX = /\b(?:if|while|return|for)\s*[\(\s]?[^;]*[<>=!][^;]*/;
|
|
27696
|
+
cppMagicNumbersRule = createRule({
|
|
27697
|
+
id: "cpp/magic-numbers",
|
|
27698
|
+
category: "typo",
|
|
27699
|
+
severity: "low",
|
|
27700
|
+
aiSpecific: true,
|
|
27701
|
+
description: "Magic-number literal in a comparison / return \u2014 name it: `constexpr int MAX = 1024;`.",
|
|
27702
|
+
create(_context) {
|
|
27703
|
+
return {};
|
|
27704
|
+
},
|
|
27705
|
+
analyze(_context, facts) {
|
|
27706
|
+
const issues = [];
|
|
27707
|
+
const source = facts.v2?._source;
|
|
27708
|
+
if (!source) return issues;
|
|
27709
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27710
|
+
const lines = source.split("\n");
|
|
27711
|
+
const allowSet = /* @__PURE__ */ new Set([
|
|
27712
|
+
"1024",
|
|
27713
|
+
"65535",
|
|
27714
|
+
"65536",
|
|
27715
|
+
"86400",
|
|
27716
|
+
"3600",
|
|
27717
|
+
"1000",
|
|
27718
|
+
"100.0",
|
|
27719
|
+
"60",
|
|
27720
|
+
"24",
|
|
27721
|
+
"7",
|
|
27722
|
+
"365",
|
|
27723
|
+
"256",
|
|
27724
|
+
"255",
|
|
27725
|
+
"0",
|
|
27726
|
+
"1",
|
|
27727
|
+
"2",
|
|
27728
|
+
"3",
|
|
27729
|
+
"4",
|
|
27730
|
+
"5"
|
|
27731
|
+
]);
|
|
27732
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27733
|
+
const line = lines[i] ?? "";
|
|
27734
|
+
if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
|
|
27735
|
+
let m;
|
|
27736
|
+
MAGIC_NUMBER_REGEX.lastIndex = 0;
|
|
27737
|
+
while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
|
|
27738
|
+
const literal = m[0] ?? "";
|
|
27739
|
+
if (allowSet.has(literal)) continue;
|
|
27740
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
|
|
27741
|
+
const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
|
|
27742
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
|
|
27743
|
+
issues.push({
|
|
27744
|
+
ruleId: "cpp/magic-numbers",
|
|
27745
|
+
category: "typo",
|
|
27746
|
+
severity: "low",
|
|
27747
|
+
aiSpecific: true,
|
|
27748
|
+
message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
|
|
27749
|
+
line: i + 1,
|
|
27750
|
+
column: 1,
|
|
27751
|
+
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.'
|
|
27752
|
+
});
|
|
27753
|
+
}
|
|
27754
|
+
}
|
|
27755
|
+
return issues;
|
|
27756
|
+
}
|
|
27757
|
+
});
|
|
27758
|
+
}
|
|
27759
|
+
});
|
|
27760
|
+
|
|
27761
|
+
// src/rules/cpp/printf-debug.ts
|
|
27762
|
+
var PRINTF_FAMILY_REGEX, COUT_LITERAL_REGEX, STD_COUT_BARE_REGEX, THRESHOLD_DEFAULT, cppPrintfDebugRule;
|
|
27763
|
+
var init_printf_debug = __esm({
|
|
27764
|
+
"src/rules/cpp/printf-debug.ts"() {
|
|
27765
|
+
"use strict";
|
|
27766
|
+
init_rule();
|
|
27767
|
+
PRINTF_FAMILY_REGEX = /\b(?:printf|fprintf|sprintf|snprintf)\s*\(/g;
|
|
27768
|
+
COUT_LITERAL_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*"[^"]*"/g;
|
|
27769
|
+
STD_COUT_BARE_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*'[^']*'/g;
|
|
27770
|
+
THRESHOLD_DEFAULT = 1;
|
|
27771
|
+
cppPrintfDebugRule = createRule({
|
|
27772
|
+
id: "cpp/printf-debug",
|
|
27773
|
+
category: "typo",
|
|
27774
|
+
severity: "low",
|
|
27775
|
+
aiSpecific: true,
|
|
27776
|
+
description: "printf family or std::cout debug printing \u2014 use spdlog / glog / AbslLog for level-controlled output.",
|
|
27777
|
+
create(_context) {
|
|
27778
|
+
return { threshold: THRESHOLD_DEFAULT };
|
|
27779
|
+
},
|
|
27780
|
+
analyze(context, facts) {
|
|
27781
|
+
const issues = [];
|
|
27782
|
+
const source = facts.v2?._source;
|
|
27783
|
+
if (!source) return issues;
|
|
27784
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27785
|
+
const printfCount = (source.match(PRINTF_FAMILY_REGEX) ?? []).length;
|
|
27786
|
+
let viaPrintf = false;
|
|
27787
|
+
if (printfCount > context.threshold) {
|
|
27788
|
+
const line = source.indexOf("printf");
|
|
27789
|
+
if (line !== -1) viaPrintf = true;
|
|
27790
|
+
}
|
|
27791
|
+
const coutHit = COUT_LITERAL_REGEX.test(source) || STD_COUT_BARE_REGEX.test(source);
|
|
27792
|
+
if (!viaPrintf && !coutHit) return issues;
|
|
27793
|
+
issues.push({
|
|
27794
|
+
ruleId: "cpp/printf-debug",
|
|
27795
|
+
category: "typo",
|
|
27796
|
+
severity: "low",
|
|
27797
|
+
aiSpecific: true,
|
|
27798
|
+
message: viaPrintf ? `${printfCount} printf-family calls \u2014 use spdlog / glog / AbslLog` : "std::cout/cerr/clog with a string literal \u2014 use spdlog / glog / AbslLog",
|
|
27799
|
+
line: 1,
|
|
27800
|
+
column: 1,
|
|
27801
|
+
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."
|
|
27802
|
+
});
|
|
27803
|
+
return issues;
|
|
27804
|
+
}
|
|
27805
|
+
});
|
|
27806
|
+
}
|
|
27807
|
+
});
|
|
27808
|
+
|
|
27809
|
+
// src/rules/cpp/raw-new-delete.ts
|
|
27810
|
+
var NEW_TYPE_REGEX, DELETE_REGEX, MIN_PAIRS_DEFAULT, cppRawNewDeleteRule;
|
|
27811
|
+
var init_raw_new_delete = __esm({
|
|
27812
|
+
"src/rules/cpp/raw-new-delete.ts"() {
|
|
27813
|
+
"use strict";
|
|
27814
|
+
init_rule();
|
|
27815
|
+
NEW_TYPE_REGEX = /\bnew\s+[A-Za-z_]\w*\s*\(/g;
|
|
27816
|
+
DELETE_REGEX = /\bdelete\s+/g;
|
|
27817
|
+
MIN_PAIRS_DEFAULT = 1;
|
|
27818
|
+
cppRawNewDeleteRule = createRule({
|
|
27819
|
+
id: "cpp/raw-new-delete",
|
|
27820
|
+
category: "typo",
|
|
27821
|
+
severity: "low",
|
|
27822
|
+
aiSpecific: true,
|
|
27823
|
+
description: "Manual `new` / `delete` pairing \u2014 use std::make_unique / std::unique_ptr instead.",
|
|
27824
|
+
create(_context) {
|
|
27825
|
+
return { minPairs: MIN_PAIRS_DEFAULT };
|
|
27826
|
+
},
|
|
27827
|
+
analyze(context, facts) {
|
|
27828
|
+
const issues = [];
|
|
27829
|
+
const source = facts.v2?._source;
|
|
27830
|
+
if (!source) return issues;
|
|
27831
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27832
|
+
let newCount = 0;
|
|
27833
|
+
let m;
|
|
27834
|
+
NEW_TYPE_REGEX.lastIndex = 0;
|
|
27835
|
+
while ((m = NEW_TYPE_REGEX.exec(source)) !== null) newCount++;
|
|
27836
|
+
let deleteCount = 0;
|
|
27837
|
+
DELETE_REGEX.lastIndex = 0;
|
|
27838
|
+
while ((m = DELETE_REGEX.exec(source)) !== null) deleteCount++;
|
|
27839
|
+
if (newCount <= context.minPairs) return issues;
|
|
27840
|
+
if (deleteCount <= context.minPairs) return issues;
|
|
27841
|
+
issues.push({
|
|
27842
|
+
ruleId: "cpp/raw-new-delete",
|
|
27843
|
+
category: "typo",
|
|
27844
|
+
severity: "low",
|
|
27845
|
+
aiSpecific: true,
|
|
27846
|
+
message: `manual new/delete pairing (${newCount} new, ${deleteCount} delete) \u2014 wrap in std::unique_ptr`,
|
|
27847
|
+
line: 1,
|
|
27848
|
+
column: 1,
|
|
27849
|
+
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."
|
|
27850
|
+
});
|
|
27851
|
+
return issues;
|
|
27852
|
+
}
|
|
27853
|
+
});
|
|
27854
|
+
}
|
|
27855
|
+
});
|
|
27856
|
+
|
|
27857
|
+
// src/rules/cpp/using-namespace-std.ts
|
|
27858
|
+
var USING_NAMESPACE_STD_REGEX, cppUsingNamespaceStdRule;
|
|
27859
|
+
var init_using_namespace_std = __esm({
|
|
27860
|
+
"src/rules/cpp/using-namespace-std.ts"() {
|
|
27861
|
+
"use strict";
|
|
27862
|
+
init_rule();
|
|
27863
|
+
USING_NAMESPACE_STD_REGEX = /\busing\s+namespace\s+std\s*;/g;
|
|
27864
|
+
cppUsingNamespaceStdRule = createRule({
|
|
27865
|
+
id: "cpp/using-namespace-std",
|
|
27866
|
+
category: "logic",
|
|
27867
|
+
severity: "medium",
|
|
27868
|
+
aiSpecific: true,
|
|
27869
|
+
description: "`using namespace std;` in a header \u2014 pollutes every consumer. Use `using std::foo;` or qualify names.",
|
|
27870
|
+
create(_context) {
|
|
27871
|
+
return {};
|
|
27872
|
+
},
|
|
27873
|
+
analyze(_context, facts) {
|
|
27874
|
+
const issues = [];
|
|
27875
|
+
const source = facts.v2?._source;
|
|
27876
|
+
if (!source) return issues;
|
|
27877
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27878
|
+
if (!/\.(h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27879
|
+
let m;
|
|
27880
|
+
USING_NAMESPACE_STD_REGEX.lastIndex = 0;
|
|
27881
|
+
while ((m = USING_NAMESPACE_STD_REGEX.exec(source)) !== null) {
|
|
27882
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
27883
|
+
issues.push({
|
|
27884
|
+
ruleId: "cpp/using-namespace-std",
|
|
27885
|
+
category: "logic",
|
|
27886
|
+
severity: "medium",
|
|
27887
|
+
aiSpecific: true,
|
|
27888
|
+
message: `using namespace std; at line ${line} \u2014 pollutes every consumer of this header`,
|
|
27889
|
+
line,
|
|
27890
|
+
column: 1,
|
|
27891
|
+
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."
|
|
27892
|
+
});
|
|
27893
|
+
}
|
|
27894
|
+
return issues;
|
|
27895
|
+
}
|
|
27896
|
+
});
|
|
27897
|
+
}
|
|
27898
|
+
});
|
|
27899
|
+
|
|
27607
27900
|
// src/rules/db/duplicate-index.ts
|
|
27608
27901
|
import { parseSync as parseSql, loadModule as loadSqlModule } from "pgsql-parser";
|
|
27609
27902
|
function parseStatements(raw) {
|
|
@@ -29645,6 +29938,533 @@ var init_identical_block = __esm({
|
|
|
29645
29938
|
}
|
|
29646
29939
|
});
|
|
29647
29940
|
|
|
29941
|
+
// src/engine/dedup/tokenize.ts
|
|
29942
|
+
import { createHash as createHash2 } from "crypto";
|
|
29943
|
+
function stripComments(source) {
|
|
29944
|
+
let out = "";
|
|
29945
|
+
let i = 0;
|
|
29946
|
+
while (i < source.length) {
|
|
29947
|
+
const ch = source[i];
|
|
29948
|
+
const next = source[i + 1];
|
|
29949
|
+
if (ch === "/" && next === "/") {
|
|
29950
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
29951
|
+
continue;
|
|
29952
|
+
}
|
|
29953
|
+
if (ch === "/" && next === "*") {
|
|
29954
|
+
i += 2;
|
|
29955
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
29956
|
+
i += 2;
|
|
29957
|
+
continue;
|
|
29958
|
+
}
|
|
29959
|
+
out += ch;
|
|
29960
|
+
i++;
|
|
29961
|
+
}
|
|
29962
|
+
return out;
|
|
29963
|
+
}
|
|
29964
|
+
function tokenize(source) {
|
|
29965
|
+
return source.toLowerCase().split(/[^a-z0-9_]+/u).filter((t) => t.length > 0);
|
|
29966
|
+
}
|
|
29967
|
+
function shingleHash(shingle) {
|
|
29968
|
+
const h = createHash2("sha1").update(shingle).digest();
|
|
29969
|
+
return (h[0] ?? 0) << 24 | (h[1] ?? 0) << 16 | (h[2] ?? 0) << 8 | (h[3] ?? 0);
|
|
29970
|
+
}
|
|
29971
|
+
function shingleSet(source, config = {}) {
|
|
29972
|
+
const { k } = { ...DEFAULT_CONFIG2, ...config };
|
|
29973
|
+
const tokens = tokenize(stripComments(source));
|
|
29974
|
+
if (tokens.length === 0) return /* @__PURE__ */ new Set();
|
|
29975
|
+
if (tokens.length < k) {
|
|
29976
|
+
return /* @__PURE__ */ new Set([shingleHash(tokens.join(" "))]);
|
|
29977
|
+
}
|
|
29978
|
+
const out = /* @__PURE__ */ new Set();
|
|
29979
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
29980
|
+
out.add(shingleHash(tokens.slice(i, i + k).join(" ")));
|
|
29981
|
+
}
|
|
29982
|
+
return out;
|
|
29983
|
+
}
|
|
29984
|
+
var DEFAULT_CONFIG2;
|
|
29985
|
+
var init_tokenize = __esm({
|
|
29986
|
+
"src/engine/dedup/tokenize.ts"() {
|
|
29987
|
+
"use strict";
|
|
29988
|
+
DEFAULT_CONFIG2 = { k: 5 };
|
|
29989
|
+
}
|
|
29990
|
+
});
|
|
29991
|
+
|
|
29992
|
+
// src/engine/dedup/minhash.ts
|
|
29993
|
+
import { createHash as createHash3 } from "crypto";
|
|
29994
|
+
function mix64(x) {
|
|
29995
|
+
x ^= x >> 33n;
|
|
29996
|
+
x = x * 0xff51afd7ed558ccdn & 0xffffffffffffffffn;
|
|
29997
|
+
x ^= x >> 33n;
|
|
29998
|
+
x = x * 0xc4ceb9fe1a85ec53n & 0xffffffffffffffffn;
|
|
29999
|
+
x ^= x >> 33n;
|
|
30000
|
+
return x;
|
|
30001
|
+
}
|
|
30002
|
+
function bmod(a, m) {
|
|
30003
|
+
const r = a % m;
|
|
30004
|
+
return r < 0n ? r + m : r;
|
|
30005
|
+
}
|
|
30006
|
+
function hashParams(i) {
|
|
30007
|
+
const a = mix64(BigInt(i) * 0x9e3779b97f4a7c15n) & 0xffffffffffffffffn | 1n;
|
|
30008
|
+
const b = mix64(BigInt(i) * 0xbf58476d1ce4e5b9n) & 0xffffffffffffffffn;
|
|
30009
|
+
return [a, b];
|
|
30010
|
+
}
|
|
30011
|
+
function hashAt(i, x) {
|
|
30012
|
+
const [a, b] = hashParams(i);
|
|
30013
|
+
const xb = BigInt(x >>> 0);
|
|
30014
|
+
const combined = a * xb + b;
|
|
30015
|
+
const h = Number(bmod(combined, PRIME) & 0xffffffffn);
|
|
30016
|
+
return h >>> 0;
|
|
30017
|
+
}
|
|
30018
|
+
function minHash(tokens, config = {}) {
|
|
30019
|
+
const { numHashes } = { ...DEFAULT_CONFIG3, ...config };
|
|
30020
|
+
const sig = new Uint32Array(numHashes);
|
|
30021
|
+
sig.fill(4294967295);
|
|
30022
|
+
const tokenIter = tokens instanceof Set ? tokens : Array.from(tokens);
|
|
30023
|
+
for (const t of tokenIter) {
|
|
30024
|
+
for (let i = 0; i < numHashes; i++) {
|
|
30025
|
+
const h = hashAt(i, t);
|
|
30026
|
+
if (h < (sig[i] ?? 4294967295)) sig[i] = h;
|
|
30027
|
+
}
|
|
30028
|
+
}
|
|
30029
|
+
return sig;
|
|
30030
|
+
}
|
|
30031
|
+
function minHashSimilarity(a, b) {
|
|
30032
|
+
if (a.length !== b.length) {
|
|
30033
|
+
throw new Error(`minHashSimilarity: length mismatch ${a.length} vs ${b.length}`);
|
|
30034
|
+
}
|
|
30035
|
+
if (a.length === 0) return 1;
|
|
30036
|
+
let matches = 0;
|
|
30037
|
+
for (let i = 0; i < a.length; i++) {
|
|
30038
|
+
if (a[i] === b[i]) matches++;
|
|
30039
|
+
}
|
|
30040
|
+
return matches / a.length;
|
|
30041
|
+
}
|
|
30042
|
+
var PRIME, DEFAULT_CONFIG3;
|
|
30043
|
+
var init_minhash = __esm({
|
|
30044
|
+
"src/engine/dedup/minhash.ts"() {
|
|
30045
|
+
"use strict";
|
|
30046
|
+
PRIME = (1n << 61n) - 1n;
|
|
30047
|
+
DEFAULT_CONFIG3 = {
|
|
30048
|
+
numHashes: 128
|
|
30049
|
+
};
|
|
30050
|
+
}
|
|
30051
|
+
});
|
|
30052
|
+
|
|
30053
|
+
// src/rules/dup/near-duplicate.ts
|
|
30054
|
+
var DEFAULT_CONTEXT, signatureCache, nearDuplicateRule;
|
|
30055
|
+
var init_near_duplicate = __esm({
|
|
30056
|
+
"src/rules/dup/near-duplicate.ts"() {
|
|
30057
|
+
"use strict";
|
|
30058
|
+
init_rule();
|
|
30059
|
+
init_tokenize();
|
|
30060
|
+
init_minhash();
|
|
30061
|
+
DEFAULT_CONTEXT = {
|
|
30062
|
+
threshold: 0.7,
|
|
30063
|
+
k: 5
|
|
30064
|
+
};
|
|
30065
|
+
signatureCache = /* @__PURE__ */ new Map();
|
|
30066
|
+
nearDuplicateRule = createRule({
|
|
30067
|
+
id: "dup/near-duplicate",
|
|
30068
|
+
category: "logic",
|
|
30069
|
+
severity: "medium",
|
|
30070
|
+
aiSpecific: true,
|
|
30071
|
+
description: "Near-duplicate file (Type-2 clone) \u2014 token-set Jaccard similarity > 0.7 after MinHash. Catches AI whitespace/comment refactors.",
|
|
30072
|
+
create(_context) {
|
|
30073
|
+
return { ...DEFAULT_CONTEXT };
|
|
30074
|
+
},
|
|
30075
|
+
analyze(context, facts) {
|
|
30076
|
+
const ctx = { ...DEFAULT_CONTEXT, ...context };
|
|
30077
|
+
const issues = [];
|
|
30078
|
+
const source = facts.v2?._source;
|
|
30079
|
+
if (!source) return issues;
|
|
30080
|
+
const filePath = facts.filePath;
|
|
30081
|
+
if (!filePath) return issues;
|
|
30082
|
+
const shingles = shingleSet(source, { k: ctx.k });
|
|
30083
|
+
if (shingles.size === 0) return issues;
|
|
30084
|
+
const sig = minHash(shingles);
|
|
30085
|
+
const matched = [];
|
|
30086
|
+
for (const [prevPath, prevSig] of signatureCache.entries()) {
|
|
30087
|
+
const sim = minHashSimilarity(sig, prevSig);
|
|
30088
|
+
if (sim >= ctx.threshold) {
|
|
30089
|
+
matched.push({ path: prevPath, similarity: sim });
|
|
30090
|
+
}
|
|
30091
|
+
}
|
|
30092
|
+
if (matched.length > 0) {
|
|
30093
|
+
matched.sort((a, b) => b.similarity - a.similarity);
|
|
30094
|
+
const best = matched[0];
|
|
30095
|
+
issues.push({
|
|
30096
|
+
ruleId: "dup/near-duplicate",
|
|
30097
|
+
category: "logic",
|
|
30098
|
+
severity: "medium",
|
|
30099
|
+
aiSpecific: true,
|
|
30100
|
+
message: `Near-duplicate of ${best.path} (Jaccard ${(best.similarity * 100).toFixed(1)}%, threshold ${(ctx.threshold * 100).toFixed(0)}%)`,
|
|
30101
|
+
line: 1,
|
|
30102
|
+
column: 1,
|
|
30103
|
+
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."
|
|
30104
|
+
});
|
|
30105
|
+
}
|
|
30106
|
+
signatureCache.set(filePath, sig);
|
|
30107
|
+
return issues;
|
|
30108
|
+
}
|
|
30109
|
+
});
|
|
30110
|
+
}
|
|
30111
|
+
});
|
|
30112
|
+
|
|
30113
|
+
// src/engine/dedup/lsh.ts
|
|
30114
|
+
import { createHash as createHash4 } from "crypto";
|
|
30115
|
+
var init_lsh = __esm({
|
|
30116
|
+
"src/engine/dedup/lsh.ts"() {
|
|
30117
|
+
"use strict";
|
|
30118
|
+
}
|
|
30119
|
+
});
|
|
30120
|
+
|
|
30121
|
+
// src/engine/dedup/structural-clone.ts
|
|
30122
|
+
function stripComments2(source) {
|
|
30123
|
+
let out = "";
|
|
30124
|
+
let i = 0;
|
|
30125
|
+
while (i < source.length) {
|
|
30126
|
+
const ch = source[i];
|
|
30127
|
+
const next = source[i + 1];
|
|
30128
|
+
if (ch === "/" && next === "/") {
|
|
30129
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
30130
|
+
continue;
|
|
30131
|
+
}
|
|
30132
|
+
if (ch === "/" && next === "*") {
|
|
30133
|
+
i += 2;
|
|
30134
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
30135
|
+
i += 2;
|
|
30136
|
+
continue;
|
|
30137
|
+
}
|
|
30138
|
+
out += ch;
|
|
30139
|
+
i++;
|
|
30140
|
+
}
|
|
30141
|
+
return out;
|
|
30142
|
+
}
|
|
30143
|
+
function canonicalTokens(source) {
|
|
30144
|
+
const stripped = stripComments2(source);
|
|
30145
|
+
const out = [];
|
|
30146
|
+
TOKEN_REGEX.lastIndex = 0;
|
|
30147
|
+
let m;
|
|
30148
|
+
while ((m = TOKEN_REGEX.exec(stripped)) !== null) {
|
|
30149
|
+
const [, ident, num, punct, dq, sq, bq] = m;
|
|
30150
|
+
if (ident !== void 0) {
|
|
30151
|
+
const lower = ident.toLowerCase();
|
|
30152
|
+
if (BOOL_TOKENS.has(lower)) {
|
|
30153
|
+
out.push("bool");
|
|
30154
|
+
} else if (KEYWORDS.has(lower)) {
|
|
30155
|
+
out.push(lower);
|
|
30156
|
+
} else {
|
|
30157
|
+
out.push("id");
|
|
30158
|
+
}
|
|
30159
|
+
continue;
|
|
30160
|
+
}
|
|
30161
|
+
if (num !== void 0) {
|
|
30162
|
+
out.push("num");
|
|
30163
|
+
continue;
|
|
30164
|
+
}
|
|
30165
|
+
if (punct !== void 0) {
|
|
30166
|
+
out.push(punct);
|
|
30167
|
+
continue;
|
|
30168
|
+
}
|
|
30169
|
+
if (dq !== void 0 || sq !== void 0 || bq !== void 0) {
|
|
30170
|
+
out.push("str");
|
|
30171
|
+
continue;
|
|
30172
|
+
}
|
|
30173
|
+
}
|
|
30174
|
+
return out;
|
|
30175
|
+
}
|
|
30176
|
+
function shingleHash2(shingle) {
|
|
30177
|
+
let h = 2166136261;
|
|
30178
|
+
for (let i = 0; i < shingle.length; i++) {
|
|
30179
|
+
h ^= shingle.charCodeAt(i);
|
|
30180
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
30181
|
+
}
|
|
30182
|
+
return h >>> 0;
|
|
30183
|
+
}
|
|
30184
|
+
function structuralShingles(source, config = {}) {
|
|
30185
|
+
const { k, minTokens } = { ...DEFAULT_STRUCTURAL_CONFIG, ...config };
|
|
30186
|
+
const tokens = canonicalTokens(source);
|
|
30187
|
+
if (tokens.length < minTokens) return /* @__PURE__ */ new Set();
|
|
30188
|
+
if (tokens.length < k) {
|
|
30189
|
+
return /* @__PURE__ */ new Set([shingleHash2(tokens.join(" "))]);
|
|
30190
|
+
}
|
|
30191
|
+
const out = /* @__PURE__ */ new Set();
|
|
30192
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
30193
|
+
out.add(shingleHash2(tokens.slice(i, i + k).join(" ")));
|
|
30194
|
+
}
|
|
30195
|
+
return out;
|
|
30196
|
+
}
|
|
30197
|
+
function structuralSignature(source, config = {}) {
|
|
30198
|
+
return minHash(structuralShingles(source, config));
|
|
30199
|
+
}
|
|
30200
|
+
var TOKEN_REGEX, BOOL_TOKENS, KEYWORDS, DEFAULT_STRUCTURAL_CONFIG;
|
|
30201
|
+
var init_structural_clone = __esm({
|
|
30202
|
+
"src/engine/dedup/structural-clone.ts"() {
|
|
30203
|
+
"use strict";
|
|
30204
|
+
init_minhash();
|
|
30205
|
+
init_tokenize();
|
|
30206
|
+
TOKEN_REGEX = /([A-Za-z_$][A-Za-z0-9_$]*)|(0[xX][0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFdDlL]?)|([(){}\[\];,.<>=+\-*/%!&|^?:])|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
|
|
30207
|
+
BOOL_TOKENS = /* @__PURE__ */ new Set(["true", "false", "null", "none", "nil"]);
|
|
30208
|
+
KEYWORDS = /* @__PURE__ */ new Set([
|
|
30209
|
+
// Control flow
|
|
30210
|
+
"if",
|
|
30211
|
+
"else",
|
|
30212
|
+
"for",
|
|
30213
|
+
"while",
|
|
30214
|
+
"do",
|
|
30215
|
+
"switch",
|
|
30216
|
+
"case",
|
|
30217
|
+
"default",
|
|
30218
|
+
"break",
|
|
30219
|
+
"continue",
|
|
30220
|
+
"return",
|
|
30221
|
+
"goto",
|
|
30222
|
+
// Declarations
|
|
30223
|
+
"function",
|
|
30224
|
+
"class",
|
|
30225
|
+
"interface",
|
|
30226
|
+
"struct",
|
|
30227
|
+
"enum",
|
|
30228
|
+
"trait",
|
|
30229
|
+
"def",
|
|
30230
|
+
"fn",
|
|
30231
|
+
"fun",
|
|
30232
|
+
"func",
|
|
30233
|
+
"var",
|
|
30234
|
+
"let",
|
|
30235
|
+
"const",
|
|
30236
|
+
"val",
|
|
30237
|
+
"mod",
|
|
30238
|
+
"use",
|
|
30239
|
+
"pub",
|
|
30240
|
+
"impl",
|
|
30241
|
+
"package",
|
|
30242
|
+
"module",
|
|
30243
|
+
"lambda",
|
|
30244
|
+
// Modifiers / visibility
|
|
30245
|
+
"public",
|
|
30246
|
+
"private",
|
|
30247
|
+
"protected",
|
|
30248
|
+
"static",
|
|
30249
|
+
"abstract",
|
|
30250
|
+
"final",
|
|
30251
|
+
"sealed",
|
|
30252
|
+
"override",
|
|
30253
|
+
"virtual",
|
|
30254
|
+
"readonly",
|
|
30255
|
+
"async",
|
|
30256
|
+
"await",
|
|
30257
|
+
"synchronized",
|
|
30258
|
+
"volatile",
|
|
30259
|
+
"transient",
|
|
30260
|
+
"native",
|
|
30261
|
+
"strictfp",
|
|
30262
|
+
"inline",
|
|
30263
|
+
"external",
|
|
30264
|
+
"export",
|
|
30265
|
+
"import",
|
|
30266
|
+
// Type system
|
|
30267
|
+
"type",
|
|
30268
|
+
"interface",
|
|
30269
|
+
"record",
|
|
30270
|
+
"object",
|
|
30271
|
+
"enum",
|
|
30272
|
+
"data",
|
|
30273
|
+
// Pattern / dispatch
|
|
30274
|
+
"match",
|
|
30275
|
+
"select",
|
|
30276
|
+
"when",
|
|
30277
|
+
"where",
|
|
30278
|
+
// Error handling
|
|
30279
|
+
"try",
|
|
30280
|
+
"catch",
|
|
30281
|
+
"finally",
|
|
30282
|
+
"throw",
|
|
30283
|
+
"throws",
|
|
30284
|
+
"raise",
|
|
30285
|
+
"rescue",
|
|
30286
|
+
"ensure",
|
|
30287
|
+
"except",
|
|
30288
|
+
// Logical connectives and tests
|
|
30289
|
+
"and",
|
|
30290
|
+
"or",
|
|
30291
|
+
"not",
|
|
30292
|
+
"in",
|
|
30293
|
+
"is",
|
|
30294
|
+
"as",
|
|
30295
|
+
"instanceof",
|
|
30296
|
+
"typeof",
|
|
30297
|
+
"delete",
|
|
30298
|
+
"void",
|
|
30299
|
+
"new",
|
|
30300
|
+
"this",
|
|
30301
|
+
"super",
|
|
30302
|
+
"self",
|
|
30303
|
+
"yield",
|
|
30304
|
+
// Misc
|
|
30305
|
+
"with",
|
|
30306
|
+
"without",
|
|
30307
|
+
"pass",
|
|
30308
|
+
"assert",
|
|
30309
|
+
"mut",
|
|
30310
|
+
"move",
|
|
30311
|
+
"ref",
|
|
30312
|
+
"out",
|
|
30313
|
+
"params",
|
|
30314
|
+
"operator",
|
|
30315
|
+
"sizeof",
|
|
30316
|
+
"typedef",
|
|
30317
|
+
"union",
|
|
30318
|
+
"register",
|
|
30319
|
+
"extern",
|
|
30320
|
+
"volatile",
|
|
30321
|
+
"goto",
|
|
30322
|
+
// Python-specific
|
|
30323
|
+
"elif",
|
|
30324
|
+
"global",
|
|
30325
|
+
"nonlocal",
|
|
30326
|
+
"del",
|
|
30327
|
+
"with",
|
|
30328
|
+
// Ruby-specific
|
|
30329
|
+
"unless",
|
|
30330
|
+
"until",
|
|
30331
|
+
"begin",
|
|
30332
|
+
"next",
|
|
30333
|
+
"then",
|
|
30334
|
+
"end",
|
|
30335
|
+
"alias",
|
|
30336
|
+
"defined",
|
|
30337
|
+
"super",
|
|
30338
|
+
"yield",
|
|
30339
|
+
"self",
|
|
30340
|
+
// Kotlin-specific
|
|
30341
|
+
"suspend",
|
|
30342
|
+
"lateinit",
|
|
30343
|
+
"companion",
|
|
30344
|
+
"internal",
|
|
30345
|
+
"open",
|
|
30346
|
+
// Rust-specific
|
|
30347
|
+
"unsafe",
|
|
30348
|
+
"dyn",
|
|
30349
|
+
"box",
|
|
30350
|
+
"crate",
|
|
30351
|
+
"where",
|
|
30352
|
+
// Swift-specific
|
|
30353
|
+
"guard",
|
|
30354
|
+
"defer",
|
|
30355
|
+
"repeat",
|
|
30356
|
+
"init",
|
|
30357
|
+
"deinit",
|
|
30358
|
+
"extension",
|
|
30359
|
+
"protocol",
|
|
30360
|
+
"throws",
|
|
30361
|
+
"rethrows",
|
|
30362
|
+
"weak",
|
|
30363
|
+
"unowned",
|
|
30364
|
+
"lazy"
|
|
30365
|
+
]);
|
|
30366
|
+
DEFAULT_STRUCTURAL_CONFIG = {
|
|
30367
|
+
k: 8,
|
|
30368
|
+
minTokens: 60
|
|
30369
|
+
};
|
|
30370
|
+
}
|
|
30371
|
+
});
|
|
30372
|
+
|
|
30373
|
+
// src/engine/dedup/index.ts
|
|
30374
|
+
var init_dedup = __esm({
|
|
30375
|
+
"src/engine/dedup/index.ts"() {
|
|
30376
|
+
"use strict";
|
|
30377
|
+
init_tokenize();
|
|
30378
|
+
init_minhash();
|
|
30379
|
+
init_lsh();
|
|
30380
|
+
init_structural_clone();
|
|
30381
|
+
}
|
|
30382
|
+
});
|
|
30383
|
+
|
|
30384
|
+
// src/rules/dup/structural-clone.ts
|
|
30385
|
+
var DEFAULT_CONTEXT2, structuralCache, identifierCache, structuralCloneRule;
|
|
30386
|
+
var init_structural_clone2 = __esm({
|
|
30387
|
+
"src/rules/dup/structural-clone.ts"() {
|
|
30388
|
+
"use strict";
|
|
30389
|
+
init_rule();
|
|
30390
|
+
init_dedup();
|
|
30391
|
+
DEFAULT_CONTEXT2 = {
|
|
30392
|
+
structuralThreshold: 0.55,
|
|
30393
|
+
verifyThreshold: 0.45,
|
|
30394
|
+
kStruct: 8,
|
|
30395
|
+
kIdent: 5,
|
|
30396
|
+
minTokens: 60,
|
|
30397
|
+
minHits: 1
|
|
30398
|
+
};
|
|
30399
|
+
structuralCache = /* @__PURE__ */ new Map();
|
|
30400
|
+
identifierCache = /* @__PURE__ */ new Map();
|
|
30401
|
+
structuralCloneRule = createRule({
|
|
30402
|
+
id: "dup/structural-clone",
|
|
30403
|
+
category: "logic",
|
|
30404
|
+
severity: "medium",
|
|
30405
|
+
aiSpecific: true,
|
|
30406
|
+
defaultOff: true,
|
|
30407
|
+
description: "Structural clone (Type-3) \u2014 same shape after identifier canonicalization with possible added/removed statements. Two-stage MinHash: canonical + identifier.",
|
|
30408
|
+
create(_context) {
|
|
30409
|
+
return { ...DEFAULT_CONTEXT2 };
|
|
30410
|
+
},
|
|
30411
|
+
analyze(context, facts) {
|
|
30412
|
+
const ctx = { ...DEFAULT_CONTEXT2, ...context };
|
|
30413
|
+
const issues = [];
|
|
30414
|
+
const source = facts.v2?._source;
|
|
30415
|
+
if (!source) return issues;
|
|
30416
|
+
const filePath = facts.filePath;
|
|
30417
|
+
if (!filePath) return issues;
|
|
30418
|
+
const sigStruct = structuralSignature(source, {
|
|
30419
|
+
k: ctx.kStruct,
|
|
30420
|
+
minTokens: ctx.minTokens
|
|
30421
|
+
});
|
|
30422
|
+
if (sigStruct.every((v) => v === 4294967295)) return issues;
|
|
30423
|
+
const identShingles = shingleSet(source, { k: ctx.kIdent });
|
|
30424
|
+
if (identShingles.size === 0) return issues;
|
|
30425
|
+
const sigIdent = minHash(identShingles);
|
|
30426
|
+
const matched = [];
|
|
30427
|
+
for (const [prevPath, prevStruct] of structuralCache.entries()) {
|
|
30428
|
+
if (prevPath === filePath) continue;
|
|
30429
|
+
const simStruct = minHashSimilarity(sigStruct, prevStruct);
|
|
30430
|
+
if (simStruct < ctx.structuralThreshold) continue;
|
|
30431
|
+
const prevIdent = identifierCache.get(prevPath);
|
|
30432
|
+
if (!prevIdent) continue;
|
|
30433
|
+
const simIdent = minHashSimilarity(sigIdent, prevIdent);
|
|
30434
|
+
if (simIdent <= 0) continue;
|
|
30435
|
+
const score = 2 * simStruct * simIdent / (simStruct + simIdent + 1e-9);
|
|
30436
|
+
if (score >= ctx.verifyThreshold) {
|
|
30437
|
+
matched.push({ path: prevPath, score, simStruct, simIdent });
|
|
30438
|
+
}
|
|
30439
|
+
}
|
|
30440
|
+
if (matched.length >= ctx.minHits) {
|
|
30441
|
+
matched.sort((a, b) => b.score - a.score);
|
|
30442
|
+
const best = matched[0];
|
|
30443
|
+
issues.push({
|
|
30444
|
+
ruleId: "dup/structural-clone",
|
|
30445
|
+
category: "logic",
|
|
30446
|
+
severity: "medium",
|
|
30447
|
+
aiSpecific: true,
|
|
30448
|
+
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)}%)`,
|
|
30449
|
+
line: 1,
|
|
30450
|
+
column: 1,
|
|
30451
|
+
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.)",
|
|
30452
|
+
extras: {
|
|
30453
|
+
structuralDuplicateOf: {
|
|
30454
|
+
file: best.path,
|
|
30455
|
+
score: best.score
|
|
30456
|
+
}
|
|
30457
|
+
}
|
|
30458
|
+
});
|
|
30459
|
+
}
|
|
30460
|
+
structuralCache.set(filePath, sigStruct);
|
|
30461
|
+
identifierCache.set(filePath, sigIdent);
|
|
30462
|
+
return issues;
|
|
30463
|
+
}
|
|
30464
|
+
});
|
|
30465
|
+
}
|
|
30466
|
+
});
|
|
30467
|
+
|
|
29648
30468
|
// src/rules/go/error-wrap-without-context.ts
|
|
29649
30469
|
var ERR_WRAP_REGEX, GENERIC_PREFIX_REGEX, goErrorWrapWithoutContextRule;
|
|
29650
30470
|
var init_error_wrap_without_context = __esm({
|
|
@@ -30114,6 +30934,264 @@ var init_system_out_println = __esm({
|
|
|
30114
30934
|
}
|
|
30115
30935
|
});
|
|
30116
30936
|
|
|
30937
|
+
// src/rules/kotlin/coroutine-global-scope.ts
|
|
30938
|
+
var GLOBAL_SCOPE_REGEX, kotlinCoroutineGlobalScopeRule;
|
|
30939
|
+
var init_coroutine_global_scope = __esm({
|
|
30940
|
+
"src/rules/kotlin/coroutine-global-scope.ts"() {
|
|
30941
|
+
"use strict";
|
|
30942
|
+
init_rule();
|
|
30943
|
+
GLOBAL_SCOPE_REGEX = /GlobalScope\s*\.\s*(?:launch|async|runBlocking)\s*[({]/g;
|
|
30944
|
+
kotlinCoroutineGlobalScopeRule = createRule({
|
|
30945
|
+
id: "kotlin/coroutine-global-scope",
|
|
30946
|
+
category: "logic",
|
|
30947
|
+
severity: "medium",
|
|
30948
|
+
aiSpecific: true,
|
|
30949
|
+
description: "GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency. Use viewModelScope / lifecycleScope / coroutineScope {}.",
|
|
30950
|
+
create(_context) {
|
|
30951
|
+
return {};
|
|
30952
|
+
},
|
|
30953
|
+
analyze(_context, facts) {
|
|
30954
|
+
const issues = [];
|
|
30955
|
+
const source = facts.v2?._source;
|
|
30956
|
+
if (!source) return issues;
|
|
30957
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
30958
|
+
let m;
|
|
30959
|
+
GLOBAL_SCOPE_REGEX.lastIndex = 0;
|
|
30960
|
+
while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
|
|
30961
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
30962
|
+
issues.push({
|
|
30963
|
+
ruleId: "kotlin/coroutine-global-scope",
|
|
30964
|
+
category: "logic",
|
|
30965
|
+
severity: "medium",
|
|
30966
|
+
aiSpecific: true,
|
|
30967
|
+
message: `GlobalScope.${m[0].split(".")[1]?.split("(")[0]}() at line ${line} \u2014 bypasses structured concurrency`,
|
|
30968
|
+
line,
|
|
30969
|
+
column: 1,
|
|
30970
|
+
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.'
|
|
30971
|
+
});
|
|
30972
|
+
}
|
|
30973
|
+
return issues;
|
|
30974
|
+
}
|
|
30975
|
+
});
|
|
30976
|
+
}
|
|
30977
|
+
});
|
|
30978
|
+
|
|
30979
|
+
// src/rules/kotlin/data-class-defaults-overuse.ts
|
|
30980
|
+
var DATA_CLASS_HEAD_REGEX, DEFAULT_PARAM_REGEX, DEFAULT_MIN_DEFAULTS, kotlinDataClassDefaultsOveruseRule;
|
|
30981
|
+
var init_data_class_defaults_overuse = __esm({
|
|
30982
|
+
"src/rules/kotlin/data-class-defaults-overuse.ts"() {
|
|
30983
|
+
"use strict";
|
|
30984
|
+
init_rule();
|
|
30985
|
+
DATA_CLASS_HEAD_REGEX = /\bdata\s+class\s+\w+\s*\(/g;
|
|
30986
|
+
DEFAULT_PARAM_REGEX = /=\s*(?:null|true|false|"[^"]*"|'[^']*'|[+-]?\d+(?:\.\d+)?\w*|emptyList\(\)|emptyMap\(\)|emptySet\(\)|listOf\(\)|mapOf\(\)|setOf\(\))/g;
|
|
30987
|
+
DEFAULT_MIN_DEFAULTS = 3;
|
|
30988
|
+
kotlinDataClassDefaultsOveruseRule = createRule({
|
|
30989
|
+
id: "kotlin/data-class-defaults-overuse",
|
|
30990
|
+
category: "typo",
|
|
30991
|
+
severity: "low",
|
|
30992
|
+
aiSpecific: true,
|
|
30993
|
+
description: "data class with 3+ literal defaults \u2014 AI defaults every field rather than only the genuinely optional ones",
|
|
30994
|
+
create(_context) {
|
|
30995
|
+
return { minDefaults: DEFAULT_MIN_DEFAULTS };
|
|
30996
|
+
},
|
|
30997
|
+
analyze(context, facts) {
|
|
30998
|
+
const issues = [];
|
|
30999
|
+
const source = facts.v2?._source;
|
|
31000
|
+
if (!source) return issues;
|
|
31001
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31002
|
+
let m;
|
|
31003
|
+
DATA_CLASS_HEAD_REGEX.lastIndex = 0;
|
|
31004
|
+
while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
|
|
31005
|
+
const headStart = m.index;
|
|
31006
|
+
const openParen = source.indexOf("(", headStart);
|
|
31007
|
+
if (openParen === -1) continue;
|
|
31008
|
+
let depth = 1;
|
|
31009
|
+
let i = openParen + 1;
|
|
31010
|
+
while (i < source.length && depth > 0) {
|
|
31011
|
+
const ch = source.charAt(i);
|
|
31012
|
+
if (ch === "(") depth++;
|
|
31013
|
+
else if (ch === ")") depth--;
|
|
31014
|
+
i++;
|
|
31015
|
+
}
|
|
31016
|
+
if (depth !== 0) continue;
|
|
31017
|
+
const paramBlock = source.slice(openParen + 1, i - 1);
|
|
31018
|
+
DEFAULT_PARAM_REGEX.lastIndex = 0;
|
|
31019
|
+
let defaults = 0;
|
|
31020
|
+
let dm;
|
|
31021
|
+
while ((dm = DEFAULT_PARAM_REGEX.exec(paramBlock)) !== null) {
|
|
31022
|
+
defaults++;
|
|
31023
|
+
}
|
|
31024
|
+
if (defaults < context.minDefaults) continue;
|
|
31025
|
+
const line = source.slice(0, headStart).split("\n").length;
|
|
31026
|
+
issues.push({
|
|
31027
|
+
ruleId: "kotlin/data-class-defaults-overuse",
|
|
31028
|
+
category: "typo",
|
|
31029
|
+
severity: "low",
|
|
31030
|
+
aiSpecific: true,
|
|
31031
|
+
message: `data class with ${defaults} literal defaults at line ${line} \u2014 only optional fields should have defaults`,
|
|
31032
|
+
line,
|
|
31033
|
+
column: 1,
|
|
31034
|
+
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."
|
|
31035
|
+
});
|
|
31036
|
+
}
|
|
31037
|
+
return issues;
|
|
31038
|
+
}
|
|
31039
|
+
});
|
|
31040
|
+
}
|
|
31041
|
+
});
|
|
31042
|
+
|
|
31043
|
+
// src/rules/kotlin/object-singleton-misuse.ts
|
|
31044
|
+
var OBJECT_DECL_REGEX, kotlinObjectSingletonMisuseRule;
|
|
31045
|
+
var init_object_singleton_misuse = __esm({
|
|
31046
|
+
"src/rules/kotlin/object-singleton-misuse.ts"() {
|
|
31047
|
+
"use strict";
|
|
31048
|
+
init_rule();
|
|
31049
|
+
OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
|
|
31050
|
+
kotlinObjectSingletonMisuseRule = createRule({
|
|
31051
|
+
id: "kotlin/object-singleton-misuse",
|
|
31052
|
+
category: "logic",
|
|
31053
|
+
severity: "medium",
|
|
31054
|
+
aiSpecific: true,
|
|
31055
|
+
description: "Kotlin object singleton holding mutable `var` state \u2014 hidden shared state. Use DI or a repository.",
|
|
31056
|
+
create(_context) {
|
|
31057
|
+
return {};
|
|
31058
|
+
},
|
|
31059
|
+
analyze(_context, facts) {
|
|
31060
|
+
const issues = [];
|
|
31061
|
+
const source = facts.v2?._source;
|
|
31062
|
+
if (!source) return issues;
|
|
31063
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31064
|
+
let m;
|
|
31065
|
+
OBJECT_DECL_REGEX.lastIndex = 0;
|
|
31066
|
+
while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
|
|
31067
|
+
const declStart = m.index;
|
|
31068
|
+
const openBrace = source.indexOf("{", declStart);
|
|
31069
|
+
if (openBrace === -1) continue;
|
|
31070
|
+
let depth = 1;
|
|
31071
|
+
let i = openBrace + 1;
|
|
31072
|
+
while (i < source.length && depth > 0) {
|
|
31073
|
+
const ch = source.charAt(i);
|
|
31074
|
+
if (ch === "{") depth++;
|
|
31075
|
+
else if (ch === "}") depth--;
|
|
31076
|
+
i++;
|
|
31077
|
+
}
|
|
31078
|
+
if (depth !== 0) continue;
|
|
31079
|
+
const body = source.slice(openBrace + 1, i - 1);
|
|
31080
|
+
if (!/\bvar\s+\w+\s*[:=]/m.test(body)) continue;
|
|
31081
|
+
const line = source.slice(0, declStart).split("\n").length;
|
|
31082
|
+
const name = m[1] ?? "<unknown>";
|
|
31083
|
+
issues.push({
|
|
31084
|
+
ruleId: "kotlin/object-singleton-misuse",
|
|
31085
|
+
category: "logic",
|
|
31086
|
+
severity: "medium",
|
|
31087
|
+
aiSpecific: true,
|
|
31088
|
+
message: `object ${name} at line ${line} holds mutable state \u2014 singleton + var = hidden shared state`,
|
|
31089
|
+
line,
|
|
31090
|
+
column: 1,
|
|
31091
|
+
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."
|
|
31092
|
+
});
|
|
31093
|
+
}
|
|
31094
|
+
return issues;
|
|
31095
|
+
}
|
|
31096
|
+
});
|
|
31097
|
+
}
|
|
31098
|
+
});
|
|
31099
|
+
|
|
31100
|
+
// src/rules/kotlin/println-debug.ts
|
|
31101
|
+
var PRINTLN_REGEX2, DEFAULT_THRESHOLD2, kotlinPrintlnDebugRule;
|
|
31102
|
+
var init_println_debug = __esm({
|
|
31103
|
+
"src/rules/kotlin/println-debug.ts"() {
|
|
31104
|
+
"use strict";
|
|
31105
|
+
init_rule();
|
|
31106
|
+
PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
|
|
31107
|
+
DEFAULT_THRESHOLD2 = 1;
|
|
31108
|
+
kotlinPrintlnDebugRule = createRule({
|
|
31109
|
+
id: "kotlin/println-debug",
|
|
31110
|
+
category: "typo",
|
|
31111
|
+
severity: "low",
|
|
31112
|
+
aiSpecific: true,
|
|
31113
|
+
description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
|
|
31114
|
+
create(_context) {
|
|
31115
|
+
return { threshold: DEFAULT_THRESHOLD2 };
|
|
31116
|
+
},
|
|
31117
|
+
analyze(context, facts) {
|
|
31118
|
+
const issues = [];
|
|
31119
|
+
const source = facts.v2?._source;
|
|
31120
|
+
if (!source) return issues;
|
|
31121
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31122
|
+
const matches = [];
|
|
31123
|
+
let m;
|
|
31124
|
+
PRINTLN_REGEX2.lastIndex = 0;
|
|
31125
|
+
while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
|
|
31126
|
+
matches.push(m.index);
|
|
31127
|
+
}
|
|
31128
|
+
if (matches.length <= context.threshold) return issues;
|
|
31129
|
+
const cap = Math.min(matches.length, 10);
|
|
31130
|
+
for (let i = 0; i < cap; i++) {
|
|
31131
|
+
const idx = matches[i];
|
|
31132
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
31133
|
+
issues.push({
|
|
31134
|
+
ruleId: "kotlin/println-debug",
|
|
31135
|
+
category: "typo",
|
|
31136
|
+
severity: "low",
|
|
31137
|
+
aiSpecific: true,
|
|
31138
|
+
message: `println(...) at line ${line} \u2014 use Timber or android.util.Log for production output`,
|
|
31139
|
+
line,
|
|
31140
|
+
column: 1,
|
|
31141
|
+
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."
|
|
31142
|
+
});
|
|
31143
|
+
}
|
|
31144
|
+
return issues;
|
|
31145
|
+
}
|
|
31146
|
+
});
|
|
31147
|
+
}
|
|
31148
|
+
});
|
|
31149
|
+
|
|
31150
|
+
// src/rules/kotlin/string-concat-loop.ts
|
|
31151
|
+
var STRING_CONCAT_REGEX2, kotlinStringConcatLoopRule;
|
|
31152
|
+
var init_string_concat_loop2 = __esm({
|
|
31153
|
+
"src/rules/kotlin/string-concat-loop.ts"() {
|
|
31154
|
+
"use strict";
|
|
31155
|
+
init_rule();
|
|
31156
|
+
STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
|
|
31157
|
+
kotlinStringConcatLoopRule = createRule({
|
|
31158
|
+
id: "kotlin/string-concat-loop",
|
|
31159
|
+
category: "perf",
|
|
31160
|
+
severity: "low",
|
|
31161
|
+
aiSpecific: true,
|
|
31162
|
+
description: "String concatenation in a loop \u2014 use buildString { ... } or a StringBuilder",
|
|
31163
|
+
create(_context) {
|
|
31164
|
+
return {};
|
|
31165
|
+
},
|
|
31166
|
+
analyze(_context, facts) {
|
|
31167
|
+
const issues = [];
|
|
31168
|
+
const source = facts.v2?._source;
|
|
31169
|
+
if (!source) return issues;
|
|
31170
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31171
|
+
if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
|
|
31172
|
+
let m;
|
|
31173
|
+
STRING_CONCAT_REGEX2.lastIndex = 0;
|
|
31174
|
+
while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
|
|
31175
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
31176
|
+
const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
|
|
31177
|
+
if (/\.append\s*\(/.test(lineText)) continue;
|
|
31178
|
+
issues.push({
|
|
31179
|
+
ruleId: "kotlin/string-concat-loop",
|
|
31180
|
+
category: "perf",
|
|
31181
|
+
severity: "low",
|
|
31182
|
+
aiSpecific: true,
|
|
31183
|
+
message: `String concatenation in a loop at line ${line} \u2014 use buildString { ... }`,
|
|
31184
|
+
line,
|
|
31185
|
+
column: 1,
|
|
31186
|
+
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."
|
|
31187
|
+
});
|
|
31188
|
+
}
|
|
31189
|
+
return issues;
|
|
31190
|
+
}
|
|
31191
|
+
});
|
|
31192
|
+
}
|
|
31193
|
+
});
|
|
31194
|
+
|
|
30117
31195
|
// src/rules/layout/gap-monopoly.ts
|
|
30118
31196
|
var GAP_RE, gapMonopolyRule;
|
|
30119
31197
|
var init_gap_monopoly = __esm({
|
|
@@ -35007,12 +36085,12 @@ __export(dist_exports2, {
|
|
|
35007
36085
|
tokenizeIdentifiers: () => tokenizeIdentifiers
|
|
35008
36086
|
});
|
|
35009
36087
|
import { readFile, writeFile, mkdir, access } from "fs/promises";
|
|
35010
|
-
import { createHash as
|
|
36088
|
+
import { createHash as createHash5 } from "crypto";
|
|
35011
36089
|
import { join as join8, dirname as dirname5 } from "path";
|
|
35012
36090
|
import { parseSync } from "@swc/core";
|
|
35013
36091
|
import { join as join22 } from "path";
|
|
35014
36092
|
import { createHash as createHash22 } from "crypto";
|
|
35015
|
-
import { createHash as
|
|
36093
|
+
import { createHash as createHash32 } from "crypto";
|
|
35016
36094
|
import { readFile as readFile2 } from "fs/promises";
|
|
35017
36095
|
import { relative as relative2 } from "path";
|
|
35018
36096
|
function computeLikelihoodRatios(ruleIds, signalData, corpus) {
|
|
@@ -35265,7 +36343,7 @@ function legacyCacheRoot() {
|
|
|
35265
36343
|
return join8(process.cwd(), ".slopbrick", "cache", "ast");
|
|
35266
36344
|
}
|
|
35267
36345
|
function hashContent(content) {
|
|
35268
|
-
return
|
|
36346
|
+
return createHash5("md5").update(content, "utf-8").digest("hex");
|
|
35269
36347
|
}
|
|
35270
36348
|
function cachePathWithRoot(content, root) {
|
|
35271
36349
|
return join8(root, `${hashContent(content)}.json`);
|
|
@@ -35319,6 +36397,7 @@ function parseSource(source, filePath) {
|
|
|
35319
36397
|
case "py":
|
|
35320
36398
|
case "go":
|
|
35321
36399
|
case "rs":
|
|
36400
|
+
case "java":
|
|
35322
36401
|
return parseBlankModule(source);
|
|
35323
36402
|
default:
|
|
35324
36403
|
return parseWithSwc(source, filePath);
|
|
@@ -35783,7 +36862,7 @@ function fingerprintSignature(sig) {
|
|
|
35783
36862
|
const sortedProps = [...sig.props].sort();
|
|
35784
36863
|
const sortedParams = [...sig.params].sort();
|
|
35785
36864
|
const payload = `${sortedHooks.join(",")}|${sortedProps.join(",")}|${sortedParams.join(",")}`;
|
|
35786
|
-
return
|
|
36865
|
+
return createHash32("sha256").update(payload).digest("hex").slice(0, 16);
|
|
35787
36866
|
}
|
|
35788
36867
|
function signatureSimilarity(a, b) {
|
|
35789
36868
|
const aSet = /* @__PURE__ */ new Set([...a.hooks, ...a.props, ...a.params]);
|
|
@@ -38010,7 +39089,7 @@ var init_css_bloat = __esm({
|
|
|
38010
39089
|
function stripNoise2(source) {
|
|
38011
39090
|
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
|
|
38012
39091
|
}
|
|
38013
|
-
function
|
|
39092
|
+
function tokenize2(source) {
|
|
38014
39093
|
const cleaned = stripNoise2(source);
|
|
38015
39094
|
const operators = [];
|
|
38016
39095
|
const operands = [];
|
|
@@ -38031,7 +39110,7 @@ function tokenize(source) {
|
|
|
38031
39110
|
return { operators, operands };
|
|
38032
39111
|
}
|
|
38033
39112
|
function computeHalstead(source) {
|
|
38034
|
-
const { operators, operands } =
|
|
39113
|
+
const { operators, operands } = tokenize2(source);
|
|
38035
39114
|
const uniqueOperators = new Set(operators);
|
|
38036
39115
|
const uniqueOperands = new Set(operands);
|
|
38037
39116
|
const n1 = uniqueOperators.size;
|
|
@@ -39597,8 +40676,262 @@ var init_unsafe_html_render = __esm({
|
|
|
39597
40676
|
}
|
|
39598
40677
|
});
|
|
39599
40678
|
|
|
40679
|
+
// src/rules/swift/fatal-error-thrown.ts
|
|
40680
|
+
var FATAL_REGEX, swiftFatalErrorThrownRule;
|
|
40681
|
+
var init_fatal_error_thrown = __esm({
|
|
40682
|
+
"src/rules/swift/fatal-error-thrown.ts"() {
|
|
40683
|
+
"use strict";
|
|
40684
|
+
init_rule();
|
|
40685
|
+
FATAL_REGEX = /\b(?:fatalError|preconditionFailure)\s*\(/g;
|
|
40686
|
+
swiftFatalErrorThrownRule = createRule({
|
|
40687
|
+
id: "swift/fatal-error-thrown",
|
|
40688
|
+
category: "logic",
|
|
40689
|
+
severity: "high",
|
|
40690
|
+
aiSpecific: true,
|
|
40691
|
+
description: "fatalError / preconditionFailure in production \u2014 crashes unconditionally. Return an error or a typed default.",
|
|
40692
|
+
create(_context) {
|
|
40693
|
+
return {};
|
|
40694
|
+
},
|
|
40695
|
+
analyze(_context, facts) {
|
|
40696
|
+
const issues = [];
|
|
40697
|
+
const source = facts.v2?._source;
|
|
40698
|
+
if (!source) return issues;
|
|
40699
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40700
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40701
|
+
let m;
|
|
40702
|
+
FATAL_REGEX.lastIndex = 0;
|
|
40703
|
+
while ((m = FATAL_REGEX.exec(source)) !== null) {
|
|
40704
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40705
|
+
const matched = m[0].replace(/\s*\($/, "");
|
|
40706
|
+
issues.push({
|
|
40707
|
+
ruleId: "swift/fatal-error-thrown",
|
|
40708
|
+
category: "logic",
|
|
40709
|
+
severity: "high",
|
|
40710
|
+
aiSpecific: true,
|
|
40711
|
+
message: `${matched}() at line ${line} \u2014 crashes unconditionally in release builds`,
|
|
40712
|
+
line,
|
|
40713
|
+
column: 1,
|
|
40714
|
+
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.'
|
|
40715
|
+
});
|
|
40716
|
+
}
|
|
40717
|
+
return issues;
|
|
40718
|
+
}
|
|
40719
|
+
});
|
|
40720
|
+
}
|
|
40721
|
+
});
|
|
40722
|
+
|
|
40723
|
+
// src/rules/swift/force-unwrap.ts
|
|
40724
|
+
var AS_FORCE_REGEX, ACCESS_FORCE_REGEX, TRY_FORCE_REGEX, swiftForceUnwrapRule;
|
|
40725
|
+
var init_force_unwrap = __esm({
|
|
40726
|
+
"src/rules/swift/force-unwrap.ts"() {
|
|
40727
|
+
"use strict";
|
|
40728
|
+
init_rule();
|
|
40729
|
+
AS_FORCE_REGEX = /\bas!\s+/g;
|
|
40730
|
+
ACCESS_FORCE_REGEX = /(?:\w|\])\!\s*(?:\.|\(|;|,|\s*$)/gm;
|
|
40731
|
+
TRY_FORCE_REGEX = /\btry!\s+/g;
|
|
40732
|
+
swiftForceUnwrapRule = createRule({
|
|
40733
|
+
id: "swift/force-unwrap",
|
|
40734
|
+
category: "logic",
|
|
40735
|
+
severity: "medium",
|
|
40736
|
+
aiSpecific: true,
|
|
40737
|
+
description: "Swift force-unwrap (as!, var!.prop, try!) \u2014 silently crashes if the value is nil. Use as? / guard let / try?.",
|
|
40738
|
+
create(_context) {
|
|
40739
|
+
return {};
|
|
40740
|
+
},
|
|
40741
|
+
analyze(_context, facts) {
|
|
40742
|
+
const issues = [];
|
|
40743
|
+
const source = facts.v2?._source;
|
|
40744
|
+
if (!source) return issues;
|
|
40745
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40746
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40747
|
+
const emit = (idx, label) => {
|
|
40748
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40749
|
+
issues.push({
|
|
40750
|
+
ruleId: "swift/force-unwrap",
|
|
40751
|
+
category: "logic",
|
|
40752
|
+
severity: "medium",
|
|
40753
|
+
aiSpecific: true,
|
|
40754
|
+
message: `force-unwrap (${label}) at line ${line} \u2014 crashes if the value is nil`,
|
|
40755
|
+
line,
|
|
40756
|
+
column: 1,
|
|
40757
|
+
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.'
|
|
40758
|
+
});
|
|
40759
|
+
};
|
|
40760
|
+
let m;
|
|
40761
|
+
AS_FORCE_REGEX.lastIndex = 0;
|
|
40762
|
+
while ((m = AS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "as!");
|
|
40763
|
+
TRY_FORCE_REGEX.lastIndex = 0;
|
|
40764
|
+
while ((m = TRY_FORCE_REGEX.exec(source)) !== null) emit(m.index, "try!");
|
|
40765
|
+
ACCESS_FORCE_REGEX.lastIndex = 0;
|
|
40766
|
+
while ((m = ACCESS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "!.");
|
|
40767
|
+
return issues;
|
|
40768
|
+
}
|
|
40769
|
+
});
|
|
40770
|
+
}
|
|
40771
|
+
});
|
|
40772
|
+
|
|
40773
|
+
// src/rules/swift/implicitly-unwrapped-optional.ts
|
|
40774
|
+
var IUO_DECL_REGEX, swiftImplicitlyUnwrappedOptionalRule;
|
|
40775
|
+
var init_implicitly_unwrapped_optional = __esm({
|
|
40776
|
+
"src/rules/swift/implicitly-unwrapped-optional.ts"() {
|
|
40777
|
+
"use strict";
|
|
40778
|
+
init_rule();
|
|
40779
|
+
IUO_DECL_REGEX = /\b(?:var|let)\s+\w+\s*:\s*[A-Za-z_][\w<>?,\.\s]*!\s*[=;\n]/g;
|
|
40780
|
+
swiftImplicitlyUnwrappedOptionalRule = createRule({
|
|
40781
|
+
id: "swift/implicitly-unwrapped-optional",
|
|
40782
|
+
category: "typo",
|
|
40783
|
+
severity: "low",
|
|
40784
|
+
aiSpecific: true,
|
|
40785
|
+
description: "Implicitly unwrapped optional (var x: Type!) \u2014 declare as Type? and unwrap with guard let.",
|
|
40786
|
+
create(_context) {
|
|
40787
|
+
return {};
|
|
40788
|
+
},
|
|
40789
|
+
analyze(_context, facts) {
|
|
40790
|
+
const issues = [];
|
|
40791
|
+
const source = facts.v2?._source;
|
|
40792
|
+
if (!source) return issues;
|
|
40793
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40794
|
+
let m;
|
|
40795
|
+
IUO_DECL_REGEX.lastIndex = 0;
|
|
40796
|
+
while ((m = IUO_DECL_REGEX.exec(source)) !== null) {
|
|
40797
|
+
const captured = m[0];
|
|
40798
|
+
const bangAt = captured.indexOf("!", captured.indexOf(":"));
|
|
40799
|
+
const between = captured.slice(captured.indexOf(":"), bangAt);
|
|
40800
|
+
if (between.includes("?")) continue;
|
|
40801
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40802
|
+
issues.push({
|
|
40803
|
+
ruleId: "swift/implicitly-unwrapped-optional",
|
|
40804
|
+
category: "typo",
|
|
40805
|
+
severity: "low",
|
|
40806
|
+
aiSpecific: true,
|
|
40807
|
+
message: `implicitly unwrapped optional at line ${line} \u2014 declare as Type? and unwrap explicitly`,
|
|
40808
|
+
line,
|
|
40809
|
+
column: 1,
|
|
40810
|
+
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.'
|
|
40811
|
+
});
|
|
40812
|
+
}
|
|
40813
|
+
return issues;
|
|
40814
|
+
}
|
|
40815
|
+
});
|
|
40816
|
+
}
|
|
40817
|
+
});
|
|
40818
|
+
|
|
40819
|
+
// src/rules/swift/print-debug.ts
|
|
40820
|
+
var PRINT_REGEX, DEFAULT_THRESHOLD3, swiftPrintDebugRule;
|
|
40821
|
+
var init_print_debug = __esm({
|
|
40822
|
+
"src/rules/swift/print-debug.ts"() {
|
|
40823
|
+
"use strict";
|
|
40824
|
+
init_rule();
|
|
40825
|
+
PRINT_REGEX = /\bprint\s*\(/g;
|
|
40826
|
+
DEFAULT_THRESHOLD3 = 1;
|
|
40827
|
+
swiftPrintDebugRule = createRule({
|
|
40828
|
+
id: "swift/print-debug",
|
|
40829
|
+
category: "typo",
|
|
40830
|
+
severity: "low",
|
|
40831
|
+
aiSpecific: true,
|
|
40832
|
+
description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
|
|
40833
|
+
create(_context) {
|
|
40834
|
+
return { threshold: DEFAULT_THRESHOLD3 };
|
|
40835
|
+
},
|
|
40836
|
+
analyze(context, facts) {
|
|
40837
|
+
const issues = [];
|
|
40838
|
+
const source = facts.v2?._source;
|
|
40839
|
+
if (!source) return issues;
|
|
40840
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40841
|
+
const matches = [];
|
|
40842
|
+
let m;
|
|
40843
|
+
PRINT_REGEX.lastIndex = 0;
|
|
40844
|
+
while ((m = PRINT_REGEX.exec(source)) !== null) {
|
|
40845
|
+
matches.push(m.index);
|
|
40846
|
+
}
|
|
40847
|
+
if (matches.length <= context.threshold) return issues;
|
|
40848
|
+
const cap = Math.min(matches.length, 10);
|
|
40849
|
+
for (let i = 0; i < cap; i++) {
|
|
40850
|
+
const idx = matches[i];
|
|
40851
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40852
|
+
issues.push({
|
|
40853
|
+
ruleId: "swift/print-debug",
|
|
40854
|
+
category: "typo",
|
|
40855
|
+
severity: "low",
|
|
40856
|
+
aiSpecific: true,
|
|
40857
|
+
message: `print(...) at line ${line} \u2014 use Logger (os.log) for production output`,
|
|
40858
|
+
line,
|
|
40859
|
+
column: 1,
|
|
40860
|
+
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.'
|
|
40861
|
+
});
|
|
40862
|
+
}
|
|
40863
|
+
return issues;
|
|
40864
|
+
}
|
|
40865
|
+
});
|
|
40866
|
+
}
|
|
40867
|
+
});
|
|
40868
|
+
|
|
40869
|
+
// src/rules/swift/strong-self-capture.ts
|
|
40870
|
+
var SELF_REF_REGEX, CAPTURE_LIST_REGEX, swiftStrongSelfCaptureRule;
|
|
40871
|
+
var init_strong_self_capture = __esm({
|
|
40872
|
+
"src/rules/swift/strong-self-capture.ts"() {
|
|
40873
|
+
"use strict";
|
|
40874
|
+
init_rule();
|
|
40875
|
+
SELF_REF_REGEX = /\bself\.(?:\w+)/g;
|
|
40876
|
+
CAPTURE_LIST_REGEX = /\[\s*(?:weak|unowned)\s+self\b/;
|
|
40877
|
+
swiftStrongSelfCaptureRule = createRule({
|
|
40878
|
+
id: "swift/strong-self-capture",
|
|
40879
|
+
category: "logic",
|
|
40880
|
+
severity: "medium",
|
|
40881
|
+
aiSpecific: true,
|
|
40882
|
+
description: "Closure captures `self` strongly \u2014 creates a retain cycle. Use `[weak self]` or `[unowned self]`.",
|
|
40883
|
+
create(_context) {
|
|
40884
|
+
return {};
|
|
40885
|
+
},
|
|
40886
|
+
analyze(_context, facts) {
|
|
40887
|
+
const issues = [];
|
|
40888
|
+
const source = facts.v2?._source;
|
|
40889
|
+
if (!source) return issues;
|
|
40890
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40891
|
+
const METHOD_HEAD_REGEX = /\b(?:func|init\??|deinit|subscript)\b[^;{}\n]*$/m;
|
|
40892
|
+
let m;
|
|
40893
|
+
SELF_REF_REGEX.lastIndex = 0;
|
|
40894
|
+
while ((m = SELF_REF_REGEX.exec(source)) !== null) {
|
|
40895
|
+
const selfIdx = m.index;
|
|
40896
|
+
let depth = 0;
|
|
40897
|
+
let openBrace = -1;
|
|
40898
|
+
for (let j = selfIdx - 1; j >= 0; j--) {
|
|
40899
|
+
const ch = source.charAt(j);
|
|
40900
|
+
if (ch === "}") depth++;
|
|
40901
|
+
else if (ch === "{") {
|
|
40902
|
+
if (depth === 0) {
|
|
40903
|
+
openBrace = j;
|
|
40904
|
+
break;
|
|
40905
|
+
}
|
|
40906
|
+
depth--;
|
|
40907
|
+
}
|
|
40908
|
+
}
|
|
40909
|
+
if (openBrace === -1) continue;
|
|
40910
|
+
const openLineStart = source.lastIndexOf("\n", openBrace) + 1;
|
|
40911
|
+
const lineBeforeBrace = source.slice(openLineStart, openBrace);
|
|
40912
|
+
if (METHOD_HEAD_REGEX.test(lineBeforeBrace)) continue;
|
|
40913
|
+
const openingSlice = source.slice(openBrace, openBrace + 80);
|
|
40914
|
+
if (CAPTURE_LIST_REGEX.test(openingSlice)) continue;
|
|
40915
|
+
const line = source.slice(0, openBrace).split("\n").length;
|
|
40916
|
+
issues.push({
|
|
40917
|
+
ruleId: "swift/strong-self-capture",
|
|
40918
|
+
category: "logic",
|
|
40919
|
+
severity: "medium",
|
|
40920
|
+
aiSpecific: true,
|
|
40921
|
+
message: `closure at line ${line} captures self strongly \u2014 use [weak self] or [unowned self]`,
|
|
40922
|
+
line,
|
|
40923
|
+
column: 1,
|
|
40924
|
+
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.'
|
|
40925
|
+
});
|
|
40926
|
+
}
|
|
40927
|
+
return issues;
|
|
40928
|
+
}
|
|
40929
|
+
});
|
|
40930
|
+
}
|
|
40931
|
+
});
|
|
40932
|
+
|
|
39600
40933
|
// src/rules/test/utils.ts
|
|
39601
|
-
import { createHash as
|
|
40934
|
+
import { createHash as createHash6 } from "crypto";
|
|
39602
40935
|
function isTestFile(filePath) {
|
|
39603
40936
|
const normalized = filePath.replace(/\\/g, "/");
|
|
39604
40937
|
if (/(^|\/)__tests__\//.test(normalized)) return true;
|
|
@@ -39734,7 +41067,7 @@ function extractSetupBlocks(source, minLines = 3) {
|
|
|
39734
41067
|
}).trim();
|
|
39735
41068
|
const lineCount = rawBody.split("\n").length;
|
|
39736
41069
|
if (lineCount < minLines) continue;
|
|
39737
|
-
const bodyHash =
|
|
41070
|
+
const bodyHash = createHash6("sha1").update(normalizedBody).digest("hex");
|
|
39738
41071
|
const { line, column } = lineColumnOf(source, m.index);
|
|
39739
41072
|
hits.push({
|
|
39740
41073
|
kind,
|
|
@@ -45223,6 +46556,11 @@ var init_builtins = __esm({
|
|
|
45223
46556
|
init_multiple_components_per_file();
|
|
45224
46557
|
init_shadcn_prop_mismatch();
|
|
45225
46558
|
init_import_path_mismatch();
|
|
46559
|
+
init_c_style_cast();
|
|
46560
|
+
init_magic_numbers();
|
|
46561
|
+
init_printf_debug();
|
|
46562
|
+
init_raw_new_delete();
|
|
46563
|
+
init_using_namespace_std();
|
|
45226
46564
|
init_duplicate_index();
|
|
45227
46565
|
init_enum_sprawl();
|
|
45228
46566
|
init_missing_fk_index();
|
|
@@ -45238,6 +46576,8 @@ var init_builtins = __esm({
|
|
|
45238
46576
|
init_stale_function_reference();
|
|
45239
46577
|
init_stale_package_reference();
|
|
45240
46578
|
init_identical_block();
|
|
46579
|
+
init_near_duplicate();
|
|
46580
|
+
init_structural_clone2();
|
|
45241
46581
|
init_error_wrap_without_context();
|
|
45242
46582
|
init_nil_slice_vs_empty();
|
|
45243
46583
|
init_struct_tag_inconsistency();
|
|
@@ -45247,6 +46587,11 @@ var init_builtins = __esm({
|
|
|
45247
46587
|
init_raw_type_overuse();
|
|
45248
46588
|
init_string_concat_loop();
|
|
45249
46589
|
init_system_out_println();
|
|
46590
|
+
init_coroutine_global_scope();
|
|
46591
|
+
init_data_class_defaults_overuse();
|
|
46592
|
+
init_object_singleton_misuse();
|
|
46593
|
+
init_println_debug();
|
|
46594
|
+
init_string_concat_loop2();
|
|
45250
46595
|
init_gap_monopoly();
|
|
45251
46596
|
init_math_element_uniformity();
|
|
45252
46597
|
init_math_grid_uniformity();
|
|
@@ -45285,6 +46630,11 @@ var init_builtins = __esm({
|
|
|
45285
46630
|
init_sql_construction();
|
|
45286
46631
|
init_target_blank_no_noopener();
|
|
45287
46632
|
init_unsafe_html_render();
|
|
46633
|
+
init_fatal_error_thrown();
|
|
46634
|
+
init_force_unwrap();
|
|
46635
|
+
init_implicitly_unwrapped_optional();
|
|
46636
|
+
init_print_debug();
|
|
46637
|
+
init_strong_self_capture();
|
|
45288
46638
|
init_duplicate_setup();
|
|
45289
46639
|
init_fake_placeholder();
|
|
45290
46640
|
init_missing_edge_case();
|
|
@@ -45341,6 +46691,11 @@ var init_builtins = __esm({
|
|
|
45341
46691
|
multipleComponentsPerFileRule,
|
|
45342
46692
|
shadcnPropMismatchRule,
|
|
45343
46693
|
importPathMismatchRule,
|
|
46694
|
+
cppCStyleCastRule,
|
|
46695
|
+
cppMagicNumbersRule,
|
|
46696
|
+
cppPrintfDebugRule,
|
|
46697
|
+
cppRawNewDeleteRule,
|
|
46698
|
+
cppUsingNamespaceStdRule,
|
|
45344
46699
|
duplicateIndexRule,
|
|
45345
46700
|
enumSprawlRule,
|
|
45346
46701
|
missingFkIndexRule,
|
|
@@ -45356,6 +46711,8 @@ var init_builtins = __esm({
|
|
|
45356
46711
|
staleFunctionReferenceRule,
|
|
45357
46712
|
stalePackageReferenceRule,
|
|
45358
46713
|
dupIdenticalBlockRule,
|
|
46714
|
+
nearDuplicateRule,
|
|
46715
|
+
structuralCloneRule,
|
|
45359
46716
|
goErrorWrapWithoutContextRule,
|
|
45360
46717
|
goNilSliceVsEmptyRule,
|
|
45361
46718
|
goStructTagInconsistencyRule,
|
|
@@ -45365,6 +46722,11 @@ var init_builtins = __esm({
|
|
|
45365
46722
|
javaRawTypeOveruseRule,
|
|
45366
46723
|
javaStringConcatLoopRule,
|
|
45367
46724
|
javaSystemOutPrintlnRule,
|
|
46725
|
+
kotlinCoroutineGlobalScopeRule,
|
|
46726
|
+
kotlinDataClassDefaultsOveruseRule,
|
|
46727
|
+
kotlinObjectSingletonMisuseRule,
|
|
46728
|
+
kotlinPrintlnDebugRule,
|
|
46729
|
+
kotlinStringConcatLoopRule,
|
|
45368
46730
|
gapMonopolyRule,
|
|
45369
46731
|
mathElementUniformityRule,
|
|
45370
46732
|
mathGridUniformityRule,
|
|
@@ -45403,6 +46765,11 @@ var init_builtins = __esm({
|
|
|
45403
46765
|
sqlConstructionRule,
|
|
45404
46766
|
targetBlankNoNoopenerRule,
|
|
45405
46767
|
unsafeHtmlRenderRule,
|
|
46768
|
+
swiftFatalErrorThrownRule,
|
|
46769
|
+
swiftForceUnwrapRule,
|
|
46770
|
+
swiftImplicitlyUnwrappedOptionalRule,
|
|
46771
|
+
swiftPrintDebugRule,
|
|
46772
|
+
swiftStrongSelfCaptureRule,
|
|
45406
46773
|
duplicateSetupRule,
|
|
45407
46774
|
fakePlaceholderRule,
|
|
45408
46775
|
missingEdgeCaseRule,
|
|
@@ -45461,7 +46828,7 @@ function tokenizeClassLike(source) {
|
|
|
45461
46828
|
if (current.length > 0) tokens.push(current);
|
|
45462
46829
|
return tokens;
|
|
45463
46830
|
}
|
|
45464
|
-
function
|
|
46831
|
+
function stripComments3(source) {
|
|
45465
46832
|
let out = source.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
45466
46833
|
out = out.replace(/(^|\s)\/\/[^\n]*/g, " ");
|
|
45467
46834
|
return out;
|
|
@@ -45473,7 +46840,7 @@ function normalizeCalc(source) {
|
|
|
45473
46840
|
});
|
|
45474
46841
|
}
|
|
45475
46842
|
function canonicalizeStyleSource(source) {
|
|
45476
|
-
let normalized =
|
|
46843
|
+
let normalized = stripComments3(source);
|
|
45477
46844
|
normalized = normalizeCalc(normalized);
|
|
45478
46845
|
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
45479
46846
|
if (normalized.includes(" ") && !/[{};]/.test(normalized)) {
|
|
@@ -47459,7 +48826,7 @@ var init_git = __esm({
|
|
|
47459
48826
|
});
|
|
47460
48827
|
|
|
47461
48828
|
// src/engine/cache-incremental.ts
|
|
47462
|
-
import { createHash as
|
|
48829
|
+
import { createHash as createHash7 } from "crypto";
|
|
47463
48830
|
import { existsSync as existsSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync3, renameSync as renameSync2, mkdirSync as mkdirSync2, unlinkSync } from "fs";
|
|
47464
48831
|
import { dirname as dirname8, isAbsolute, resolve as resolve8 } from "path";
|
|
47465
48832
|
function loadCache(cachePath3) {
|
|
@@ -47489,7 +48856,7 @@ function saveCache(cachePath3, cache) {
|
|
|
47489
48856
|
}
|
|
47490
48857
|
function computeFileHash(filePath) {
|
|
47491
48858
|
const content = readFileSync12(filePath);
|
|
47492
|
-
return
|
|
48859
|
+
return createHash7("md5").update(content).digest("hex");
|
|
47493
48860
|
}
|
|
47494
48861
|
function partitionByCache(files, cache) {
|
|
47495
48862
|
const toScan = [];
|
|
@@ -48423,9 +49790,9 @@ var init_signal_strength = __esm({
|
|
|
48423
49790
|
ratio: 1,
|
|
48424
49791
|
precision: 0,
|
|
48425
49792
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48426
|
-
verdict: "
|
|
49793
|
+
verdict: "OK",
|
|
48427
49794
|
defaultOff: false,
|
|
48428
|
-
_calibrationNote: "v0.19: new rule
|
|
49795
|
+
_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
49796
|
aiSpecific: false,
|
|
48430
49797
|
_v7Verdict: "DORMANT",
|
|
48431
49798
|
_v7Lift: 1,
|
|
@@ -48433,8 +49800,39 @@ var init_signal_strength = __esm({
|
|
|
48433
49800
|
_v7FpRate: 0,
|
|
48434
49801
|
_v7Precision: 0,
|
|
48435
49802
|
_v8Verdict: "DORMANT",
|
|
48436
|
-
_v8Lift: 1
|
|
48437
|
-
|
|
49803
|
+
_v8Lift: 1
|
|
49804
|
+
},
|
|
49805
|
+
"dup/near-duplicate": {
|
|
49806
|
+
recall: 0,
|
|
49807
|
+
fpRate: 0,
|
|
49808
|
+
ratio: 1,
|
|
49809
|
+
precision: 0,
|
|
49810
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
49811
|
+
verdict: "DORMANT",
|
|
49812
|
+
defaultOff: true,
|
|
49813
|
+
_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.",
|
|
49814
|
+
aiSpecific: true,
|
|
49815
|
+
_v7Verdict: "DORMANT",
|
|
49816
|
+
_v7Lift: 1,
|
|
49817
|
+
_v7Recall: 0,
|
|
49818
|
+
_v7FpRate: 0,
|
|
49819
|
+
_v7Precision: 0
|
|
49820
|
+
},
|
|
49821
|
+
"dup/structural-clone": {
|
|
49822
|
+
recall: 0,
|
|
49823
|
+
fpRate: 0,
|
|
49824
|
+
ratio: 1,
|
|
49825
|
+
precision: 0,
|
|
49826
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
49827
|
+
verdict: "DORMANT",
|
|
49828
|
+
defaultOff: true,
|
|
49829
|
+
_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.",
|
|
49830
|
+
aiSpecific: true,
|
|
49831
|
+
_v7Verdict: "DORMANT",
|
|
49832
|
+
_v7Lift: 1,
|
|
49833
|
+
_v7Recall: 0,
|
|
49834
|
+
_v7FpRate: 0,
|
|
49835
|
+
_v7Precision: 0
|
|
48438
49836
|
},
|
|
48439
49837
|
"go/error-wrap-without-context": {
|
|
48440
49838
|
recall: 0,
|
|
@@ -49840,6 +51238,276 @@ var init_signal_strength = __esm({
|
|
|
49840
51238
|
_v8Verdict: "DORMANT",
|
|
49841
51239
|
_v8Lift: 1,
|
|
49842
51240
|
defaultOff: true
|
|
51241
|
+
},
|
|
51242
|
+
"kotlin/data-class-defaults-overuse": {
|
|
51243
|
+
recall: 0,
|
|
51244
|
+
fpRate: 0,
|
|
51245
|
+
ratio: 1,
|
|
51246
|
+
precision: 0,
|
|
51247
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51248
|
+
verdict: "DORMANT",
|
|
51249
|
+
_calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
|
|
51250
|
+
aiSpecific: true,
|
|
51251
|
+
_v7Verdict: "DORMANT",
|
|
51252
|
+
_v7Lift: 1,
|
|
51253
|
+
_v7Recall: 0,
|
|
51254
|
+
_v7FpRate: 0,
|
|
51255
|
+
_v7Precision: 0,
|
|
51256
|
+
_v8Verdict: "DORMANT",
|
|
51257
|
+
_v8Lift: 1,
|
|
51258
|
+
defaultOff: true
|
|
51259
|
+
},
|
|
51260
|
+
"kotlin/coroutine-global-scope": {
|
|
51261
|
+
recall: 0,
|
|
51262
|
+
fpRate: 0,
|
|
51263
|
+
ratio: 1,
|
|
51264
|
+
precision: 0,
|
|
51265
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51266
|
+
verdict: "DORMANT",
|
|
51267
|
+
_calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
|
|
51268
|
+
aiSpecific: true,
|
|
51269
|
+
_v7Verdict: "DORMANT",
|
|
51270
|
+
_v7Lift: 1,
|
|
51271
|
+
_v7Recall: 0,
|
|
51272
|
+
_v7FpRate: 0,
|
|
51273
|
+
_v7Precision: 0,
|
|
51274
|
+
_v8Verdict: "DORMANT",
|
|
51275
|
+
_v8Lift: 1,
|
|
51276
|
+
defaultOff: true
|
|
51277
|
+
},
|
|
51278
|
+
"kotlin/println-debug": {
|
|
51279
|
+
recall: 0,
|
|
51280
|
+
fpRate: 0,
|
|
51281
|
+
ratio: 1,
|
|
51282
|
+
precision: 0,
|
|
51283
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51284
|
+
verdict: "DORMANT",
|
|
51285
|
+
_calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
|
|
51286
|
+
aiSpecific: true,
|
|
51287
|
+
_v7Verdict: "DORMANT",
|
|
51288
|
+
_v7Lift: 1,
|
|
51289
|
+
_v7Recall: 0,
|
|
51290
|
+
_v7FpRate: 0,
|
|
51291
|
+
_v7Precision: 0,
|
|
51292
|
+
_v8Verdict: "DORMANT",
|
|
51293
|
+
_v8Lift: 1,
|
|
51294
|
+
defaultOff: true
|
|
51295
|
+
},
|
|
51296
|
+
"kotlin/object-singleton-misuse": {
|
|
51297
|
+
recall: 0,
|
|
51298
|
+
fpRate: 0,
|
|
51299
|
+
ratio: 1,
|
|
51300
|
+
precision: 0,
|
|
51301
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51302
|
+
verdict: "DORMANT",
|
|
51303
|
+
_calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
|
|
51304
|
+
aiSpecific: true,
|
|
51305
|
+
_v7Verdict: "DORMANT",
|
|
51306
|
+
_v7Lift: 1,
|
|
51307
|
+
_v7Recall: 0,
|
|
51308
|
+
_v7FpRate: 0,
|
|
51309
|
+
_v7Precision: 0,
|
|
51310
|
+
_v8Verdict: "DORMANT",
|
|
51311
|
+
_v8Lift: 1,
|
|
51312
|
+
defaultOff: true
|
|
51313
|
+
},
|
|
51314
|
+
"kotlin/string-concat-loop": {
|
|
51315
|
+
recall: 0,
|
|
51316
|
+
fpRate: 0,
|
|
51317
|
+
ratio: 1,
|
|
51318
|
+
precision: 0,
|
|
51319
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51320
|
+
verdict: "DORMANT",
|
|
51321
|
+
_calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
|
|
51322
|
+
aiSpecific: true,
|
|
51323
|
+
_v7Verdict: "DORMANT",
|
|
51324
|
+
_v7Lift: 1,
|
|
51325
|
+
_v7Recall: 0,
|
|
51326
|
+
_v7FpRate: 0,
|
|
51327
|
+
_v7Precision: 0,
|
|
51328
|
+
_v8Verdict: "DORMANT",
|
|
51329
|
+
_v8Lift: 1,
|
|
51330
|
+
defaultOff: true
|
|
51331
|
+
},
|
|
51332
|
+
"swift/force-unwrap": {
|
|
51333
|
+
recall: 0,
|
|
51334
|
+
fpRate: 0,
|
|
51335
|
+
ratio: 1,
|
|
51336
|
+
precision: 0,
|
|
51337
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51338
|
+
verdict: "DORMANT",
|
|
51339
|
+
_calibrationNote: "v0.24: new rule (Swift force-unwrap \u2014 `as!`, `try!`, `var!.prop` in production). DORMANT until v9 Swift corpus calibration.",
|
|
51340
|
+
aiSpecific: true,
|
|
51341
|
+
_v7Verdict: "DORMANT",
|
|
51342
|
+
_v7Lift: 1,
|
|
51343
|
+
_v7Recall: 0,
|
|
51344
|
+
_v7FpRate: 0,
|
|
51345
|
+
_v7Precision: 0,
|
|
51346
|
+
_v8Verdict: "DORMANT",
|
|
51347
|
+
_v8Lift: 1,
|
|
51348
|
+
defaultOff: true
|
|
51349
|
+
},
|
|
51350
|
+
"swift/print-debug": {
|
|
51351
|
+
recall: 0,
|
|
51352
|
+
fpRate: 0,
|
|
51353
|
+
ratio: 1,
|
|
51354
|
+
precision: 0,
|
|
51355
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51356
|
+
verdict: "DORMANT",
|
|
51357
|
+
_calibrationNote: "v0.24: new rule (multi-print in Swift source \u2014 use Logger / os.log). DORMANT until v9 Swift corpus calibration.",
|
|
51358
|
+
aiSpecific: true,
|
|
51359
|
+
_v7Verdict: "DORMANT",
|
|
51360
|
+
_v7Lift: 1,
|
|
51361
|
+
_v7Recall: 0,
|
|
51362
|
+
_v7FpRate: 0,
|
|
51363
|
+
_v7Precision: 0,
|
|
51364
|
+
_v8Verdict: "DORMANT",
|
|
51365
|
+
_v8Lift: 1,
|
|
51366
|
+
defaultOff: true
|
|
51367
|
+
},
|
|
51368
|
+
"swift/fatal-error-thrown": {
|
|
51369
|
+
recall: 0,
|
|
51370
|
+
fpRate: 0,
|
|
51371
|
+
ratio: 1,
|
|
51372
|
+
precision: 0,
|
|
51373
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51374
|
+
verdict: "DORMANT",
|
|
51375
|
+
_calibrationNote: "v0.24: new rule (`fatalError` / `preconditionFailure` in production Swift \u2014 replaces with typed default). DORMANT until v9 Swift corpus calibration.",
|
|
51376
|
+
aiSpecific: true,
|
|
51377
|
+
_v7Verdict: "DORMANT",
|
|
51378
|
+
_v7Lift: 1,
|
|
51379
|
+
_v7Recall: 0,
|
|
51380
|
+
_v7FpRate: 0,
|
|
51381
|
+
_v7Precision: 0,
|
|
51382
|
+
_v8Verdict: "DORMANT",
|
|
51383
|
+
_v8Lift: 1,
|
|
51384
|
+
defaultOff: true
|
|
51385
|
+
},
|
|
51386
|
+
"swift/implicitly-unwrapped-optional": {
|
|
51387
|
+
recall: 0,
|
|
51388
|
+
fpRate: 0,
|
|
51389
|
+
ratio: 1,
|
|
51390
|
+
precision: 0,
|
|
51391
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51392
|
+
verdict: "DORMANT",
|
|
51393
|
+
_calibrationNote: "v0.24: new rule (Swift implicitly unwrapped optional \u2014 declare as Type? and unwrap with guard let). DORMANT until v9 Swift corpus calibration.",
|
|
51394
|
+
aiSpecific: true,
|
|
51395
|
+
_v7Verdict: "DORMANT",
|
|
51396
|
+
_v7Lift: 1,
|
|
51397
|
+
_v7Recall: 0,
|
|
51398
|
+
_v7FpRate: 0,
|
|
51399
|
+
_v7Precision: 0,
|
|
51400
|
+
_v8Verdict: "DORMANT",
|
|
51401
|
+
_v8Lift: 1,
|
|
51402
|
+
defaultOff: true
|
|
51403
|
+
},
|
|
51404
|
+
"swift/strong-self-capture": {
|
|
51405
|
+
recall: 0,
|
|
51406
|
+
fpRate: 0,
|
|
51407
|
+
ratio: 1,
|
|
51408
|
+
precision: 0,
|
|
51409
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51410
|
+
verdict: "DORMANT",
|
|
51411
|
+
_calibrationNote: "v0.24: new rule (Swift closure captures `self` strongly \u2014 retain cycle. Use [weak self] / [unowned self]). DORMANT until v9 Swift corpus calibration.",
|
|
51412
|
+
aiSpecific: true,
|
|
51413
|
+
_v7Verdict: "DORMANT",
|
|
51414
|
+
_v7Lift: 1,
|
|
51415
|
+
_v7Recall: 0,
|
|
51416
|
+
_v7FpRate: 0,
|
|
51417
|
+
_v7Precision: 0,
|
|
51418
|
+
_v8Verdict: "DORMANT",
|
|
51419
|
+
_v8Lift: 1,
|
|
51420
|
+
defaultOff: true
|
|
51421
|
+
},
|
|
51422
|
+
"cpp/using-namespace-std": {
|
|
51423
|
+
recall: 0,
|
|
51424
|
+
fpRate: 0,
|
|
51425
|
+
ratio: 1,
|
|
51426
|
+
precision: 0,
|
|
51427
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51428
|
+
verdict: "DORMANT",
|
|
51429
|
+
_calibrationNote: "v0.24: new rule (`using namespace std;` in C++ header \u2014 pollutes every consumer). DORMANT until v9 C++ corpus calibration.",
|
|
51430
|
+
aiSpecific: true,
|
|
51431
|
+
_v7Verdict: "DORMANT",
|
|
51432
|
+
_v7Lift: 1,
|
|
51433
|
+
_v7Recall: 0,
|
|
51434
|
+
_v7FpRate: 0,
|
|
51435
|
+
_v7Precision: 0,
|
|
51436
|
+
_v8Verdict: "DORMANT",
|
|
51437
|
+
_v8Lift: 1,
|
|
51438
|
+
defaultOff: true
|
|
51439
|
+
},
|
|
51440
|
+
"cpp/raw-new-delete": {
|
|
51441
|
+
recall: 0,
|
|
51442
|
+
fpRate: 0,
|
|
51443
|
+
ratio: 1,
|
|
51444
|
+
precision: 0,
|
|
51445
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51446
|
+
verdict: "DORMANT",
|
|
51447
|
+
_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.",
|
|
51448
|
+
aiSpecific: true,
|
|
51449
|
+
_v7Verdict: "DORMANT",
|
|
51450
|
+
_v7Lift: 1,
|
|
51451
|
+
_v7Recall: 0,
|
|
51452
|
+
_v7FpRate: 0,
|
|
51453
|
+
_v7Precision: 0,
|
|
51454
|
+
_v8Verdict: "DORMANT",
|
|
51455
|
+
_v8Lift: 1,
|
|
51456
|
+
defaultOff: true
|
|
51457
|
+
},
|
|
51458
|
+
"cpp/c-style-cast": {
|
|
51459
|
+
recall: 0,
|
|
51460
|
+
fpRate: 0,
|
|
51461
|
+
ratio: 1,
|
|
51462
|
+
precision: 0,
|
|
51463
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51464
|
+
verdict: "DORMANT",
|
|
51465
|
+
_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.",
|
|
51466
|
+
aiSpecific: true,
|
|
51467
|
+
_v7Verdict: "DORMANT",
|
|
51468
|
+
_v7Lift: 1,
|
|
51469
|
+
_v7Recall: 0,
|
|
51470
|
+
_v7FpRate: 0,
|
|
51471
|
+
_v7Precision: 0,
|
|
51472
|
+
_v8Verdict: "DORMANT",
|
|
51473
|
+
_v8Lift: 1,
|
|
51474
|
+
defaultOff: true
|
|
51475
|
+
},
|
|
51476
|
+
"cpp/printf-debug": {
|
|
51477
|
+
recall: 0,
|
|
51478
|
+
fpRate: 0,
|
|
51479
|
+
ratio: 1,
|
|
51480
|
+
precision: 0,
|
|
51481
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51482
|
+
verdict: "DORMANT",
|
|
51483
|
+
_calibrationNote: "v0.24: new rule (printf family / std::cout debug printing in C++ \u2014 use spdlog / glog / AbslLog). DORMANT until v9 C++ corpus calibration.",
|
|
51484
|
+
aiSpecific: true,
|
|
51485
|
+
_v7Verdict: "DORMANT",
|
|
51486
|
+
_v7Lift: 1,
|
|
51487
|
+
_v7Recall: 0,
|
|
51488
|
+
_v7FpRate: 0,
|
|
51489
|
+
_v7Precision: 0,
|
|
51490
|
+
_v8Verdict: "DORMANT",
|
|
51491
|
+
_v8Lift: 1,
|
|
51492
|
+
defaultOff: true
|
|
51493
|
+
},
|
|
51494
|
+
"cpp/magic-numbers": {
|
|
51495
|
+
recall: 0,
|
|
51496
|
+
fpRate: 0,
|
|
51497
|
+
ratio: 1,
|
|
51498
|
+
precision: 0,
|
|
51499
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51500
|
+
verdict: "DORMANT",
|
|
51501
|
+
_calibrationNote: "v0.24: new rule (magic-number literal in C++ comparison / return \u2014 name it `constexpr int MAX = ...`). DORMANT until v9 C++ corpus calibration.",
|
|
51502
|
+
aiSpecific: true,
|
|
51503
|
+
_v7Verdict: "DORMANT",
|
|
51504
|
+
_v7Lift: 1,
|
|
51505
|
+
_v7Recall: 0,
|
|
51506
|
+
_v7FpRate: 0,
|
|
51507
|
+
_v7Precision: 0,
|
|
51508
|
+
_v8Verdict: "DORMANT",
|
|
51509
|
+
_v8Lift: 1,
|
|
51510
|
+
defaultOff: true
|
|
49843
51511
|
}
|
|
49844
51512
|
};
|
|
49845
51513
|
}
|
|
@@ -49881,7 +51549,8 @@ var init_signal_strength2 = __esm({
|
|
|
49881
51549
|
// src/engine/worker.ts
|
|
49882
51550
|
import { isMainThread, parentPort, workerData } from "worker_threads";
|
|
49883
51551
|
import { extname as extname5 } from "path";
|
|
49884
|
-
import { join as join13 } from "path";
|
|
51552
|
+
import { join as join13, relative as relative5, sep as sep2 } from "path";
|
|
51553
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
49885
51554
|
function buildParserCacheConfig(cwd) {
|
|
49886
51555
|
const envVal = process.env.SLOP_AUDIT_CACHE;
|
|
49887
51556
|
const enabled = envVal === "1" || envVal === "true";
|
|
@@ -49901,7 +51570,23 @@ function applyRuleOverrides(issues, rules) {
|
|
|
49901
51570
|
}
|
|
49902
51571
|
return result;
|
|
49903
51572
|
}
|
|
51573
|
+
function isExcludedBySelfScan(filePath, cwd, excludePaths) {
|
|
51574
|
+
if (!excludePaths || excludePaths.length === 0) return false;
|
|
51575
|
+
const rel = relative5(cwd, filePath).split(sep2).join("/");
|
|
51576
|
+
return excludePaths.some((pattern) => minimatch2(rel, pattern, { dot: true }));
|
|
51577
|
+
}
|
|
49904
51578
|
async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
51579
|
+
if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
|
|
51580
|
+
return {
|
|
51581
|
+
filePath,
|
|
51582
|
+
componentCount: 0,
|
|
51583
|
+
issues: [],
|
|
51584
|
+
gapValues: [],
|
|
51585
|
+
styleSources: [],
|
|
51586
|
+
elementTags: [],
|
|
51587
|
+
unmatchedStringLiterals: []
|
|
51588
|
+
};
|
|
51589
|
+
}
|
|
49905
51590
|
const cache = buildParserCacheConfig(cwd);
|
|
49906
51591
|
const ext = extname5(filePath).toLowerCase();
|
|
49907
51592
|
const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
|
|
@@ -49916,7 +51601,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
49916
51601
|
".h",
|
|
49917
51602
|
".hpp",
|
|
49918
51603
|
".hxx",
|
|
49919
|
-
".java",
|
|
49920
51604
|
".rb",
|
|
49921
51605
|
".php"
|
|
49922
51606
|
]);
|
|
@@ -50039,7 +51723,7 @@ var init_worker = __esm({
|
|
|
50039
51723
|
});
|
|
50040
51724
|
|
|
50041
51725
|
// src/engine/flywheel.ts
|
|
50042
|
-
import { createHash as
|
|
51726
|
+
import { createHash as createHash8 } from "crypto";
|
|
50043
51727
|
import { existsSync as existsSync14, mkdirSync as mkdirSync3, readFileSync as readFileSync13, writeFileSync as writeFileSync4 } from "fs";
|
|
50044
51728
|
import { join as join14 } from "path";
|
|
50045
51729
|
function severityBump(severity) {
|
|
@@ -50191,7 +51875,7 @@ function saveFlywheelState(cwd, state) {
|
|
|
50191
51875
|
writeFileSync4(join14(dir, STATE_FILE), JSON.stringify(state, null, 2));
|
|
50192
51876
|
}
|
|
50193
51877
|
function hashFile(filePath) {
|
|
50194
|
-
return
|
|
51878
|
+
return createHash8("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
50195
51879
|
}
|
|
50196
51880
|
var FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
|
|
50197
51881
|
var init_flywheel = __esm({
|
|
@@ -50204,45 +51888,6 @@ var init_flywheel = __esm({
|
|
|
50204
51888
|
}
|
|
50205
51889
|
});
|
|
50206
51890
|
|
|
50207
|
-
// src/engine/ai-security-risk.ts
|
|
50208
|
-
function computeAiSecurityRisk(issues) {
|
|
50209
|
-
const findings = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
50210
|
-
for (const issue of issues) {
|
|
50211
|
-
switch (issue.severity) {
|
|
50212
|
-
case "high":
|
|
50213
|
-
findings.high += 1;
|
|
50214
|
-
break;
|
|
50215
|
-
case "medium":
|
|
50216
|
-
findings.medium += 1;
|
|
50217
|
-
break;
|
|
50218
|
-
case "low":
|
|
50219
|
-
findings.low += 1;
|
|
50220
|
-
break;
|
|
50221
|
-
default:
|
|
50222
|
-
findings.low += 1;
|
|
50223
|
-
}
|
|
50224
|
-
}
|
|
50225
|
-
let risk = "low";
|
|
50226
|
-
if (findings.critical >= 1 || findings.high >= 3) {
|
|
50227
|
-
risk = "critical";
|
|
50228
|
-
} else if (findings.high >= 1 || findings.medium >= 3) {
|
|
50229
|
-
risk = "high";
|
|
50230
|
-
} else if (findings.medium >= 1) {
|
|
50231
|
-
risk = "medium";
|
|
50232
|
-
}
|
|
50233
|
-
return { risk, findings };
|
|
50234
|
-
}
|
|
50235
|
-
function formatAiSecurityRiskLine(risk, findings) {
|
|
50236
|
-
const total = findings.critical + findings.high + findings.medium + findings.low;
|
|
50237
|
-
if (total === 0) return "AI Security Risk: low";
|
|
50238
|
-
return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
|
|
50239
|
-
}
|
|
50240
|
-
var init_ai_security_risk = __esm({
|
|
50241
|
-
"src/engine/ai-security-risk.ts"() {
|
|
50242
|
-
"use strict";
|
|
50243
|
-
}
|
|
50244
|
-
});
|
|
50245
|
-
|
|
50246
51891
|
// src/engine/test-quality.ts
|
|
50247
51892
|
var test_quality_exports = {};
|
|
50248
51893
|
__export(test_quality_exports, {
|
|
@@ -50311,7 +51956,7 @@ var init_test_quality = __esm({
|
|
|
50311
51956
|
});
|
|
50312
51957
|
|
|
50313
51958
|
// src/engine/metrics.ts
|
|
50314
|
-
import { isAbsolute as isAbsolute2, relative as
|
|
51959
|
+
import { isAbsolute as isAbsolute2, relative as relative6 } from "path";
|
|
50315
51960
|
function bucketFor(ruleId) {
|
|
50316
51961
|
return RULE_TO_BUCKET[ruleId] ?? "visual";
|
|
50317
51962
|
}
|
|
@@ -50335,7 +51980,7 @@ function scoreFile(result, frameworkMultiplier, config, baseline, cwd) {
|
|
|
50335
51980
|
100,
|
|
50336
51981
|
rawScore * frameworkMultiplier * CONTEXT_DENSITY_MULTIPLIER
|
|
50337
51982
|
);
|
|
50338
|
-
const baselineKey = cwd ? isAbsolute2(result.filePath) ?
|
|
51983
|
+
const baselineKey = cwd ? isAbsolute2(result.filePath) ? relative6(cwd, result.filePath) : result.filePath : result.filePath;
|
|
50339
51984
|
const baselineScore = baseline?.scores[baselineKey]?.baselineScore ?? 0;
|
|
50340
51985
|
const adjustedScore = baseline ? Math.max(0, componentScore - baselineScore) : componentScore;
|
|
50341
51986
|
return {
|
|
@@ -50419,14 +52064,10 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
|
|
|
50419
52064
|
});
|
|
50420
52065
|
}
|
|
50421
52066
|
}
|
|
50422
|
-
const
|
|
50423
|
-
|
|
50424
|
-
|
|
50425
|
-
|
|
50426
|
-
high: 33,
|
|
50427
|
-
critical: 0
|
|
50428
|
-
};
|
|
50429
|
-
const security = securityFromRisk[risk];
|
|
52067
|
+
const securityIssueCount = flatIssues.filter(
|
|
52068
|
+
(i) => i.category === "security" || (i.ruleId ?? "").startsWith("security/")
|
|
52069
|
+
).length;
|
|
52070
|
+
const security = Math.max(0, 100 / (1 + securityIssueCount / 5));
|
|
50430
52071
|
const engineeringCategoryScores = [
|
|
50431
52072
|
categoryScores.arch ?? 0,
|
|
50432
52073
|
categoryScores.logic ?? 0,
|
|
@@ -50484,7 +52125,6 @@ var SEVERITY_WEIGHTS, CONTEXT_DENSITY_MULTIPLIER, COMPOSITE_WEIGHTS, RULE_TO_BUC
|
|
|
50484
52125
|
var init_metrics = __esm({
|
|
50485
52126
|
"src/engine/metrics.ts"() {
|
|
50486
52127
|
"use strict";
|
|
50487
|
-
init_ai_security_risk();
|
|
50488
52128
|
init_test_quality();
|
|
50489
52129
|
SEVERITY_WEIGHTS = {
|
|
50490
52130
|
low: 1,
|
|
@@ -50548,7 +52188,7 @@ var init_metrics = __esm({
|
|
|
50548
52188
|
});
|
|
50549
52189
|
|
|
50550
52190
|
// src/engine/cache.ts
|
|
50551
|
-
import { createHash as
|
|
52191
|
+
import { createHash as createHash9 } from "crypto";
|
|
50552
52192
|
import { existsSync as existsSync15, mkdirSync as mkdirSync4, readFileSync as readFileSync14, writeFileSync as writeFileSync5 } from "fs";
|
|
50553
52193
|
import { join as join15 } from "path";
|
|
50554
52194
|
function parseVersion(version) {
|
|
@@ -50622,7 +52262,7 @@ function pickBaselineConfig(config) {
|
|
|
50622
52262
|
return picked;
|
|
50623
52263
|
}
|
|
50624
52264
|
function hashConfig(config) {
|
|
50625
|
-
return
|
|
52265
|
+
return createHash9("sha256").update(JSON.stringify(sanitizeForHash(pickBaselineConfig(config)))).digest("hex");
|
|
50626
52266
|
}
|
|
50627
52267
|
function baselinePath(projectPath) {
|
|
50628
52268
|
return join15(projectPath, ".slopbrick", "cache", "baseline.json");
|
|
@@ -52416,6 +54056,45 @@ var init_business_logic = __esm({
|
|
|
52416
54056
|
}
|
|
52417
54057
|
});
|
|
52418
54058
|
|
|
54059
|
+
// src/engine/ai-security-risk.ts
|
|
54060
|
+
function computeAiSecurityRisk(issues) {
|
|
54061
|
+
const findings = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
54062
|
+
for (const issue of issues) {
|
|
54063
|
+
switch (issue.severity) {
|
|
54064
|
+
case "high":
|
|
54065
|
+
findings.high += 1;
|
|
54066
|
+
break;
|
|
54067
|
+
case "medium":
|
|
54068
|
+
findings.medium += 1;
|
|
54069
|
+
break;
|
|
54070
|
+
case "low":
|
|
54071
|
+
findings.low += 1;
|
|
54072
|
+
break;
|
|
54073
|
+
default:
|
|
54074
|
+
findings.low += 1;
|
|
54075
|
+
}
|
|
54076
|
+
}
|
|
54077
|
+
let risk = "low";
|
|
54078
|
+
if (findings.critical >= 1 || findings.high >= 3) {
|
|
54079
|
+
risk = "critical";
|
|
54080
|
+
} else if (findings.high >= 1 || findings.medium >= 3) {
|
|
54081
|
+
risk = "high";
|
|
54082
|
+
} else if (findings.medium >= 1) {
|
|
54083
|
+
risk = "medium";
|
|
54084
|
+
}
|
|
54085
|
+
return { risk, findings };
|
|
54086
|
+
}
|
|
54087
|
+
function formatAiSecurityRiskLine(risk, findings) {
|
|
54088
|
+
const total = findings.critical + findings.high + findings.medium + findings.low;
|
|
54089
|
+
if (total === 0) return "AI Security Risk: low";
|
|
54090
|
+
return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
|
|
54091
|
+
}
|
|
54092
|
+
var init_ai_security_risk = __esm({
|
|
54093
|
+
"src/engine/ai-security-risk.ts"() {
|
|
54094
|
+
"use strict";
|
|
54095
|
+
}
|
|
54096
|
+
});
|
|
54097
|
+
|
|
52419
54098
|
// src/engine/maintenance-cost.ts
|
|
52420
54099
|
var maintenance_cost_exports = {};
|
|
52421
54100
|
__export(maintenance_cost_exports, {
|
|
@@ -52692,7 +54371,7 @@ __export(db_health_exports, {
|
|
|
52692
54371
|
buildDbHealth: () => buildDbHealth
|
|
52693
54372
|
});
|
|
52694
54373
|
import { readFileSync as readFileSync17 } from "fs";
|
|
52695
|
-
import { relative as
|
|
54374
|
+
import { relative as relative7 } from "path";
|
|
52696
54375
|
import { globby as globby3 } from "globby";
|
|
52697
54376
|
import { parse as parseSql6, loadModule as loadSqlModule6 } from "pgsql-parser";
|
|
52698
54377
|
function ensureSqlModule() {
|
|
@@ -52743,7 +54422,7 @@ async function buildDbHealth(cwd, _config, options = {}) {
|
|
|
52743
54422
|
for (const abs of sqlFiles.slice(0, maxFiles)) {
|
|
52744
54423
|
const parsed = await parseSqlFile(abs);
|
|
52745
54424
|
if (!parsed) continue;
|
|
52746
|
-
const relPath =
|
|
54425
|
+
const relPath = relative7(cwd, abs);
|
|
52747
54426
|
parsedFiles.push({ relPath, parsed });
|
|
52748
54427
|
for (const stmt of parsed.statements) {
|
|
52749
54428
|
if (stmt.type !== "IndexStmt") continue;
|
|
@@ -52798,7 +54477,7 @@ async function buildDbHealth(cwd, _config, options = {}) {
|
|
|
52798
54477
|
} catch {
|
|
52799
54478
|
continue;
|
|
52800
54479
|
}
|
|
52801
|
-
const relPath =
|
|
54480
|
+
const relPath = relative7(cwd, abs);
|
|
52802
54481
|
const context = { config: _config, filePath: relPath, cwd };
|
|
52803
54482
|
const facts = { filePath: relPath, v2: { _source: source } };
|
|
52804
54483
|
const ruleContext = sqlConcatRule.create(context);
|
|
@@ -53155,7 +54834,7 @@ function computeDomainScores(issues) {
|
|
|
53155
54834
|
performance2.score = issueCountToScore(performance2.issueCount);
|
|
53156
54835
|
const security = tally(SECURITY_CATEGORIES);
|
|
53157
54836
|
security.name = "security";
|
|
53158
|
-
security.score =
|
|
54837
|
+
security.score = Math.max(0, 100 / (1 + security.issueCount / 5));
|
|
53159
54838
|
return { codeHygiene, accessibility, performance: performance2, security };
|
|
53160
54839
|
}
|
|
53161
54840
|
var COHERENCE_WEIGHTS, AI_DEBT_NUMERIC, CODE_HYGIENE_CATEGORIES, ACCESSIBILITY_CATEGORIES, PERFORMANCE_CATEGORIES, SECURITY_CATEGORIES;
|
|
@@ -53183,7 +54862,7 @@ var init_coherence = __esm({
|
|
|
53183
54862
|
|
|
53184
54863
|
// src/cli/report/enrichReport.ts
|
|
53185
54864
|
import { readFileSync as readFileSync18 } from "fs";
|
|
53186
|
-
import { relative as
|
|
54865
|
+
import { relative as relative8 } from "path";
|
|
53187
54866
|
function collectBusinessLogicIssues(cwd, filePaths) {
|
|
53188
54867
|
const issues = [];
|
|
53189
54868
|
for (const absPath of filePaths) {
|
|
@@ -53197,7 +54876,7 @@ function collectBusinessLogicIssues(cwd, filePaths) {
|
|
|
53197
54876
|
for (const issue of fileIssues) {
|
|
53198
54877
|
issues.push({
|
|
53199
54878
|
...issue,
|
|
53200
|
-
filePath:
|
|
54879
|
+
filePath: relative8(cwd, absPath) || absPath
|
|
53201
54880
|
});
|
|
53202
54881
|
}
|
|
53203
54882
|
}
|
|
@@ -53587,17 +55266,17 @@ import {
|
|
|
53587
55266
|
rmSync,
|
|
53588
55267
|
statSync as statSync6
|
|
53589
55268
|
} from "fs";
|
|
53590
|
-
import { createHash as
|
|
53591
|
-
import { dirname as dirname11, join as join17, relative as
|
|
55269
|
+
import { createHash as createHash10 } from "crypto";
|
|
55270
|
+
import { dirname as dirname11, join as join17, relative as relative9 } from "path";
|
|
53592
55271
|
function telemetryPath2(cwd) {
|
|
53593
55272
|
return join17(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
|
|
53594
55273
|
}
|
|
53595
55274
|
function hashString(input) {
|
|
53596
|
-
return
|
|
55275
|
+
return createHash10("sha256").update(input).digest("hex").slice(0, 16);
|
|
53597
55276
|
}
|
|
53598
55277
|
function safeRelative(cwd, filePath) {
|
|
53599
55278
|
try {
|
|
53600
|
-
return
|
|
55279
|
+
return relative9(cwd, filePath);
|
|
53601
55280
|
} catch {
|
|
53602
55281
|
return filePath;
|
|
53603
55282
|
}
|
|
@@ -53945,7 +55624,7 @@ var init_structure_md = __esm({
|
|
|
53945
55624
|
|
|
53946
55625
|
// src/cli/report/persistRun.ts
|
|
53947
55626
|
import { existsSync as existsSync18, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
|
|
53948
|
-
import { join as join19, relative as
|
|
55627
|
+
import { join as join19, relative as relative10 } from "path";
|
|
53949
55628
|
async function persistRun(input) {
|
|
53950
55629
|
const {
|
|
53951
55630
|
cwd,
|
|
@@ -54007,7 +55686,7 @@ async function persistRun(input) {
|
|
|
54007
55686
|
const recentTopHashes = telemetryPayloads.map(
|
|
54008
55687
|
(payload) => [...payload.files].sort((a, b) => b.score - a.score).slice(0, 10).map((file) => file.hash)
|
|
54009
55688
|
);
|
|
54010
|
-
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile(
|
|
55689
|
+
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile(relative10(cwd, c.filePath)) }));
|
|
54011
55690
|
const unmatchedStringLiterals = results.flatMap((r) => r.unmatchedStringLiterals ?? []);
|
|
54012
55691
|
const flywheelOutput = computeFlywheelOutput(
|
|
54013
55692
|
runs,
|
|
@@ -54212,11 +55891,11 @@ var init_finalizeReport = __esm({
|
|
|
54212
55891
|
});
|
|
54213
55892
|
|
|
54214
55893
|
// src/cli/report/baseline-cache.ts
|
|
54215
|
-
import { relative as
|
|
55894
|
+
import { relative as relative11 } from "path";
|
|
54216
55895
|
function buildBaselineCache(report, configHash, gitHead, cwd) {
|
|
54217
55896
|
const scores = {};
|
|
54218
55897
|
for (const component of report.components) {
|
|
54219
|
-
scores[
|
|
55898
|
+
scores[relative11(cwd, component.filePath)] = {
|
|
54220
55899
|
baselineScore: component.componentScore,
|
|
54221
55900
|
componentCount: component.componentCount
|
|
54222
55901
|
};
|
|
@@ -54292,16 +55971,16 @@ var init_json = __esm({
|
|
|
54292
55971
|
});
|
|
54293
55972
|
|
|
54294
55973
|
// src/report/sarif.ts
|
|
54295
|
-
import { createHash as
|
|
55974
|
+
import { createHash as createHash11 } from "crypto";
|
|
54296
55975
|
import { readFileSync as readFileSync21 } from "fs";
|
|
54297
|
-
import { basename as basename3, isAbsolute as isAbsolute3, relative as
|
|
55976
|
+
import { basename as basename3, isAbsolute as isAbsolute3, relative as relative12, resolve as resolve9 } from "path";
|
|
54298
55977
|
function buildArtifactUri(filePath, cwd) {
|
|
54299
55978
|
if (!filePath) {
|
|
54300
55979
|
return ".";
|
|
54301
55980
|
}
|
|
54302
55981
|
if (cwd) {
|
|
54303
55982
|
const absoluteFilePath = isAbsolute3(filePath) ? filePath : resolve9(cwd, filePath);
|
|
54304
|
-
const rel =
|
|
55983
|
+
const rel = relative12(cwd, absoluteFilePath);
|
|
54305
55984
|
if (rel.startsWith("..")) {
|
|
54306
55985
|
return basename3(filePath);
|
|
54307
55986
|
}
|
|
@@ -54365,7 +56044,7 @@ function buildHelpUri(ruleId, category) {
|
|
|
54365
56044
|
}
|
|
54366
56045
|
function computeFingerprint(input) {
|
|
54367
56046
|
const payload = `${input.ruleId}|${input.fileUri}|${input.line}|${input.column}|${input.category}`;
|
|
54368
|
-
return
|
|
56047
|
+
return createHash11("sha256").update(payload).digest("hex").slice(0, 16);
|
|
54369
56048
|
}
|
|
54370
56049
|
function buildRuleFromIssue(issue) {
|
|
54371
56050
|
return {
|
|
@@ -55676,7 +57355,7 @@ var init_layout_token = __esm({
|
|
|
55676
57355
|
|
|
55677
57356
|
// src/report/unified-diff.ts
|
|
55678
57357
|
import { existsSync as existsSync19, readFileSync as readFileSync23 } from "fs";
|
|
55679
|
-
import { relative as
|
|
57358
|
+
import { relative as relative13 } from "path";
|
|
55680
57359
|
function collectAllFixes(issue) {
|
|
55681
57360
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
55682
57361
|
}
|
|
@@ -55762,7 +57441,7 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
55762
57441
|
parts.push("");
|
|
55763
57442
|
hasHunk = true;
|
|
55764
57443
|
}
|
|
55765
|
-
const rel =
|
|
57444
|
+
const rel = relative13(cwd, filePath);
|
|
55766
57445
|
parts.push(`--- a/${rel}`);
|
|
55767
57446
|
parts.push(`+++ b/${rel}`);
|
|
55768
57447
|
parts.push(...formatHunk(original, patched));
|
|
@@ -55786,7 +57465,7 @@ __export(heatmap_exports, {
|
|
|
55786
57465
|
buildHeatmap: () => buildHeatmap,
|
|
55787
57466
|
formatHeatmap: () => formatHeatmap
|
|
55788
57467
|
});
|
|
55789
|
-
import { relative as
|
|
57468
|
+
import { relative as relative14 } from "path";
|
|
55790
57469
|
function isWithinDays(lastModified, reference, days) {
|
|
55791
57470
|
const msPerDay = 1e3 * 60 * 60 * 24;
|
|
55792
57471
|
const daysAgo = (reference.getTime() - lastModified.getTime()) / msPerDay;
|
|
@@ -55802,7 +57481,7 @@ async function buildHeatmap(report, cwd, helpers = { getFileEditCount, getFileLa
|
|
|
55802
57481
|
const reference = new Date(report.generatedAt);
|
|
55803
57482
|
const entries = await Promise.all(
|
|
55804
57483
|
report.components.map(async (component) => {
|
|
55805
|
-
const relPath =
|
|
57484
|
+
const relPath = relative14(cwd, component.filePath) || component.filePath;
|
|
55806
57485
|
const [edits, lastModified] = await Promise.all([
|
|
55807
57486
|
helpers.getFileEditCount(cwd, relPath, RECENCY_DAYS),
|
|
55808
57487
|
helpers.getFileLastModifiedDate(cwd, relPath)
|
|
@@ -56147,10 +57826,12 @@ __export(scan_exports, {
|
|
|
56147
57826
|
watchProject: () => watchProject
|
|
56148
57827
|
});
|
|
56149
57828
|
import { existsSync as existsSync20, statSync as statSync8 } from "fs";
|
|
56150
|
-
import { resolve as resolve12, relative as
|
|
57829
|
+
import { resolve as resolve12, relative as relative15, extname as extname7, sep as sep3 } from "path";
|
|
57830
|
+
import { randomUUID } from "crypto";
|
|
56151
57831
|
async function runScan(options, explicitPaths) {
|
|
56152
57832
|
setLoggerQuiet(!!options.quiet);
|
|
56153
57833
|
const startTime = Date.now();
|
|
57834
|
+
const scanId = randomUUID();
|
|
56154
57835
|
const cwd = resolve12(options.workspace ?? process.cwd());
|
|
56155
57836
|
if (!existsSync20(cwd)) {
|
|
56156
57837
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
@@ -56193,7 +57874,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56193
57874
|
let files;
|
|
56194
57875
|
if (explicitPaths && explicitPaths.length > 0) {
|
|
56195
57876
|
const { globby: globby4 } = await import("globby");
|
|
56196
|
-
const { minimatch:
|
|
57877
|
+
const { minimatch: minimatch4 } = await import("minimatch");
|
|
56197
57878
|
const resolved = explicitPaths.map((p) => resolve12(cwd, p));
|
|
56198
57879
|
const expanded = [];
|
|
56199
57880
|
for (const p of resolved) {
|
|
@@ -56201,11 +57882,11 @@ async function runScan(options, explicitPaths) {
|
|
|
56201
57882
|
const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
|
|
56202
57883
|
for (const f of found) {
|
|
56203
57884
|
if (!ALL_SOURCE_EXTENSIONS.has(extname7(f).toLowerCase())) continue;
|
|
56204
|
-
const rel =
|
|
56205
|
-
if (config.include.length > 0 && !config.include.some((pattern) =>
|
|
57885
|
+
const rel = relative15(cwd, f).split(sep3).join("/");
|
|
57886
|
+
if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
|
|
56206
57887
|
continue;
|
|
56207
57888
|
}
|
|
56208
|
-
if (config.exclude.some((pattern) =>
|
|
57889
|
+
if (config.exclude.some((pattern) => minimatch4(rel, pattern, { dot: true }))) {
|
|
56209
57890
|
continue;
|
|
56210
57891
|
}
|
|
56211
57892
|
expanded.push(f);
|
|
@@ -56404,7 +58085,18 @@ async function runScan(options, explicitPaths) {
|
|
|
56404
58085
|
config,
|
|
56405
58086
|
noIncreaseFailure,
|
|
56406
58087
|
baseline,
|
|
56407
|
-
machineReadableStdout
|
|
58088
|
+
machineReadableStdout,
|
|
58089
|
+
// v0.24.0 (Workstream C): stats for the opt-in network beacon.
|
|
58090
|
+
// `fileCount` = files that ran through the worker pool (excludes
|
|
58091
|
+
// incremental-cache skips). `ruleCount` = builtin rules, or 1
|
|
58092
|
+
// when `--rule <id>` narrows the run. `durationMs` = wall-clock
|
|
58093
|
+
// since `runScan` entry (matches `report.scanDurationMs`).
|
|
58094
|
+
scanStats: {
|
|
58095
|
+
scanId,
|
|
58096
|
+
fileCount: results.length,
|
|
58097
|
+
ruleCount: options.rule ? 1 : builtinRules.length,
|
|
58098
|
+
durationMs: Date.now() - startTime
|
|
58099
|
+
}
|
|
56408
58100
|
};
|
|
56409
58101
|
}
|
|
56410
58102
|
async function scanProject(options) {
|
|
@@ -57762,13 +59454,35 @@ var RULE_HINTS = {
|
|
|
57762
59454
|
"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
59455
|
// v0.19.0 — Duplication detector
|
|
57764
59456
|
"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.",
|
|
59457
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
59458
|
+
"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.)",
|
|
59459
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
59460
|
+
"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
59461
|
// v0.20.0 — Java-specific rules
|
|
57766
59462
|
"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
59463
|
"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
59464
|
"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
59465
|
"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
59466
|
"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)."
|
|
59467
|
+
"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).",
|
|
59468
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
59469
|
+
"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.",
|
|
59470
|
+
"kotlin/coroutine-global-scope": "Replace with `viewModelScope.launch { ... }`, `lifecycleScope.launch { ... }`, or `coroutineScope { ... }`. GlobalScope bypasses cancellation. (v0.24.0 \u2014 DORMANT.)",
|
|
59471
|
+
"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.)",
|
|
59472
|
+
"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.)",
|
|
59473
|
+
"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.)",
|
|
59474
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
59475
|
+
"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.)",
|
|
59476
|
+
"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.)",
|
|
59477
|
+
"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.)",
|
|
59478
|
+
"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.)",
|
|
59479
|
+
"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.)",
|
|
59480
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
59481
|
+
"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.)",
|
|
59482
|
+
"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.)",
|
|
59483
|
+
"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.)",
|
|
59484
|
+
"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.)",
|
|
59485
|
+
"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
59486
|
};
|
|
57773
59487
|
|
|
57774
59488
|
// src/cli/commands/explain.ts
|
|
@@ -59406,7 +61120,7 @@ import { resolve as resolve26 } from "path";
|
|
|
59406
61120
|
init_discover();
|
|
59407
61121
|
init_patterns();
|
|
59408
61122
|
import { readFileSync as readFileSync30 } from "fs";
|
|
59409
|
-
import { basename as basename4, relative as
|
|
61123
|
+
import { basename as basename4, relative as relative16 } from "path";
|
|
59410
61124
|
async function runDrift(cwd, config, options = {}) {
|
|
59411
61125
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
59412
61126
|
const allFiles = await discoverFiles(cwd, config);
|
|
@@ -59428,7 +61142,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
59428
61142
|
byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
|
|
59429
61143
|
byFile.push({
|
|
59430
61144
|
file: absPath,
|
|
59431
|
-
relPath:
|
|
61145
|
+
relPath: relative16(cwd, absPath),
|
|
59432
61146
|
category: v.category,
|
|
59433
61147
|
import: v.import,
|
|
59434
61148
|
declared: v.declared,
|
|
@@ -59550,10 +61264,10 @@ init_metrics();
|
|
|
59550
61264
|
init_patterns();
|
|
59551
61265
|
init_discover();
|
|
59552
61266
|
import { readFileSync as readFileSync31 } from "fs";
|
|
59553
|
-
import { extname as extname9, relative as
|
|
61267
|
+
import { extname as extname9, relative as relative17, resolve as resolve27 } from "path";
|
|
59554
61268
|
import { execFile as execFileCb } from "child_process";
|
|
59555
61269
|
import { promisify as promisify2 } from "util";
|
|
59556
|
-
import { minimatch as
|
|
61270
|
+
import { minimatch as minimatch3 } from "minimatch";
|
|
59557
61271
|
var execFile2 = promisify2(execFileCb);
|
|
59558
61272
|
var PR_EXTENSIONS = /* @__PURE__ */ new Set([...SOURCE_EXTENSIONS, ".mdx"]);
|
|
59559
61273
|
async function refExists(cwd, ref) {
|
|
@@ -59594,11 +61308,11 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
59594
61308
|
const abs = resolve27(cwd, relOrAbs);
|
|
59595
61309
|
const ext = extname9(abs).toLowerCase();
|
|
59596
61310
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
59597
|
-
const rel =
|
|
59598
|
-
if (config.include.length > 0 && !config.include.some((pattern) =>
|
|
61311
|
+
const rel = relative17(cwd, abs).split("\\").join("/");
|
|
61312
|
+
if (config.include.length > 0 && !config.include.some((pattern) => minimatch3(rel, pattern))) {
|
|
59599
61313
|
continue;
|
|
59600
61314
|
}
|
|
59601
|
-
if (config.exclude.some((pattern) =>
|
|
61315
|
+
if (config.exclude.some((pattern) => minimatch3(rel, pattern))) {
|
|
59602
61316
|
continue;
|
|
59603
61317
|
}
|
|
59604
61318
|
sourceFiles.push(abs);
|
|
@@ -59665,7 +61379,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
59665
61379
|
totalScore += fileScore;
|
|
59666
61380
|
files.push({
|
|
59667
61381
|
file: absPath,
|
|
59668
|
-
relPath:
|
|
61382
|
+
relPath: relative17(cwd, absPath).split("\\").join("/"),
|
|
59669
61383
|
score: fileScore,
|
|
59670
61384
|
issueCount: issues.length,
|
|
59671
61385
|
constitutionViolationCount: constitutionViolations.length,
|
|
@@ -60042,7 +61756,7 @@ import { resolve as resolve33 } from "path";
|
|
|
60042
61756
|
init_discover();
|
|
60043
61757
|
init_business_logic();
|
|
60044
61758
|
import { readFileSync as readFileSync32 } from "fs";
|
|
60045
|
-
import { relative as
|
|
61759
|
+
import { relative as relative18 } from "path";
|
|
60046
61760
|
async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
60047
61761
|
const maxFiles = options.maxFiles ?? 500;
|
|
60048
61762
|
const allFiles = await discoverFiles(cwd, config);
|
|
@@ -60061,7 +61775,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
60061
61775
|
for (const issue of fileIssues) {
|
|
60062
61776
|
issues.push({
|
|
60063
61777
|
...issue,
|
|
60064
|
-
filePath:
|
|
61778
|
+
filePath: relative18(cwd, absPath) || absPath
|
|
60065
61779
|
});
|
|
60066
61780
|
}
|
|
60067
61781
|
}
|
|
@@ -60623,7 +62337,7 @@ import { resolve as resolve37 } from "path";
|
|
|
60623
62337
|
init_patterns();
|
|
60624
62338
|
init_discover();
|
|
60625
62339
|
import { readFileSync as readFileSync33 } from "fs";
|
|
60626
|
-
import { basename as basename5, relative as
|
|
62340
|
+
import { basename as basename5, relative as relative19 } from "path";
|
|
60627
62341
|
var PATTERN_CATEGORIES = [
|
|
60628
62342
|
"modal",
|
|
60629
62343
|
"button",
|
|
@@ -60715,7 +62429,7 @@ function detectApiFromFiles(files, cwd) {
|
|
|
60715
62429
|
const out = [];
|
|
60716
62430
|
for (const f of files) {
|
|
60717
62431
|
if (API_PATH_RE2.test(f)) {
|
|
60718
|
-
out.push(
|
|
62432
|
+
out.push(relative19(cwd, f).split("\\").join("/"));
|
|
60719
62433
|
}
|
|
60720
62434
|
}
|
|
60721
62435
|
return out;
|
|
@@ -62025,7 +63739,11 @@ var OPTION_CATEGORY = {
|
|
|
62025
63739
|
// Tokens
|
|
62026
63740
|
"--tokens": "tokens",
|
|
62027
63741
|
// Telemetry
|
|
62028
|
-
"--no-telemetry": "telemetry"
|
|
63742
|
+
"--no-telemetry": "telemetry",
|
|
63743
|
+
// v0.24.0 (Workstream C): opt-in network beacon. Distinct from
|
|
63744
|
+
// --no-telemetry (which gates the local flywheel); this gates a
|
|
63745
|
+
// single POST at scan end.
|
|
63746
|
+
"--report-usage": "telemetry"
|
|
62029
63747
|
};
|
|
62030
63748
|
var CATEGORY_ORDER2 = [
|
|
62031
63749
|
"file",
|
|
@@ -62094,6 +63812,109 @@ function formatGroupedHelp(cmd) {
|
|
|
62094
63812
|
return lines.join("\n");
|
|
62095
63813
|
}
|
|
62096
63814
|
|
|
63815
|
+
// src/beacon/endpoint.ts
|
|
63816
|
+
import * as http from "http";
|
|
63817
|
+
import * as https from "https";
|
|
63818
|
+
import { URL as URL2 } from "url";
|
|
63819
|
+
var sendBeacon = (url, payload, timeoutMs) => {
|
|
63820
|
+
return new Promise((resolve42) => {
|
|
63821
|
+
let parsed;
|
|
63822
|
+
try {
|
|
63823
|
+
parsed = new URL2(url);
|
|
63824
|
+
} catch {
|
|
63825
|
+
resolve42();
|
|
63826
|
+
return;
|
|
63827
|
+
}
|
|
63828
|
+
const isHttps = parsed.protocol === "https:";
|
|
63829
|
+
const lib = isHttps ? https : http;
|
|
63830
|
+
const body = JSON.stringify(payload);
|
|
63831
|
+
const opts = {
|
|
63832
|
+
method: "POST",
|
|
63833
|
+
hostname: parsed.hostname,
|
|
63834
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
63835
|
+
path: `${parsed.pathname || "/"}${parsed.search || ""}`,
|
|
63836
|
+
headers: {
|
|
63837
|
+
"content-type": "application/json",
|
|
63838
|
+
"content-length": Buffer.byteLength(body),
|
|
63839
|
+
"user-agent": `slopbrick/${payload.slopbrick_version}`
|
|
63840
|
+
}
|
|
63841
|
+
};
|
|
63842
|
+
let settled = false;
|
|
63843
|
+
const settle = () => {
|
|
63844
|
+
if (settled) return;
|
|
63845
|
+
settled = true;
|
|
63846
|
+
resolve42();
|
|
63847
|
+
};
|
|
63848
|
+
let req;
|
|
63849
|
+
try {
|
|
63850
|
+
req = lib.request(opts, (res) => {
|
|
63851
|
+
res.on("data", () => {
|
|
63852
|
+
});
|
|
63853
|
+
res.on("end", settle);
|
|
63854
|
+
res.on("error", settle);
|
|
63855
|
+
});
|
|
63856
|
+
} catch {
|
|
63857
|
+
settle();
|
|
63858
|
+
return;
|
|
63859
|
+
}
|
|
63860
|
+
req.on("error", settle);
|
|
63861
|
+
req.setTimeout(timeoutMs, () => {
|
|
63862
|
+
req.destroy();
|
|
63863
|
+
settle();
|
|
63864
|
+
});
|
|
63865
|
+
try {
|
|
63866
|
+
req.end(body);
|
|
63867
|
+
} catch {
|
|
63868
|
+
settle();
|
|
63869
|
+
}
|
|
63870
|
+
});
|
|
63871
|
+
};
|
|
63872
|
+
|
|
63873
|
+
// src/beacon/index.ts
|
|
63874
|
+
var BEACON_TIMEOUT_MS = 5e3;
|
|
63875
|
+
var BeaconEmitter = class {
|
|
63876
|
+
flag;
|
|
63877
|
+
envEndpoint;
|
|
63878
|
+
version;
|
|
63879
|
+
transport;
|
|
63880
|
+
constructor(opts) {
|
|
63881
|
+
this.flag = opts.flag;
|
|
63882
|
+
this.envEndpoint = opts.envEndpoint;
|
|
63883
|
+
this.version = opts.version;
|
|
63884
|
+
this.transport = opts.transport ?? sendBeacon;
|
|
63885
|
+
}
|
|
63886
|
+
/**
|
|
63887
|
+
* The two-gate opt-in. Returns true only when the user opted in
|
|
63888
|
+
* AND the endpoint env var is present and non-empty. Empty
|
|
63889
|
+
* string is treated as unset.
|
|
63890
|
+
*/
|
|
63891
|
+
shouldFire() {
|
|
63892
|
+
return this.flag && !!this.envEndpoint && this.envEndpoint.length > 0;
|
|
63893
|
+
}
|
|
63894
|
+
/**
|
|
63895
|
+
* Build the locked 8-field payload and hand it to the transport.
|
|
63896
|
+
* Never throws. The returned Promise resolves when the transport
|
|
63897
|
+
* resolves — which is always, by contract.
|
|
63898
|
+
*/
|
|
63899
|
+
async emit(stats) {
|
|
63900
|
+
if (!this.shouldFire()) return;
|
|
63901
|
+
const payload = {
|
|
63902
|
+
schema_version: "1",
|
|
63903
|
+
slopbrick_version: this.version,
|
|
63904
|
+
scan_id: stats.scanId,
|
|
63905
|
+
file_count: stats.fileCount,
|
|
63906
|
+
rule_count: stats.ruleCount,
|
|
63907
|
+
duration_ms: stats.durationMs,
|
|
63908
|
+
platform: process.platform,
|
|
63909
|
+
node_version: process.version
|
|
63910
|
+
};
|
|
63911
|
+
try {
|
|
63912
|
+
await this.transport(this.envEndpoint, payload, BEACON_TIMEOUT_MS);
|
|
63913
|
+
} catch {
|
|
63914
|
+
}
|
|
63915
|
+
}
|
|
63916
|
+
};
|
|
63917
|
+
|
|
62097
63918
|
// src/cli/program.ts
|
|
62098
63919
|
init_scan2();
|
|
62099
63920
|
process.on("uncaughtException", (err) => {
|
|
@@ -62102,7 +63923,7 @@ process.on("uncaughtException", (err) => {
|
|
|
62102
63923
|
});
|
|
62103
63924
|
async function runCli({ start }) {
|
|
62104
63925
|
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)");
|
|
63926
|
+
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
63927
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
62107
63928
|
registerInit(program);
|
|
62108
63929
|
registerInstall(program);
|
|
@@ -62155,10 +63976,25 @@ async function runCli({ start }) {
|
|
|
62155
63976
|
config,
|
|
62156
63977
|
noIncreaseFailure,
|
|
62157
63978
|
baseline,
|
|
62158
|
-
machineReadableStdout
|
|
63979
|
+
machineReadableStdout,
|
|
63980
|
+
scanStats
|
|
62159
63981
|
} = await runScan(options, paths);
|
|
62160
63982
|
const scanElapsed = Math.round(performance.now() - scanStart);
|
|
62161
63983
|
const totalElapsed = Math.round(performance.now() - start);
|
|
63984
|
+
const beaconEnv = process.env.SLOPBRICK_TELEMETRY_ENDPOINT;
|
|
63985
|
+
if (options.reportUsage && beaconEnv && command.name() === "scan") {
|
|
63986
|
+
const beacon = new BeaconEmitter({
|
|
63987
|
+
flag: true,
|
|
63988
|
+
envEndpoint: beaconEnv,
|
|
63989
|
+
version: VERSION
|
|
63990
|
+
});
|
|
63991
|
+
void beacon.emit({
|
|
63992
|
+
scanId: scanStats.scanId,
|
|
63993
|
+
fileCount: scanStats.fileCount,
|
|
63994
|
+
ruleCount: scanStats.ruleCount,
|
|
63995
|
+
durationMs: scanStats.durationMs
|
|
63996
|
+
});
|
|
63997
|
+
}
|
|
62162
63998
|
if (options.baseline) {
|
|
62163
63999
|
const cwd2 = resolve41(options.workspace ?? process.cwd());
|
|
62164
64000
|
const configHash = hashConfig(config);
|