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.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var VERSION;
|
|
|
36
36
|
var init_header = __esm({
|
|
37
37
|
"src/types/_header.ts"() {
|
|
38
38
|
"use strict";
|
|
39
|
-
VERSION = "0.
|
|
39
|
+
VERSION = "0.25.0";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -326,6 +326,38 @@ var init_defaults = __esm({
|
|
|
326
326
|
*/
|
|
327
327
|
testIntelligence: {
|
|
328
328
|
missingEdgeCase: false
|
|
329
|
+
},
|
|
330
|
+
/**
|
|
331
|
+
* v0.25.0: self-scan exclude paths. Defaults cover the three paths
|
|
332
|
+
* that are always false positives when scanning the slopbrick repo
|
|
333
|
+
* itself:
|
|
334
|
+
*
|
|
335
|
+
* - `src/rules/**` — rule definitions contain example patterns
|
|
336
|
+
* the rules themselves detect (self-fire). E.g. a
|
|
337
|
+
* `security/sql-construction` rule's source file has a SQL
|
|
338
|
+
* concat string literal that fires its own regex.
|
|
339
|
+
* - `tests/fixtures/**` — test fixtures contain intentional bad
|
|
340
|
+
* code that the rules must fire on to be useful (each fixture
|
|
341
|
+
* is a positive test case). Scanning them in a self-scan
|
|
342
|
+
* produces ~70 false-positive "issues" that are really just
|
|
343
|
+
* test data.
|
|
344
|
+
* - `tests/rules/**` — rule test files contain expected-issue
|
|
345
|
+
* assertions, also meta-code.
|
|
346
|
+
*
|
|
347
|
+
* Three patterns, ~70 issues removed per self-scan. Combined with
|
|
348
|
+
* the v0.25.0 graded security cap, this restores the v9 plan's
|
|
349
|
+
* "security ≥ 80" criterion (unachievable in v0.24.0 due to 90
|
|
350
|
+
* self-scan FPs collapsing the score to 0).
|
|
351
|
+
*
|
|
352
|
+
* Set `selfScan: { excludePaths: [] }` in `slopbrick.config.mjs`
|
|
353
|
+
* to opt out and scan every file (legacy behavior).
|
|
354
|
+
*/
|
|
355
|
+
selfScan: {
|
|
356
|
+
excludePaths: [
|
|
357
|
+
"src/rules/**",
|
|
358
|
+
"tests/fixtures/**",
|
|
359
|
+
"tests/rules/**"
|
|
360
|
+
]
|
|
329
361
|
}
|
|
330
362
|
};
|
|
331
363
|
}
|
|
@@ -27622,6 +27654,267 @@ var init_import_path_mismatch = __esm({
|
|
|
27622
27654
|
}
|
|
27623
27655
|
});
|
|
27624
27656
|
|
|
27657
|
+
// src/rules/cpp/c-style-cast.ts
|
|
27658
|
+
var C_STYLE_CAST_REGEX, NAMED_CAST_PREFIX_REGEX, cppCStyleCastRule;
|
|
27659
|
+
var init_c_style_cast = __esm({
|
|
27660
|
+
"src/rules/cpp/c-style-cast.ts"() {
|
|
27661
|
+
"use strict";
|
|
27662
|
+
init_rule();
|
|
27663
|
+
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;
|
|
27664
|
+
NAMED_CAST_PREFIX_REGEX = /\b(?:static|reinterpret|const|dynamic)_cast\s*<[^>]*>\s*\($/;
|
|
27665
|
+
cppCStyleCastRule = createRule({
|
|
27666
|
+
id: "cpp/c-style-cast",
|
|
27667
|
+
category: "typo",
|
|
27668
|
+
severity: "low",
|
|
27669
|
+
aiSpecific: true,
|
|
27670
|
+
description: "C-style cast `(int)x` \u2014 use static_cast / reinterpret_cast / const_cast so the kind is greppable.",
|
|
27671
|
+
create(_context) {
|
|
27672
|
+
return {};
|
|
27673
|
+
},
|
|
27674
|
+
analyze(_context, facts) {
|
|
27675
|
+
const issues = [];
|
|
27676
|
+
const source = facts.v2?._source;
|
|
27677
|
+
if (!source) return issues;
|
|
27678
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27679
|
+
let m;
|
|
27680
|
+
C_STYLE_CAST_REGEX.lastIndex = 0;
|
|
27681
|
+
while ((m = C_STYLE_CAST_REGEX.exec(source)) !== null) {
|
|
27682
|
+
const innerMatch = /\(\s*([^)]+?)\s*\)/.exec(m[0]) ?? [];
|
|
27683
|
+
const inner = (innerMatch[1] ?? "").trim();
|
|
27684
|
+
const looksLikeCast = /\b(?:int|long|short|char|float|double|bool|unsigned|signed|size_t)\b/.test(inner) || /[*&]/.test(inner);
|
|
27685
|
+
if (!looksLikeCast) continue;
|
|
27686
|
+
const before = source.slice(Math.max(0, m.index - 40), m.index);
|
|
27687
|
+
if (NAMED_CAST_PREFIX_REGEX.test(before)) continue;
|
|
27688
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
27689
|
+
issues.push({
|
|
27690
|
+
ruleId: "cpp/c-style-cast",
|
|
27691
|
+
category: "typo",
|
|
27692
|
+
severity: "low",
|
|
27693
|
+
aiSpecific: true,
|
|
27694
|
+
message: `C-style cast at line ${line} \u2014 use static_cast / reinterpret_cast / const_cast`,
|
|
27695
|
+
line,
|
|
27696
|
+
column: 1,
|
|
27697
|
+
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."
|
|
27698
|
+
});
|
|
27699
|
+
}
|
|
27700
|
+
return issues;
|
|
27701
|
+
}
|
|
27702
|
+
});
|
|
27703
|
+
}
|
|
27704
|
+
});
|
|
27705
|
+
|
|
27706
|
+
// src/rules/cpp/magic-numbers.ts
|
|
27707
|
+
var MAGIC_NUMBER_REGEX, COMPARE_OR_RETURN_REGEX, cppMagicNumbersRule;
|
|
27708
|
+
var init_magic_numbers = __esm({
|
|
27709
|
+
"src/rules/cpp/magic-numbers.ts"() {
|
|
27710
|
+
"use strict";
|
|
27711
|
+
init_rule();
|
|
27712
|
+
MAGIC_NUMBER_REGEX = /\b(\d+(?:\.\d+)?)\b/g;
|
|
27713
|
+
COMPARE_OR_RETURN_REGEX = /\b(?:if|while|return|for)\s*[\(\s]?[^;]*[<>=!][^;]*/;
|
|
27714
|
+
cppMagicNumbersRule = createRule({
|
|
27715
|
+
id: "cpp/magic-numbers",
|
|
27716
|
+
category: "typo",
|
|
27717
|
+
severity: "low",
|
|
27718
|
+
aiSpecific: true,
|
|
27719
|
+
description: "Magic-number literal in a comparison / return \u2014 name it: `constexpr int MAX = 1024;`.",
|
|
27720
|
+
create(_context) {
|
|
27721
|
+
return {};
|
|
27722
|
+
},
|
|
27723
|
+
analyze(_context, facts) {
|
|
27724
|
+
const issues = [];
|
|
27725
|
+
const source = facts.v2?._source;
|
|
27726
|
+
if (!source) return issues;
|
|
27727
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27728
|
+
const lines = source.split("\n");
|
|
27729
|
+
const allowSet = /* @__PURE__ */ new Set([
|
|
27730
|
+
"1024",
|
|
27731
|
+
"65535",
|
|
27732
|
+
"65536",
|
|
27733
|
+
"86400",
|
|
27734
|
+
"3600",
|
|
27735
|
+
"1000",
|
|
27736
|
+
"100.0",
|
|
27737
|
+
"60",
|
|
27738
|
+
"24",
|
|
27739
|
+
"7",
|
|
27740
|
+
"365",
|
|
27741
|
+
"256",
|
|
27742
|
+
"255",
|
|
27743
|
+
"0",
|
|
27744
|
+
"1",
|
|
27745
|
+
"2",
|
|
27746
|
+
"3",
|
|
27747
|
+
"4",
|
|
27748
|
+
"5"
|
|
27749
|
+
]);
|
|
27750
|
+
for (let i = 0; i < lines.length; i++) {
|
|
27751
|
+
const line = lines[i] ?? "";
|
|
27752
|
+
if (!COMPARE_OR_RETURN_REGEX.test(line)) continue;
|
|
27753
|
+
let m;
|
|
27754
|
+
MAGIC_NUMBER_REGEX.lastIndex = 0;
|
|
27755
|
+
while ((m = MAGIC_NUMBER_REGEX.exec(line)) !== null) {
|
|
27756
|
+
const literal = m[0] ?? "";
|
|
27757
|
+
if (allowSet.has(literal)) continue;
|
|
27758
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(line.slice(0, m.index).trimEnd())) continue;
|
|
27759
|
+
const prevLine = i > 0 ? lines[i - 1] ?? "" : "";
|
|
27760
|
+
if (/\b(?:constexpr|const)\s+\w+\s*=\s*$/.test(prevLine.trim())) continue;
|
|
27761
|
+
issues.push({
|
|
27762
|
+
ruleId: "cpp/magic-numbers",
|
|
27763
|
+
category: "typo",
|
|
27764
|
+
severity: "low",
|
|
27765
|
+
aiSpecific: true,
|
|
27766
|
+
message: `magic number ${literal} at line ${i + 1} \u2014 name it: constexpr int MAX = ${literal};`,
|
|
27767
|
+
line: i + 1,
|
|
27768
|
+
column: 1,
|
|
27769
|
+
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.'
|
|
27770
|
+
});
|
|
27771
|
+
}
|
|
27772
|
+
}
|
|
27773
|
+
return issues;
|
|
27774
|
+
}
|
|
27775
|
+
});
|
|
27776
|
+
}
|
|
27777
|
+
});
|
|
27778
|
+
|
|
27779
|
+
// src/rules/cpp/printf-debug.ts
|
|
27780
|
+
var PRINTF_FAMILY_REGEX, COUT_LITERAL_REGEX, STD_COUT_BARE_REGEX, THRESHOLD_DEFAULT, cppPrintfDebugRule;
|
|
27781
|
+
var init_printf_debug = __esm({
|
|
27782
|
+
"src/rules/cpp/printf-debug.ts"() {
|
|
27783
|
+
"use strict";
|
|
27784
|
+
init_rule();
|
|
27785
|
+
PRINTF_FAMILY_REGEX = /\b(?:printf|fprintf|sprintf|snprintf)\s*\(/g;
|
|
27786
|
+
COUT_LITERAL_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*"[^"]*"/g;
|
|
27787
|
+
STD_COUT_BARE_REGEX = /std\s*::\s*(?:cout|cerr|clog)\s*<<\s*'[^']*'/g;
|
|
27788
|
+
THRESHOLD_DEFAULT = 1;
|
|
27789
|
+
cppPrintfDebugRule = createRule({
|
|
27790
|
+
id: "cpp/printf-debug",
|
|
27791
|
+
category: "typo",
|
|
27792
|
+
severity: "low",
|
|
27793
|
+
aiSpecific: true,
|
|
27794
|
+
description: "printf family or std::cout debug printing \u2014 use spdlog / glog / AbslLog for level-controlled output.",
|
|
27795
|
+
create(_context) {
|
|
27796
|
+
return { threshold: THRESHOLD_DEFAULT };
|
|
27797
|
+
},
|
|
27798
|
+
analyze(context, facts) {
|
|
27799
|
+
const issues = [];
|
|
27800
|
+
const source = facts.v2?._source;
|
|
27801
|
+
if (!source) return issues;
|
|
27802
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27803
|
+
const printfCount = (source.match(PRINTF_FAMILY_REGEX) ?? []).length;
|
|
27804
|
+
let viaPrintf = false;
|
|
27805
|
+
if (printfCount > context.threshold) {
|
|
27806
|
+
const line = source.indexOf("printf");
|
|
27807
|
+
if (line !== -1) viaPrintf = true;
|
|
27808
|
+
}
|
|
27809
|
+
const coutHit = COUT_LITERAL_REGEX.test(source) || STD_COUT_BARE_REGEX.test(source);
|
|
27810
|
+
if (!viaPrintf && !coutHit) return issues;
|
|
27811
|
+
issues.push({
|
|
27812
|
+
ruleId: "cpp/printf-debug",
|
|
27813
|
+
category: "typo",
|
|
27814
|
+
severity: "low",
|
|
27815
|
+
aiSpecific: true,
|
|
27816
|
+
message: viaPrintf ? `${printfCount} printf-family calls \u2014 use spdlog / glog / AbslLog` : "std::cout/cerr/clog with a string literal \u2014 use spdlog / glog / AbslLog",
|
|
27817
|
+
line: 1,
|
|
27818
|
+
column: 1,
|
|
27819
|
+
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."
|
|
27820
|
+
});
|
|
27821
|
+
return issues;
|
|
27822
|
+
}
|
|
27823
|
+
});
|
|
27824
|
+
}
|
|
27825
|
+
});
|
|
27826
|
+
|
|
27827
|
+
// src/rules/cpp/raw-new-delete.ts
|
|
27828
|
+
var NEW_TYPE_REGEX, DELETE_REGEX, MIN_PAIRS_DEFAULT, cppRawNewDeleteRule;
|
|
27829
|
+
var init_raw_new_delete = __esm({
|
|
27830
|
+
"src/rules/cpp/raw-new-delete.ts"() {
|
|
27831
|
+
"use strict";
|
|
27832
|
+
init_rule();
|
|
27833
|
+
NEW_TYPE_REGEX = /\bnew\s+[A-Za-z_]\w*\s*\(/g;
|
|
27834
|
+
DELETE_REGEX = /\bdelete\s+/g;
|
|
27835
|
+
MIN_PAIRS_DEFAULT = 1;
|
|
27836
|
+
cppRawNewDeleteRule = createRule({
|
|
27837
|
+
id: "cpp/raw-new-delete",
|
|
27838
|
+
category: "typo",
|
|
27839
|
+
severity: "low",
|
|
27840
|
+
aiSpecific: true,
|
|
27841
|
+
description: "Manual `new` / `delete` pairing \u2014 use std::make_unique / std::unique_ptr instead.",
|
|
27842
|
+
create(_context) {
|
|
27843
|
+
return { minPairs: MIN_PAIRS_DEFAULT };
|
|
27844
|
+
},
|
|
27845
|
+
analyze(context, facts) {
|
|
27846
|
+
const issues = [];
|
|
27847
|
+
const source = facts.v2?._source;
|
|
27848
|
+
if (!source) return issues;
|
|
27849
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27850
|
+
let newCount = 0;
|
|
27851
|
+
let m;
|
|
27852
|
+
NEW_TYPE_REGEX.lastIndex = 0;
|
|
27853
|
+
while ((m = NEW_TYPE_REGEX.exec(source)) !== null) newCount++;
|
|
27854
|
+
let deleteCount = 0;
|
|
27855
|
+
DELETE_REGEX.lastIndex = 0;
|
|
27856
|
+
while ((m = DELETE_REGEX.exec(source)) !== null) deleteCount++;
|
|
27857
|
+
if (newCount <= context.minPairs) return issues;
|
|
27858
|
+
if (deleteCount <= context.minPairs) return issues;
|
|
27859
|
+
issues.push({
|
|
27860
|
+
ruleId: "cpp/raw-new-delete",
|
|
27861
|
+
category: "typo",
|
|
27862
|
+
severity: "low",
|
|
27863
|
+
aiSpecific: true,
|
|
27864
|
+
message: `manual new/delete pairing (${newCount} new, ${deleteCount} delete) \u2014 wrap in std::unique_ptr`,
|
|
27865
|
+
line: 1,
|
|
27866
|
+
column: 1,
|
|
27867
|
+
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."
|
|
27868
|
+
});
|
|
27869
|
+
return issues;
|
|
27870
|
+
}
|
|
27871
|
+
});
|
|
27872
|
+
}
|
|
27873
|
+
});
|
|
27874
|
+
|
|
27875
|
+
// src/rules/cpp/using-namespace-std.ts
|
|
27876
|
+
var USING_NAMESPACE_STD_REGEX, cppUsingNamespaceStdRule;
|
|
27877
|
+
var init_using_namespace_std = __esm({
|
|
27878
|
+
"src/rules/cpp/using-namespace-std.ts"() {
|
|
27879
|
+
"use strict";
|
|
27880
|
+
init_rule();
|
|
27881
|
+
USING_NAMESPACE_STD_REGEX = /\busing\s+namespace\s+std\s*;/g;
|
|
27882
|
+
cppUsingNamespaceStdRule = createRule({
|
|
27883
|
+
id: "cpp/using-namespace-std",
|
|
27884
|
+
category: "logic",
|
|
27885
|
+
severity: "medium",
|
|
27886
|
+
aiSpecific: true,
|
|
27887
|
+
description: "`using namespace std;` in a header \u2014 pollutes every consumer. Use `using std::foo;` or qualify names.",
|
|
27888
|
+
create(_context) {
|
|
27889
|
+
return {};
|
|
27890
|
+
},
|
|
27891
|
+
analyze(_context, facts) {
|
|
27892
|
+
const issues = [];
|
|
27893
|
+
const source = facts.v2?._source;
|
|
27894
|
+
if (!source) return issues;
|
|
27895
|
+
if (!/\.(cpp|cc|cxx|h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27896
|
+
if (!/\.(h|hpp|hh|hxx|H)$/i.test(facts.filePath)) return issues;
|
|
27897
|
+
let m;
|
|
27898
|
+
USING_NAMESPACE_STD_REGEX.lastIndex = 0;
|
|
27899
|
+
while ((m = USING_NAMESPACE_STD_REGEX.exec(source)) !== null) {
|
|
27900
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
27901
|
+
issues.push({
|
|
27902
|
+
ruleId: "cpp/using-namespace-std",
|
|
27903
|
+
category: "logic",
|
|
27904
|
+
severity: "medium",
|
|
27905
|
+
aiSpecific: true,
|
|
27906
|
+
message: `using namespace std; at line ${line} \u2014 pollutes every consumer of this header`,
|
|
27907
|
+
line,
|
|
27908
|
+
column: 1,
|
|
27909
|
+
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."
|
|
27910
|
+
});
|
|
27911
|
+
}
|
|
27912
|
+
return issues;
|
|
27913
|
+
}
|
|
27914
|
+
});
|
|
27915
|
+
}
|
|
27916
|
+
});
|
|
27917
|
+
|
|
27625
27918
|
// src/rules/db/duplicate-index.ts
|
|
27626
27919
|
function parseStatements(raw) {
|
|
27627
27920
|
let result;
|
|
@@ -29663,6 +29956,534 @@ var init_identical_block = __esm({
|
|
|
29663
29956
|
}
|
|
29664
29957
|
});
|
|
29665
29958
|
|
|
29959
|
+
// src/engine/dedup/tokenize.ts
|
|
29960
|
+
function stripComments(source) {
|
|
29961
|
+
let out = "";
|
|
29962
|
+
let i = 0;
|
|
29963
|
+
while (i < source.length) {
|
|
29964
|
+
const ch = source[i];
|
|
29965
|
+
const next = source[i + 1];
|
|
29966
|
+
if (ch === "/" && next === "/") {
|
|
29967
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
29968
|
+
continue;
|
|
29969
|
+
}
|
|
29970
|
+
if (ch === "/" && next === "*") {
|
|
29971
|
+
i += 2;
|
|
29972
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
29973
|
+
i += 2;
|
|
29974
|
+
continue;
|
|
29975
|
+
}
|
|
29976
|
+
out += ch;
|
|
29977
|
+
i++;
|
|
29978
|
+
}
|
|
29979
|
+
return out;
|
|
29980
|
+
}
|
|
29981
|
+
function tokenize(source) {
|
|
29982
|
+
return source.toLowerCase().split(/[^a-z0-9_]+/u).filter((t) => t.length > 0);
|
|
29983
|
+
}
|
|
29984
|
+
function shingleHash(shingle) {
|
|
29985
|
+
const h = (0, import_node_crypto.createHash)("sha1").update(shingle).digest();
|
|
29986
|
+
return (h[0] ?? 0) << 24 | (h[1] ?? 0) << 16 | (h[2] ?? 0) << 8 | (h[3] ?? 0);
|
|
29987
|
+
}
|
|
29988
|
+
function shingleSet(source, config = {}) {
|
|
29989
|
+
const { k } = { ...DEFAULT_CONFIG2, ...config };
|
|
29990
|
+
const tokens = tokenize(stripComments(source));
|
|
29991
|
+
if (tokens.length === 0) return /* @__PURE__ */ new Set();
|
|
29992
|
+
if (tokens.length < k) {
|
|
29993
|
+
return /* @__PURE__ */ new Set([shingleHash(tokens.join(" "))]);
|
|
29994
|
+
}
|
|
29995
|
+
const out = /* @__PURE__ */ new Set();
|
|
29996
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
29997
|
+
out.add(shingleHash(tokens.slice(i, i + k).join(" ")));
|
|
29998
|
+
}
|
|
29999
|
+
return out;
|
|
30000
|
+
}
|
|
30001
|
+
var import_node_crypto, DEFAULT_CONFIG2;
|
|
30002
|
+
var init_tokenize = __esm({
|
|
30003
|
+
"src/engine/dedup/tokenize.ts"() {
|
|
30004
|
+
"use strict";
|
|
30005
|
+
import_node_crypto = require("crypto");
|
|
30006
|
+
DEFAULT_CONFIG2 = { k: 5 };
|
|
30007
|
+
}
|
|
30008
|
+
});
|
|
30009
|
+
|
|
30010
|
+
// src/engine/dedup/minhash.ts
|
|
30011
|
+
function mix64(x) {
|
|
30012
|
+
x ^= x >> 33n;
|
|
30013
|
+
x = x * 0xff51afd7ed558ccdn & 0xffffffffffffffffn;
|
|
30014
|
+
x ^= x >> 33n;
|
|
30015
|
+
x = x * 0xc4ceb9fe1a85ec53n & 0xffffffffffffffffn;
|
|
30016
|
+
x ^= x >> 33n;
|
|
30017
|
+
return x;
|
|
30018
|
+
}
|
|
30019
|
+
function bmod(a, m) {
|
|
30020
|
+
const r = a % m;
|
|
30021
|
+
return r < 0n ? r + m : r;
|
|
30022
|
+
}
|
|
30023
|
+
function hashParams(i) {
|
|
30024
|
+
const a = mix64(BigInt(i) * 0x9e3779b97f4a7c15n) & 0xffffffffffffffffn | 1n;
|
|
30025
|
+
const b = mix64(BigInt(i) * 0xbf58476d1ce4e5b9n) & 0xffffffffffffffffn;
|
|
30026
|
+
return [a, b];
|
|
30027
|
+
}
|
|
30028
|
+
function hashAt(i, x) {
|
|
30029
|
+
const [a, b] = hashParams(i);
|
|
30030
|
+
const xb = BigInt(x >>> 0);
|
|
30031
|
+
const combined = a * xb + b;
|
|
30032
|
+
const h = Number(bmod(combined, PRIME) & 0xffffffffn);
|
|
30033
|
+
return h >>> 0;
|
|
30034
|
+
}
|
|
30035
|
+
function minHash(tokens, config = {}) {
|
|
30036
|
+
const { numHashes } = { ...DEFAULT_CONFIG3, ...config };
|
|
30037
|
+
const sig = new Uint32Array(numHashes);
|
|
30038
|
+
sig.fill(4294967295);
|
|
30039
|
+
const tokenIter = tokens instanceof Set ? tokens : Array.from(tokens);
|
|
30040
|
+
for (const t of tokenIter) {
|
|
30041
|
+
for (let i = 0; i < numHashes; i++) {
|
|
30042
|
+
const h = hashAt(i, t);
|
|
30043
|
+
if (h < (sig[i] ?? 4294967295)) sig[i] = h;
|
|
30044
|
+
}
|
|
30045
|
+
}
|
|
30046
|
+
return sig;
|
|
30047
|
+
}
|
|
30048
|
+
function minHashSimilarity(a, b) {
|
|
30049
|
+
if (a.length !== b.length) {
|
|
30050
|
+
throw new Error(`minHashSimilarity: length mismatch ${a.length} vs ${b.length}`);
|
|
30051
|
+
}
|
|
30052
|
+
if (a.length === 0) return 1;
|
|
30053
|
+
let matches = 0;
|
|
30054
|
+
for (let i = 0; i < a.length; i++) {
|
|
30055
|
+
if (a[i] === b[i]) matches++;
|
|
30056
|
+
}
|
|
30057
|
+
return matches / a.length;
|
|
30058
|
+
}
|
|
30059
|
+
var import_node_crypto2, PRIME, DEFAULT_CONFIG3;
|
|
30060
|
+
var init_minhash = __esm({
|
|
30061
|
+
"src/engine/dedup/minhash.ts"() {
|
|
30062
|
+
"use strict";
|
|
30063
|
+
import_node_crypto2 = require("crypto");
|
|
30064
|
+
PRIME = (1n << 61n) - 1n;
|
|
30065
|
+
DEFAULT_CONFIG3 = {
|
|
30066
|
+
numHashes: 128
|
|
30067
|
+
};
|
|
30068
|
+
}
|
|
30069
|
+
});
|
|
30070
|
+
|
|
30071
|
+
// src/rules/dup/near-duplicate.ts
|
|
30072
|
+
var DEFAULT_CONTEXT, signatureCache, nearDuplicateRule;
|
|
30073
|
+
var init_near_duplicate = __esm({
|
|
30074
|
+
"src/rules/dup/near-duplicate.ts"() {
|
|
30075
|
+
"use strict";
|
|
30076
|
+
init_rule();
|
|
30077
|
+
init_tokenize();
|
|
30078
|
+
init_minhash();
|
|
30079
|
+
DEFAULT_CONTEXT = {
|
|
30080
|
+
threshold: 0.7,
|
|
30081
|
+
k: 5
|
|
30082
|
+
};
|
|
30083
|
+
signatureCache = /* @__PURE__ */ new Map();
|
|
30084
|
+
nearDuplicateRule = createRule({
|
|
30085
|
+
id: "dup/near-duplicate",
|
|
30086
|
+
category: "logic",
|
|
30087
|
+
severity: "medium",
|
|
30088
|
+
aiSpecific: true,
|
|
30089
|
+
description: "Near-duplicate file (Type-2 clone) \u2014 token-set Jaccard similarity > 0.7 after MinHash. Catches AI whitespace/comment refactors.",
|
|
30090
|
+
create(_context) {
|
|
30091
|
+
return { ...DEFAULT_CONTEXT };
|
|
30092
|
+
},
|
|
30093
|
+
analyze(context, facts) {
|
|
30094
|
+
const ctx = { ...DEFAULT_CONTEXT, ...context };
|
|
30095
|
+
const issues = [];
|
|
30096
|
+
const source = facts.v2?._source;
|
|
30097
|
+
if (!source) return issues;
|
|
30098
|
+
const filePath = facts.filePath;
|
|
30099
|
+
if (!filePath) return issues;
|
|
30100
|
+
const shingles = shingleSet(source, { k: ctx.k });
|
|
30101
|
+
if (shingles.size === 0) return issues;
|
|
30102
|
+
const sig = minHash(shingles);
|
|
30103
|
+
const matched = [];
|
|
30104
|
+
for (const [prevPath, prevSig] of signatureCache.entries()) {
|
|
30105
|
+
const sim = minHashSimilarity(sig, prevSig);
|
|
30106
|
+
if (sim >= ctx.threshold) {
|
|
30107
|
+
matched.push({ path: prevPath, similarity: sim });
|
|
30108
|
+
}
|
|
30109
|
+
}
|
|
30110
|
+
if (matched.length > 0) {
|
|
30111
|
+
matched.sort((a, b) => b.similarity - a.similarity);
|
|
30112
|
+
const best = matched[0];
|
|
30113
|
+
issues.push({
|
|
30114
|
+
ruleId: "dup/near-duplicate",
|
|
30115
|
+
category: "logic",
|
|
30116
|
+
severity: "medium",
|
|
30117
|
+
aiSpecific: true,
|
|
30118
|
+
message: `Near-duplicate of ${best.path} (Jaccard ${(best.similarity * 100).toFixed(1)}%, threshold ${(ctx.threshold * 100).toFixed(0)}%)`,
|
|
30119
|
+
line: 1,
|
|
30120
|
+
column: 1,
|
|
30121
|
+
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."
|
|
30122
|
+
});
|
|
30123
|
+
}
|
|
30124
|
+
signatureCache.set(filePath, sig);
|
|
30125
|
+
return issues;
|
|
30126
|
+
}
|
|
30127
|
+
});
|
|
30128
|
+
}
|
|
30129
|
+
});
|
|
30130
|
+
|
|
30131
|
+
// src/engine/dedup/lsh.ts
|
|
30132
|
+
var import_node_crypto3;
|
|
30133
|
+
var init_lsh = __esm({
|
|
30134
|
+
"src/engine/dedup/lsh.ts"() {
|
|
30135
|
+
"use strict";
|
|
30136
|
+
import_node_crypto3 = require("crypto");
|
|
30137
|
+
}
|
|
30138
|
+
});
|
|
30139
|
+
|
|
30140
|
+
// src/engine/dedup/structural-clone.ts
|
|
30141
|
+
function stripComments2(source) {
|
|
30142
|
+
let out = "";
|
|
30143
|
+
let i = 0;
|
|
30144
|
+
while (i < source.length) {
|
|
30145
|
+
const ch = source[i];
|
|
30146
|
+
const next = source[i + 1];
|
|
30147
|
+
if (ch === "/" && next === "/") {
|
|
30148
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
30149
|
+
continue;
|
|
30150
|
+
}
|
|
30151
|
+
if (ch === "/" && next === "*") {
|
|
30152
|
+
i += 2;
|
|
30153
|
+
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i++;
|
|
30154
|
+
i += 2;
|
|
30155
|
+
continue;
|
|
30156
|
+
}
|
|
30157
|
+
out += ch;
|
|
30158
|
+
i++;
|
|
30159
|
+
}
|
|
30160
|
+
return out;
|
|
30161
|
+
}
|
|
30162
|
+
function canonicalTokens(source) {
|
|
30163
|
+
const stripped = stripComments2(source);
|
|
30164
|
+
const out = [];
|
|
30165
|
+
TOKEN_REGEX.lastIndex = 0;
|
|
30166
|
+
let m;
|
|
30167
|
+
while ((m = TOKEN_REGEX.exec(stripped)) !== null) {
|
|
30168
|
+
const [, ident, num, punct, dq, sq, bq] = m;
|
|
30169
|
+
if (ident !== void 0) {
|
|
30170
|
+
const lower = ident.toLowerCase();
|
|
30171
|
+
if (BOOL_TOKENS.has(lower)) {
|
|
30172
|
+
out.push("bool");
|
|
30173
|
+
} else if (KEYWORDS.has(lower)) {
|
|
30174
|
+
out.push(lower);
|
|
30175
|
+
} else {
|
|
30176
|
+
out.push("id");
|
|
30177
|
+
}
|
|
30178
|
+
continue;
|
|
30179
|
+
}
|
|
30180
|
+
if (num !== void 0) {
|
|
30181
|
+
out.push("num");
|
|
30182
|
+
continue;
|
|
30183
|
+
}
|
|
30184
|
+
if (punct !== void 0) {
|
|
30185
|
+
out.push(punct);
|
|
30186
|
+
continue;
|
|
30187
|
+
}
|
|
30188
|
+
if (dq !== void 0 || sq !== void 0 || bq !== void 0) {
|
|
30189
|
+
out.push("str");
|
|
30190
|
+
continue;
|
|
30191
|
+
}
|
|
30192
|
+
}
|
|
30193
|
+
return out;
|
|
30194
|
+
}
|
|
30195
|
+
function shingleHash2(shingle) {
|
|
30196
|
+
let h = 2166136261;
|
|
30197
|
+
for (let i = 0; i < shingle.length; i++) {
|
|
30198
|
+
h ^= shingle.charCodeAt(i);
|
|
30199
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
30200
|
+
}
|
|
30201
|
+
return h >>> 0;
|
|
30202
|
+
}
|
|
30203
|
+
function structuralShingles(source, config = {}) {
|
|
30204
|
+
const { k, minTokens } = { ...DEFAULT_STRUCTURAL_CONFIG, ...config };
|
|
30205
|
+
const tokens = canonicalTokens(source);
|
|
30206
|
+
if (tokens.length < minTokens) return /* @__PURE__ */ new Set();
|
|
30207
|
+
if (tokens.length < k) {
|
|
30208
|
+
return /* @__PURE__ */ new Set([shingleHash2(tokens.join(" "))]);
|
|
30209
|
+
}
|
|
30210
|
+
const out = /* @__PURE__ */ new Set();
|
|
30211
|
+
for (let i = 0; i + k <= tokens.length; i++) {
|
|
30212
|
+
out.add(shingleHash2(tokens.slice(i, i + k).join(" ")));
|
|
30213
|
+
}
|
|
30214
|
+
return out;
|
|
30215
|
+
}
|
|
30216
|
+
function structuralSignature(source, config = {}) {
|
|
30217
|
+
return minHash(structuralShingles(source, config));
|
|
30218
|
+
}
|
|
30219
|
+
var TOKEN_REGEX, BOOL_TOKENS, KEYWORDS, DEFAULT_STRUCTURAL_CONFIG;
|
|
30220
|
+
var init_structural_clone = __esm({
|
|
30221
|
+
"src/engine/dedup/structural-clone.ts"() {
|
|
30222
|
+
"use strict";
|
|
30223
|
+
init_minhash();
|
|
30224
|
+
init_tokenize();
|
|
30225
|
+
TOKEN_REGEX = /([A-Za-z_$][A-Za-z0-9_$]*)|(0[xX][0-9A-Fa-f]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?[fFdDlL]?)|([(){}\[\];,.<>=+\-*/%!&|^?:])|"((?:[^"\\\n]|\\.)*)"|'((?:[^'\\\n]|\\.)*)'|`((?:[^`\\]|\\.)*)`/g;
|
|
30226
|
+
BOOL_TOKENS = /* @__PURE__ */ new Set(["true", "false", "null", "none", "nil"]);
|
|
30227
|
+
KEYWORDS = /* @__PURE__ */ new Set([
|
|
30228
|
+
// Control flow
|
|
30229
|
+
"if",
|
|
30230
|
+
"else",
|
|
30231
|
+
"for",
|
|
30232
|
+
"while",
|
|
30233
|
+
"do",
|
|
30234
|
+
"switch",
|
|
30235
|
+
"case",
|
|
30236
|
+
"default",
|
|
30237
|
+
"break",
|
|
30238
|
+
"continue",
|
|
30239
|
+
"return",
|
|
30240
|
+
"goto",
|
|
30241
|
+
// Declarations
|
|
30242
|
+
"function",
|
|
30243
|
+
"class",
|
|
30244
|
+
"interface",
|
|
30245
|
+
"struct",
|
|
30246
|
+
"enum",
|
|
30247
|
+
"trait",
|
|
30248
|
+
"def",
|
|
30249
|
+
"fn",
|
|
30250
|
+
"fun",
|
|
30251
|
+
"func",
|
|
30252
|
+
"var",
|
|
30253
|
+
"let",
|
|
30254
|
+
"const",
|
|
30255
|
+
"val",
|
|
30256
|
+
"mod",
|
|
30257
|
+
"use",
|
|
30258
|
+
"pub",
|
|
30259
|
+
"impl",
|
|
30260
|
+
"package",
|
|
30261
|
+
"module",
|
|
30262
|
+
"lambda",
|
|
30263
|
+
// Modifiers / visibility
|
|
30264
|
+
"public",
|
|
30265
|
+
"private",
|
|
30266
|
+
"protected",
|
|
30267
|
+
"static",
|
|
30268
|
+
"abstract",
|
|
30269
|
+
"final",
|
|
30270
|
+
"sealed",
|
|
30271
|
+
"override",
|
|
30272
|
+
"virtual",
|
|
30273
|
+
"readonly",
|
|
30274
|
+
"async",
|
|
30275
|
+
"await",
|
|
30276
|
+
"synchronized",
|
|
30277
|
+
"volatile",
|
|
30278
|
+
"transient",
|
|
30279
|
+
"native",
|
|
30280
|
+
"strictfp",
|
|
30281
|
+
"inline",
|
|
30282
|
+
"external",
|
|
30283
|
+
"export",
|
|
30284
|
+
"import",
|
|
30285
|
+
// Type system
|
|
30286
|
+
"type",
|
|
30287
|
+
"interface",
|
|
30288
|
+
"record",
|
|
30289
|
+
"object",
|
|
30290
|
+
"enum",
|
|
30291
|
+
"data",
|
|
30292
|
+
// Pattern / dispatch
|
|
30293
|
+
"match",
|
|
30294
|
+
"select",
|
|
30295
|
+
"when",
|
|
30296
|
+
"where",
|
|
30297
|
+
// Error handling
|
|
30298
|
+
"try",
|
|
30299
|
+
"catch",
|
|
30300
|
+
"finally",
|
|
30301
|
+
"throw",
|
|
30302
|
+
"throws",
|
|
30303
|
+
"raise",
|
|
30304
|
+
"rescue",
|
|
30305
|
+
"ensure",
|
|
30306
|
+
"except",
|
|
30307
|
+
// Logical connectives and tests
|
|
30308
|
+
"and",
|
|
30309
|
+
"or",
|
|
30310
|
+
"not",
|
|
30311
|
+
"in",
|
|
30312
|
+
"is",
|
|
30313
|
+
"as",
|
|
30314
|
+
"instanceof",
|
|
30315
|
+
"typeof",
|
|
30316
|
+
"delete",
|
|
30317
|
+
"void",
|
|
30318
|
+
"new",
|
|
30319
|
+
"this",
|
|
30320
|
+
"super",
|
|
30321
|
+
"self",
|
|
30322
|
+
"yield",
|
|
30323
|
+
// Misc
|
|
30324
|
+
"with",
|
|
30325
|
+
"without",
|
|
30326
|
+
"pass",
|
|
30327
|
+
"assert",
|
|
30328
|
+
"mut",
|
|
30329
|
+
"move",
|
|
30330
|
+
"ref",
|
|
30331
|
+
"out",
|
|
30332
|
+
"params",
|
|
30333
|
+
"operator",
|
|
30334
|
+
"sizeof",
|
|
30335
|
+
"typedef",
|
|
30336
|
+
"union",
|
|
30337
|
+
"register",
|
|
30338
|
+
"extern",
|
|
30339
|
+
"volatile",
|
|
30340
|
+
"goto",
|
|
30341
|
+
// Python-specific
|
|
30342
|
+
"elif",
|
|
30343
|
+
"global",
|
|
30344
|
+
"nonlocal",
|
|
30345
|
+
"del",
|
|
30346
|
+
"with",
|
|
30347
|
+
// Ruby-specific
|
|
30348
|
+
"unless",
|
|
30349
|
+
"until",
|
|
30350
|
+
"begin",
|
|
30351
|
+
"next",
|
|
30352
|
+
"then",
|
|
30353
|
+
"end",
|
|
30354
|
+
"alias",
|
|
30355
|
+
"defined",
|
|
30356
|
+
"super",
|
|
30357
|
+
"yield",
|
|
30358
|
+
"self",
|
|
30359
|
+
// Kotlin-specific
|
|
30360
|
+
"suspend",
|
|
30361
|
+
"lateinit",
|
|
30362
|
+
"companion",
|
|
30363
|
+
"internal",
|
|
30364
|
+
"open",
|
|
30365
|
+
// Rust-specific
|
|
30366
|
+
"unsafe",
|
|
30367
|
+
"dyn",
|
|
30368
|
+
"box",
|
|
30369
|
+
"crate",
|
|
30370
|
+
"where",
|
|
30371
|
+
// Swift-specific
|
|
30372
|
+
"guard",
|
|
30373
|
+
"defer",
|
|
30374
|
+
"repeat",
|
|
30375
|
+
"init",
|
|
30376
|
+
"deinit",
|
|
30377
|
+
"extension",
|
|
30378
|
+
"protocol",
|
|
30379
|
+
"throws",
|
|
30380
|
+
"rethrows",
|
|
30381
|
+
"weak",
|
|
30382
|
+
"unowned",
|
|
30383
|
+
"lazy"
|
|
30384
|
+
]);
|
|
30385
|
+
DEFAULT_STRUCTURAL_CONFIG = {
|
|
30386
|
+
k: 8,
|
|
30387
|
+
minTokens: 60
|
|
30388
|
+
};
|
|
30389
|
+
}
|
|
30390
|
+
});
|
|
30391
|
+
|
|
30392
|
+
// src/engine/dedup/index.ts
|
|
30393
|
+
var init_dedup = __esm({
|
|
30394
|
+
"src/engine/dedup/index.ts"() {
|
|
30395
|
+
"use strict";
|
|
30396
|
+
init_tokenize();
|
|
30397
|
+
init_minhash();
|
|
30398
|
+
init_lsh();
|
|
30399
|
+
init_structural_clone();
|
|
30400
|
+
}
|
|
30401
|
+
});
|
|
30402
|
+
|
|
30403
|
+
// src/rules/dup/structural-clone.ts
|
|
30404
|
+
var DEFAULT_CONTEXT2, structuralCache, identifierCache, structuralCloneRule;
|
|
30405
|
+
var init_structural_clone2 = __esm({
|
|
30406
|
+
"src/rules/dup/structural-clone.ts"() {
|
|
30407
|
+
"use strict";
|
|
30408
|
+
init_rule();
|
|
30409
|
+
init_dedup();
|
|
30410
|
+
DEFAULT_CONTEXT2 = {
|
|
30411
|
+
structuralThreshold: 0.55,
|
|
30412
|
+
verifyThreshold: 0.45,
|
|
30413
|
+
kStruct: 8,
|
|
30414
|
+
kIdent: 5,
|
|
30415
|
+
minTokens: 60,
|
|
30416
|
+
minHits: 1
|
|
30417
|
+
};
|
|
30418
|
+
structuralCache = /* @__PURE__ */ new Map();
|
|
30419
|
+
identifierCache = /* @__PURE__ */ new Map();
|
|
30420
|
+
structuralCloneRule = createRule({
|
|
30421
|
+
id: "dup/structural-clone",
|
|
30422
|
+
category: "logic",
|
|
30423
|
+
severity: "medium",
|
|
30424
|
+
aiSpecific: true,
|
|
30425
|
+
defaultOff: true,
|
|
30426
|
+
description: "Structural clone (Type-3) \u2014 same shape after identifier canonicalization with possible added/removed statements. Two-stage MinHash: canonical + identifier.",
|
|
30427
|
+
create(_context) {
|
|
30428
|
+
return { ...DEFAULT_CONTEXT2 };
|
|
30429
|
+
},
|
|
30430
|
+
analyze(context, facts) {
|
|
30431
|
+
const ctx = { ...DEFAULT_CONTEXT2, ...context };
|
|
30432
|
+
const issues = [];
|
|
30433
|
+
const source = facts.v2?._source;
|
|
30434
|
+
if (!source) return issues;
|
|
30435
|
+
const filePath = facts.filePath;
|
|
30436
|
+
if (!filePath) return issues;
|
|
30437
|
+
const sigStruct = structuralSignature(source, {
|
|
30438
|
+
k: ctx.kStruct,
|
|
30439
|
+
minTokens: ctx.minTokens
|
|
30440
|
+
});
|
|
30441
|
+
if (sigStruct.every((v) => v === 4294967295)) return issues;
|
|
30442
|
+
const identShingles = shingleSet(source, { k: ctx.kIdent });
|
|
30443
|
+
if (identShingles.size === 0) return issues;
|
|
30444
|
+
const sigIdent = minHash(identShingles);
|
|
30445
|
+
const matched = [];
|
|
30446
|
+
for (const [prevPath, prevStruct] of structuralCache.entries()) {
|
|
30447
|
+
if (prevPath === filePath) continue;
|
|
30448
|
+
const simStruct = minHashSimilarity(sigStruct, prevStruct);
|
|
30449
|
+
if (simStruct < ctx.structuralThreshold) continue;
|
|
30450
|
+
const prevIdent = identifierCache.get(prevPath);
|
|
30451
|
+
if (!prevIdent) continue;
|
|
30452
|
+
const simIdent = minHashSimilarity(sigIdent, prevIdent);
|
|
30453
|
+
if (simIdent <= 0) continue;
|
|
30454
|
+
const score = 2 * simStruct * simIdent / (simStruct + simIdent + 1e-9);
|
|
30455
|
+
if (score >= ctx.verifyThreshold) {
|
|
30456
|
+
matched.push({ path: prevPath, score, simStruct, simIdent });
|
|
30457
|
+
}
|
|
30458
|
+
}
|
|
30459
|
+
if (matched.length >= ctx.minHits) {
|
|
30460
|
+
matched.sort((a, b) => b.score - a.score);
|
|
30461
|
+
const best = matched[0];
|
|
30462
|
+
issues.push({
|
|
30463
|
+
ruleId: "dup/structural-clone",
|
|
30464
|
+
category: "logic",
|
|
30465
|
+
severity: "medium",
|
|
30466
|
+
aiSpecific: true,
|
|
30467
|
+
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)}%)`,
|
|
30468
|
+
line: 1,
|
|
30469
|
+
column: 1,
|
|
30470
|
+
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.)",
|
|
30471
|
+
extras: {
|
|
30472
|
+
structuralDuplicateOf: {
|
|
30473
|
+
file: best.path,
|
|
30474
|
+
score: best.score
|
|
30475
|
+
}
|
|
30476
|
+
}
|
|
30477
|
+
});
|
|
30478
|
+
}
|
|
30479
|
+
structuralCache.set(filePath, sigStruct);
|
|
30480
|
+
identifierCache.set(filePath, sigIdent);
|
|
30481
|
+
return issues;
|
|
30482
|
+
}
|
|
30483
|
+
});
|
|
30484
|
+
}
|
|
30485
|
+
});
|
|
30486
|
+
|
|
29666
30487
|
// src/rules/go/error-wrap-without-context.ts
|
|
29667
30488
|
var ERR_WRAP_REGEX, GENERIC_PREFIX_REGEX, goErrorWrapWithoutContextRule;
|
|
29668
30489
|
var init_error_wrap_without_context = __esm({
|
|
@@ -30132,6 +30953,264 @@ var init_system_out_println = __esm({
|
|
|
30132
30953
|
}
|
|
30133
30954
|
});
|
|
30134
30955
|
|
|
30956
|
+
// src/rules/kotlin/coroutine-global-scope.ts
|
|
30957
|
+
var GLOBAL_SCOPE_REGEX, kotlinCoroutineGlobalScopeRule;
|
|
30958
|
+
var init_coroutine_global_scope = __esm({
|
|
30959
|
+
"src/rules/kotlin/coroutine-global-scope.ts"() {
|
|
30960
|
+
"use strict";
|
|
30961
|
+
init_rule();
|
|
30962
|
+
GLOBAL_SCOPE_REGEX = /GlobalScope\s*\.\s*(?:launch|async|runBlocking)\s*[({]/g;
|
|
30963
|
+
kotlinCoroutineGlobalScopeRule = createRule({
|
|
30964
|
+
id: "kotlin/coroutine-global-scope",
|
|
30965
|
+
category: "logic",
|
|
30966
|
+
severity: "medium",
|
|
30967
|
+
aiSpecific: true,
|
|
30968
|
+
description: "GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency. Use viewModelScope / lifecycleScope / coroutineScope {}.",
|
|
30969
|
+
create(_context) {
|
|
30970
|
+
return {};
|
|
30971
|
+
},
|
|
30972
|
+
analyze(_context, facts) {
|
|
30973
|
+
const issues = [];
|
|
30974
|
+
const source = facts.v2?._source;
|
|
30975
|
+
if (!source) return issues;
|
|
30976
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
30977
|
+
let m;
|
|
30978
|
+
GLOBAL_SCOPE_REGEX.lastIndex = 0;
|
|
30979
|
+
while ((m = GLOBAL_SCOPE_REGEX.exec(source)) !== null) {
|
|
30980
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
30981
|
+
issues.push({
|
|
30982
|
+
ruleId: "kotlin/coroutine-global-scope",
|
|
30983
|
+
category: "logic",
|
|
30984
|
+
severity: "medium",
|
|
30985
|
+
aiSpecific: true,
|
|
30986
|
+
message: `GlobalScope.${m[0].split(".")[1]?.split("(")[0]}() at line ${line} \u2014 bypasses structured concurrency`,
|
|
30987
|
+
line,
|
|
30988
|
+
column: 1,
|
|
30989
|
+
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.'
|
|
30990
|
+
});
|
|
30991
|
+
}
|
|
30992
|
+
return issues;
|
|
30993
|
+
}
|
|
30994
|
+
});
|
|
30995
|
+
}
|
|
30996
|
+
});
|
|
30997
|
+
|
|
30998
|
+
// src/rules/kotlin/data-class-defaults-overuse.ts
|
|
30999
|
+
var DATA_CLASS_HEAD_REGEX, DEFAULT_PARAM_REGEX, DEFAULT_MIN_DEFAULTS, kotlinDataClassDefaultsOveruseRule;
|
|
31000
|
+
var init_data_class_defaults_overuse = __esm({
|
|
31001
|
+
"src/rules/kotlin/data-class-defaults-overuse.ts"() {
|
|
31002
|
+
"use strict";
|
|
31003
|
+
init_rule();
|
|
31004
|
+
DATA_CLASS_HEAD_REGEX = /\bdata\s+class\s+\w+\s*\(/g;
|
|
31005
|
+
DEFAULT_PARAM_REGEX = /=\s*(?:null|true|false|"[^"]*"|'[^']*'|[+-]?\d+(?:\.\d+)?\w*|emptyList\(\)|emptyMap\(\)|emptySet\(\)|listOf\(\)|mapOf\(\)|setOf\(\))/g;
|
|
31006
|
+
DEFAULT_MIN_DEFAULTS = 3;
|
|
31007
|
+
kotlinDataClassDefaultsOveruseRule = createRule({
|
|
31008
|
+
id: "kotlin/data-class-defaults-overuse",
|
|
31009
|
+
category: "typo",
|
|
31010
|
+
severity: "low",
|
|
31011
|
+
aiSpecific: true,
|
|
31012
|
+
description: "data class with 3+ literal defaults \u2014 AI defaults every field rather than only the genuinely optional ones",
|
|
31013
|
+
create(_context) {
|
|
31014
|
+
return { minDefaults: DEFAULT_MIN_DEFAULTS };
|
|
31015
|
+
},
|
|
31016
|
+
analyze(context, facts) {
|
|
31017
|
+
const issues = [];
|
|
31018
|
+
const source = facts.v2?._source;
|
|
31019
|
+
if (!source) return issues;
|
|
31020
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31021
|
+
let m;
|
|
31022
|
+
DATA_CLASS_HEAD_REGEX.lastIndex = 0;
|
|
31023
|
+
while ((m = DATA_CLASS_HEAD_REGEX.exec(source)) !== null) {
|
|
31024
|
+
const headStart = m.index;
|
|
31025
|
+
const openParen = source.indexOf("(", headStart);
|
|
31026
|
+
if (openParen === -1) continue;
|
|
31027
|
+
let depth = 1;
|
|
31028
|
+
let i = openParen + 1;
|
|
31029
|
+
while (i < source.length && depth > 0) {
|
|
31030
|
+
const ch = source.charAt(i);
|
|
31031
|
+
if (ch === "(") depth++;
|
|
31032
|
+
else if (ch === ")") depth--;
|
|
31033
|
+
i++;
|
|
31034
|
+
}
|
|
31035
|
+
if (depth !== 0) continue;
|
|
31036
|
+
const paramBlock = source.slice(openParen + 1, i - 1);
|
|
31037
|
+
DEFAULT_PARAM_REGEX.lastIndex = 0;
|
|
31038
|
+
let defaults = 0;
|
|
31039
|
+
let dm;
|
|
31040
|
+
while ((dm = DEFAULT_PARAM_REGEX.exec(paramBlock)) !== null) {
|
|
31041
|
+
defaults++;
|
|
31042
|
+
}
|
|
31043
|
+
if (defaults < context.minDefaults) continue;
|
|
31044
|
+
const line = source.slice(0, headStart).split("\n").length;
|
|
31045
|
+
issues.push({
|
|
31046
|
+
ruleId: "kotlin/data-class-defaults-overuse",
|
|
31047
|
+
category: "typo",
|
|
31048
|
+
severity: "low",
|
|
31049
|
+
aiSpecific: true,
|
|
31050
|
+
message: `data class with ${defaults} literal defaults at line ${line} \u2014 only optional fields should have defaults`,
|
|
31051
|
+
line,
|
|
31052
|
+
column: 1,
|
|
31053
|
+
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."
|
|
31054
|
+
});
|
|
31055
|
+
}
|
|
31056
|
+
return issues;
|
|
31057
|
+
}
|
|
31058
|
+
});
|
|
31059
|
+
}
|
|
31060
|
+
});
|
|
31061
|
+
|
|
31062
|
+
// src/rules/kotlin/object-singleton-misuse.ts
|
|
31063
|
+
var OBJECT_DECL_REGEX, kotlinObjectSingletonMisuseRule;
|
|
31064
|
+
var init_object_singleton_misuse = __esm({
|
|
31065
|
+
"src/rules/kotlin/object-singleton-misuse.ts"() {
|
|
31066
|
+
"use strict";
|
|
31067
|
+
init_rule();
|
|
31068
|
+
OBJECT_DECL_REGEX = /\bobject\s+(?!\w*Companion\b)(\w+)\s*\{/g;
|
|
31069
|
+
kotlinObjectSingletonMisuseRule = createRule({
|
|
31070
|
+
id: "kotlin/object-singleton-misuse",
|
|
31071
|
+
category: "logic",
|
|
31072
|
+
severity: "medium",
|
|
31073
|
+
aiSpecific: true,
|
|
31074
|
+
description: "Kotlin object singleton holding mutable `var` state \u2014 hidden shared state. Use DI or a repository.",
|
|
31075
|
+
create(_context) {
|
|
31076
|
+
return {};
|
|
31077
|
+
},
|
|
31078
|
+
analyze(_context, facts) {
|
|
31079
|
+
const issues = [];
|
|
31080
|
+
const source = facts.v2?._source;
|
|
31081
|
+
if (!source) return issues;
|
|
31082
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31083
|
+
let m;
|
|
31084
|
+
OBJECT_DECL_REGEX.lastIndex = 0;
|
|
31085
|
+
while ((m = OBJECT_DECL_REGEX.exec(source)) !== null) {
|
|
31086
|
+
const declStart = m.index;
|
|
31087
|
+
const openBrace = source.indexOf("{", declStart);
|
|
31088
|
+
if (openBrace === -1) continue;
|
|
31089
|
+
let depth = 1;
|
|
31090
|
+
let i = openBrace + 1;
|
|
31091
|
+
while (i < source.length && depth > 0) {
|
|
31092
|
+
const ch = source.charAt(i);
|
|
31093
|
+
if (ch === "{") depth++;
|
|
31094
|
+
else if (ch === "}") depth--;
|
|
31095
|
+
i++;
|
|
31096
|
+
}
|
|
31097
|
+
if (depth !== 0) continue;
|
|
31098
|
+
const body = source.slice(openBrace + 1, i - 1);
|
|
31099
|
+
if (!/\bvar\s+\w+\s*[:=]/m.test(body)) continue;
|
|
31100
|
+
const line = source.slice(0, declStart).split("\n").length;
|
|
31101
|
+
const name = m[1] ?? "<unknown>";
|
|
31102
|
+
issues.push({
|
|
31103
|
+
ruleId: "kotlin/object-singleton-misuse",
|
|
31104
|
+
category: "logic",
|
|
31105
|
+
severity: "medium",
|
|
31106
|
+
aiSpecific: true,
|
|
31107
|
+
message: `object ${name} at line ${line} holds mutable state \u2014 singleton + var = hidden shared state`,
|
|
31108
|
+
line,
|
|
31109
|
+
column: 1,
|
|
31110
|
+
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."
|
|
31111
|
+
});
|
|
31112
|
+
}
|
|
31113
|
+
return issues;
|
|
31114
|
+
}
|
|
31115
|
+
});
|
|
31116
|
+
}
|
|
31117
|
+
});
|
|
31118
|
+
|
|
31119
|
+
// src/rules/kotlin/println-debug.ts
|
|
31120
|
+
var PRINTLN_REGEX2, DEFAULT_THRESHOLD2, kotlinPrintlnDebugRule;
|
|
31121
|
+
var init_println_debug = __esm({
|
|
31122
|
+
"src/rules/kotlin/println-debug.ts"() {
|
|
31123
|
+
"use strict";
|
|
31124
|
+
init_rule();
|
|
31125
|
+
PRINTLN_REGEX2 = /^\s*println\s*\(/gm;
|
|
31126
|
+
DEFAULT_THRESHOLD2 = 1;
|
|
31127
|
+
kotlinPrintlnDebugRule = createRule({
|
|
31128
|
+
id: "kotlin/println-debug",
|
|
31129
|
+
category: "typo",
|
|
31130
|
+
severity: "low",
|
|
31131
|
+
aiSpecific: true,
|
|
31132
|
+
description: "println(...) in production code \u2014 use Timber, android.util.Log, or an SLF4J facade",
|
|
31133
|
+
create(_context) {
|
|
31134
|
+
return { threshold: DEFAULT_THRESHOLD2 };
|
|
31135
|
+
},
|
|
31136
|
+
analyze(context, facts) {
|
|
31137
|
+
const issues = [];
|
|
31138
|
+
const source = facts.v2?._source;
|
|
31139
|
+
if (!source) return issues;
|
|
31140
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31141
|
+
const matches = [];
|
|
31142
|
+
let m;
|
|
31143
|
+
PRINTLN_REGEX2.lastIndex = 0;
|
|
31144
|
+
while ((m = PRINTLN_REGEX2.exec(source)) !== null) {
|
|
31145
|
+
matches.push(m.index);
|
|
31146
|
+
}
|
|
31147
|
+
if (matches.length <= context.threshold) return issues;
|
|
31148
|
+
const cap = Math.min(matches.length, 10);
|
|
31149
|
+
for (let i = 0; i < cap; i++) {
|
|
31150
|
+
const idx = matches[i];
|
|
31151
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
31152
|
+
issues.push({
|
|
31153
|
+
ruleId: "kotlin/println-debug",
|
|
31154
|
+
category: "typo",
|
|
31155
|
+
severity: "low",
|
|
31156
|
+
aiSpecific: true,
|
|
31157
|
+
message: `println(...) at line ${line} \u2014 use Timber or android.util.Log for production output`,
|
|
31158
|
+
line,
|
|
31159
|
+
column: 1,
|
|
31160
|
+
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."
|
|
31161
|
+
});
|
|
31162
|
+
}
|
|
31163
|
+
return issues;
|
|
31164
|
+
}
|
|
31165
|
+
});
|
|
31166
|
+
}
|
|
31167
|
+
});
|
|
31168
|
+
|
|
31169
|
+
// src/rules/kotlin/string-concat-loop.ts
|
|
31170
|
+
var STRING_CONCAT_REGEX2, kotlinStringConcatLoopRule;
|
|
31171
|
+
var init_string_concat_loop2 = __esm({
|
|
31172
|
+
"src/rules/kotlin/string-concat-loop.ts"() {
|
|
31173
|
+
"use strict";
|
|
31174
|
+
init_rule();
|
|
31175
|
+
STRING_CONCAT_REGEX2 = /\b(\w+)\s*=\s*\1\s*\+\s*[^;}]+[;}\n]/g;
|
|
31176
|
+
kotlinStringConcatLoopRule = createRule({
|
|
31177
|
+
id: "kotlin/string-concat-loop",
|
|
31178
|
+
category: "perf",
|
|
31179
|
+
severity: "low",
|
|
31180
|
+
aiSpecific: true,
|
|
31181
|
+
description: "String concatenation in a loop \u2014 use buildString { ... } or a StringBuilder",
|
|
31182
|
+
create(_context) {
|
|
31183
|
+
return {};
|
|
31184
|
+
},
|
|
31185
|
+
analyze(_context, facts) {
|
|
31186
|
+
const issues = [];
|
|
31187
|
+
const source = facts.v2?._source;
|
|
31188
|
+
if (!source) return issues;
|
|
31189
|
+
if (!/\.kt$/i.test(facts.filePath)) return issues;
|
|
31190
|
+
if (!/\b(?:for|while|repeat|forEach)\b/.test(source)) return issues;
|
|
31191
|
+
let m;
|
|
31192
|
+
STRING_CONCAT_REGEX2.lastIndex = 0;
|
|
31193
|
+
while ((m = STRING_CONCAT_REGEX2.exec(source)) !== null) {
|
|
31194
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
31195
|
+
const lineText = source.slice(0, m.index).split("\n").pop() ?? "";
|
|
31196
|
+
if (/\.append\s*\(/.test(lineText)) continue;
|
|
31197
|
+
issues.push({
|
|
31198
|
+
ruleId: "kotlin/string-concat-loop",
|
|
31199
|
+
category: "perf",
|
|
31200
|
+
severity: "low",
|
|
31201
|
+
aiSpecific: true,
|
|
31202
|
+
message: `String concatenation in a loop at line ${line} \u2014 use buildString { ... }`,
|
|
31203
|
+
line,
|
|
31204
|
+
column: 1,
|
|
31205
|
+
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."
|
|
31206
|
+
});
|
|
31207
|
+
}
|
|
31208
|
+
return issues;
|
|
31209
|
+
}
|
|
31210
|
+
});
|
|
31211
|
+
}
|
|
31212
|
+
});
|
|
31213
|
+
|
|
30135
31214
|
// src/rules/layout/gap-monopoly.ts
|
|
30136
31215
|
var GAP_RE, gapMonopolyRule;
|
|
30137
31216
|
var init_gap_monopoly = __esm({
|
|
@@ -35328,6 +36407,7 @@ function parseSource(source, filePath) {
|
|
|
35328
36407
|
case "py":
|
|
35329
36408
|
case "go":
|
|
35330
36409
|
case "rs":
|
|
36410
|
+
case "java":
|
|
35331
36411
|
return parseBlankModule(source);
|
|
35332
36412
|
default:
|
|
35333
36413
|
return parseWithSwc(source, filePath);
|
|
@@ -38028,7 +39108,7 @@ var init_css_bloat = __esm({
|
|
|
38028
39108
|
function stripNoise2(source) {
|
|
38029
39109
|
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/'(?:\\.|[^'\\])*'/g, "''").replace(/"(?:\\.|[^"\\])*"/g, '""').replace(/`(?:\\.|[^`\\])*`/g, "``");
|
|
38030
39110
|
}
|
|
38031
|
-
function
|
|
39111
|
+
function tokenize2(source) {
|
|
38032
39112
|
const cleaned = stripNoise2(source);
|
|
38033
39113
|
const operators = [];
|
|
38034
39114
|
const operands = [];
|
|
@@ -38049,7 +39129,7 @@ function tokenize(source) {
|
|
|
38049
39129
|
return { operators, operands };
|
|
38050
39130
|
}
|
|
38051
39131
|
function computeHalstead(source) {
|
|
38052
|
-
const { operators, operands } =
|
|
39132
|
+
const { operators, operands } = tokenize2(source);
|
|
38053
39133
|
const uniqueOperators = new Set(operators);
|
|
38054
39134
|
const uniqueOperands = new Set(operands);
|
|
38055
39135
|
const n1 = uniqueOperators.size;
|
|
@@ -39615,6 +40695,260 @@ var init_unsafe_html_render = __esm({
|
|
|
39615
40695
|
}
|
|
39616
40696
|
});
|
|
39617
40697
|
|
|
40698
|
+
// src/rules/swift/fatal-error-thrown.ts
|
|
40699
|
+
var FATAL_REGEX, swiftFatalErrorThrownRule;
|
|
40700
|
+
var init_fatal_error_thrown = __esm({
|
|
40701
|
+
"src/rules/swift/fatal-error-thrown.ts"() {
|
|
40702
|
+
"use strict";
|
|
40703
|
+
init_rule();
|
|
40704
|
+
FATAL_REGEX = /\b(?:fatalError|preconditionFailure)\s*\(/g;
|
|
40705
|
+
swiftFatalErrorThrownRule = createRule({
|
|
40706
|
+
id: "swift/fatal-error-thrown",
|
|
40707
|
+
category: "logic",
|
|
40708
|
+
severity: "high",
|
|
40709
|
+
aiSpecific: true,
|
|
40710
|
+
description: "fatalError / preconditionFailure in production \u2014 crashes unconditionally. Return an error or a typed default.",
|
|
40711
|
+
create(_context) {
|
|
40712
|
+
return {};
|
|
40713
|
+
},
|
|
40714
|
+
analyze(_context, facts) {
|
|
40715
|
+
const issues = [];
|
|
40716
|
+
const source = facts.v2?._source;
|
|
40717
|
+
if (!source) return issues;
|
|
40718
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40719
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40720
|
+
let m;
|
|
40721
|
+
FATAL_REGEX.lastIndex = 0;
|
|
40722
|
+
while ((m = FATAL_REGEX.exec(source)) !== null) {
|
|
40723
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40724
|
+
const matched = m[0].replace(/\s*\($/, "");
|
|
40725
|
+
issues.push({
|
|
40726
|
+
ruleId: "swift/fatal-error-thrown",
|
|
40727
|
+
category: "logic",
|
|
40728
|
+
severity: "high",
|
|
40729
|
+
aiSpecific: true,
|
|
40730
|
+
message: `${matched}() at line ${line} \u2014 crashes unconditionally in release builds`,
|
|
40731
|
+
line,
|
|
40732
|
+
column: 1,
|
|
40733
|
+
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.'
|
|
40734
|
+
});
|
|
40735
|
+
}
|
|
40736
|
+
return issues;
|
|
40737
|
+
}
|
|
40738
|
+
});
|
|
40739
|
+
}
|
|
40740
|
+
});
|
|
40741
|
+
|
|
40742
|
+
// src/rules/swift/force-unwrap.ts
|
|
40743
|
+
var AS_FORCE_REGEX, ACCESS_FORCE_REGEX, TRY_FORCE_REGEX, swiftForceUnwrapRule;
|
|
40744
|
+
var init_force_unwrap = __esm({
|
|
40745
|
+
"src/rules/swift/force-unwrap.ts"() {
|
|
40746
|
+
"use strict";
|
|
40747
|
+
init_rule();
|
|
40748
|
+
AS_FORCE_REGEX = /\bas!\s+/g;
|
|
40749
|
+
ACCESS_FORCE_REGEX = /(?:\w|\])\!\s*(?:\.|\(|;|,|\s*$)/gm;
|
|
40750
|
+
TRY_FORCE_REGEX = /\btry!\s+/g;
|
|
40751
|
+
swiftForceUnwrapRule = createRule({
|
|
40752
|
+
id: "swift/force-unwrap",
|
|
40753
|
+
category: "logic",
|
|
40754
|
+
severity: "medium",
|
|
40755
|
+
aiSpecific: true,
|
|
40756
|
+
description: "Swift force-unwrap (as!, var!.prop, try!) \u2014 silently crashes if the value is nil. Use as? / guard let / try?.",
|
|
40757
|
+
create(_context) {
|
|
40758
|
+
return {};
|
|
40759
|
+
},
|
|
40760
|
+
analyze(_context, facts) {
|
|
40761
|
+
const issues = [];
|
|
40762
|
+
const source = facts.v2?._source;
|
|
40763
|
+
if (!source) return issues;
|
|
40764
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40765
|
+
if (/\bimport\s+XCTest\b/.test(source)) return issues;
|
|
40766
|
+
const emit = (idx, label) => {
|
|
40767
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40768
|
+
issues.push({
|
|
40769
|
+
ruleId: "swift/force-unwrap",
|
|
40770
|
+
category: "logic",
|
|
40771
|
+
severity: "medium",
|
|
40772
|
+
aiSpecific: true,
|
|
40773
|
+
message: `force-unwrap (${label}) at line ${line} \u2014 crashes if the value is nil`,
|
|
40774
|
+
line,
|
|
40775
|
+
column: 1,
|
|
40776
|
+
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.'
|
|
40777
|
+
});
|
|
40778
|
+
};
|
|
40779
|
+
let m;
|
|
40780
|
+
AS_FORCE_REGEX.lastIndex = 0;
|
|
40781
|
+
while ((m = AS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "as!");
|
|
40782
|
+
TRY_FORCE_REGEX.lastIndex = 0;
|
|
40783
|
+
while ((m = TRY_FORCE_REGEX.exec(source)) !== null) emit(m.index, "try!");
|
|
40784
|
+
ACCESS_FORCE_REGEX.lastIndex = 0;
|
|
40785
|
+
while ((m = ACCESS_FORCE_REGEX.exec(source)) !== null) emit(m.index, "!.");
|
|
40786
|
+
return issues;
|
|
40787
|
+
}
|
|
40788
|
+
});
|
|
40789
|
+
}
|
|
40790
|
+
});
|
|
40791
|
+
|
|
40792
|
+
// src/rules/swift/implicitly-unwrapped-optional.ts
|
|
40793
|
+
var IUO_DECL_REGEX, swiftImplicitlyUnwrappedOptionalRule;
|
|
40794
|
+
var init_implicitly_unwrapped_optional = __esm({
|
|
40795
|
+
"src/rules/swift/implicitly-unwrapped-optional.ts"() {
|
|
40796
|
+
"use strict";
|
|
40797
|
+
init_rule();
|
|
40798
|
+
IUO_DECL_REGEX = /\b(?:var|let)\s+\w+\s*:\s*[A-Za-z_][\w<>?,\.\s]*!\s*[=;\n]/g;
|
|
40799
|
+
swiftImplicitlyUnwrappedOptionalRule = createRule({
|
|
40800
|
+
id: "swift/implicitly-unwrapped-optional",
|
|
40801
|
+
category: "typo",
|
|
40802
|
+
severity: "low",
|
|
40803
|
+
aiSpecific: true,
|
|
40804
|
+
description: "Implicitly unwrapped optional (var x: Type!) \u2014 declare as Type? and unwrap with guard let.",
|
|
40805
|
+
create(_context) {
|
|
40806
|
+
return {};
|
|
40807
|
+
},
|
|
40808
|
+
analyze(_context, facts) {
|
|
40809
|
+
const issues = [];
|
|
40810
|
+
const source = facts.v2?._source;
|
|
40811
|
+
if (!source) return issues;
|
|
40812
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40813
|
+
let m;
|
|
40814
|
+
IUO_DECL_REGEX.lastIndex = 0;
|
|
40815
|
+
while ((m = IUO_DECL_REGEX.exec(source)) !== null) {
|
|
40816
|
+
const captured = m[0];
|
|
40817
|
+
const bangAt = captured.indexOf("!", captured.indexOf(":"));
|
|
40818
|
+
const between = captured.slice(captured.indexOf(":"), bangAt);
|
|
40819
|
+
if (between.includes("?")) continue;
|
|
40820
|
+
const line = source.slice(0, m.index).split("\n").length;
|
|
40821
|
+
issues.push({
|
|
40822
|
+
ruleId: "swift/implicitly-unwrapped-optional",
|
|
40823
|
+
category: "typo",
|
|
40824
|
+
severity: "low",
|
|
40825
|
+
aiSpecific: true,
|
|
40826
|
+
message: `implicitly unwrapped optional at line ${line} \u2014 declare as Type? and unwrap explicitly`,
|
|
40827
|
+
line,
|
|
40828
|
+
column: 1,
|
|
40829
|
+
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.'
|
|
40830
|
+
});
|
|
40831
|
+
}
|
|
40832
|
+
return issues;
|
|
40833
|
+
}
|
|
40834
|
+
});
|
|
40835
|
+
}
|
|
40836
|
+
});
|
|
40837
|
+
|
|
40838
|
+
// src/rules/swift/print-debug.ts
|
|
40839
|
+
var PRINT_REGEX, DEFAULT_THRESHOLD3, swiftPrintDebugRule;
|
|
40840
|
+
var init_print_debug = __esm({
|
|
40841
|
+
"src/rules/swift/print-debug.ts"() {
|
|
40842
|
+
"use strict";
|
|
40843
|
+
init_rule();
|
|
40844
|
+
PRINT_REGEX = /\bprint\s*\(/g;
|
|
40845
|
+
DEFAULT_THRESHOLD3 = 1;
|
|
40846
|
+
swiftPrintDebugRule = createRule({
|
|
40847
|
+
id: "swift/print-debug",
|
|
40848
|
+
category: "typo",
|
|
40849
|
+
severity: "low",
|
|
40850
|
+
aiSpecific: true,
|
|
40851
|
+
description: "print(...) in production Swift \u2014 use Logger (os.log) for level-controlled output",
|
|
40852
|
+
create(_context) {
|
|
40853
|
+
return { threshold: DEFAULT_THRESHOLD3 };
|
|
40854
|
+
},
|
|
40855
|
+
analyze(context, facts) {
|
|
40856
|
+
const issues = [];
|
|
40857
|
+
const source = facts.v2?._source;
|
|
40858
|
+
if (!source) return issues;
|
|
40859
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40860
|
+
const matches = [];
|
|
40861
|
+
let m;
|
|
40862
|
+
PRINT_REGEX.lastIndex = 0;
|
|
40863
|
+
while ((m = PRINT_REGEX.exec(source)) !== null) {
|
|
40864
|
+
matches.push(m.index);
|
|
40865
|
+
}
|
|
40866
|
+
if (matches.length <= context.threshold) return issues;
|
|
40867
|
+
const cap = Math.min(matches.length, 10);
|
|
40868
|
+
for (let i = 0; i < cap; i++) {
|
|
40869
|
+
const idx = matches[i];
|
|
40870
|
+
const line = source.slice(0, idx).split("\n").length;
|
|
40871
|
+
issues.push({
|
|
40872
|
+
ruleId: "swift/print-debug",
|
|
40873
|
+
category: "typo",
|
|
40874
|
+
severity: "low",
|
|
40875
|
+
aiSpecific: true,
|
|
40876
|
+
message: `print(...) at line ${line} \u2014 use Logger (os.log) for production output`,
|
|
40877
|
+
line,
|
|
40878
|
+
column: 1,
|
|
40879
|
+
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.'
|
|
40880
|
+
});
|
|
40881
|
+
}
|
|
40882
|
+
return issues;
|
|
40883
|
+
}
|
|
40884
|
+
});
|
|
40885
|
+
}
|
|
40886
|
+
});
|
|
40887
|
+
|
|
40888
|
+
// src/rules/swift/strong-self-capture.ts
|
|
40889
|
+
var SELF_REF_REGEX, CAPTURE_LIST_REGEX, swiftStrongSelfCaptureRule;
|
|
40890
|
+
var init_strong_self_capture = __esm({
|
|
40891
|
+
"src/rules/swift/strong-self-capture.ts"() {
|
|
40892
|
+
"use strict";
|
|
40893
|
+
init_rule();
|
|
40894
|
+
SELF_REF_REGEX = /\bself\.(?:\w+)/g;
|
|
40895
|
+
CAPTURE_LIST_REGEX = /\[\s*(?:weak|unowned)\s+self\b/;
|
|
40896
|
+
swiftStrongSelfCaptureRule = createRule({
|
|
40897
|
+
id: "swift/strong-self-capture",
|
|
40898
|
+
category: "logic",
|
|
40899
|
+
severity: "medium",
|
|
40900
|
+
aiSpecific: true,
|
|
40901
|
+
description: "Closure captures `self` strongly \u2014 creates a retain cycle. Use `[weak self]` or `[unowned self]`.",
|
|
40902
|
+
create(_context) {
|
|
40903
|
+
return {};
|
|
40904
|
+
},
|
|
40905
|
+
analyze(_context, facts) {
|
|
40906
|
+
const issues = [];
|
|
40907
|
+
const source = facts.v2?._source;
|
|
40908
|
+
if (!source) return issues;
|
|
40909
|
+
if (!/\.swift$/i.test(facts.filePath)) return issues;
|
|
40910
|
+
const METHOD_HEAD_REGEX = /\b(?:func|init\??|deinit|subscript)\b[^;{}\n]*$/m;
|
|
40911
|
+
let m;
|
|
40912
|
+
SELF_REF_REGEX.lastIndex = 0;
|
|
40913
|
+
while ((m = SELF_REF_REGEX.exec(source)) !== null) {
|
|
40914
|
+
const selfIdx = m.index;
|
|
40915
|
+
let depth = 0;
|
|
40916
|
+
let openBrace = -1;
|
|
40917
|
+
for (let j = selfIdx - 1; j >= 0; j--) {
|
|
40918
|
+
const ch = source.charAt(j);
|
|
40919
|
+
if (ch === "}") depth++;
|
|
40920
|
+
else if (ch === "{") {
|
|
40921
|
+
if (depth === 0) {
|
|
40922
|
+
openBrace = j;
|
|
40923
|
+
break;
|
|
40924
|
+
}
|
|
40925
|
+
depth--;
|
|
40926
|
+
}
|
|
40927
|
+
}
|
|
40928
|
+
if (openBrace === -1) continue;
|
|
40929
|
+
const openLineStart = source.lastIndexOf("\n", openBrace) + 1;
|
|
40930
|
+
const lineBeforeBrace = source.slice(openLineStart, openBrace);
|
|
40931
|
+
if (METHOD_HEAD_REGEX.test(lineBeforeBrace)) continue;
|
|
40932
|
+
const openingSlice = source.slice(openBrace, openBrace + 80);
|
|
40933
|
+
if (CAPTURE_LIST_REGEX.test(openingSlice)) continue;
|
|
40934
|
+
const line = source.slice(0, openBrace).split("\n").length;
|
|
40935
|
+
issues.push({
|
|
40936
|
+
ruleId: "swift/strong-self-capture",
|
|
40937
|
+
category: "logic",
|
|
40938
|
+
severity: "medium",
|
|
40939
|
+
aiSpecific: true,
|
|
40940
|
+
message: `closure at line ${line} captures self strongly \u2014 use [weak self] or [unowned self]`,
|
|
40941
|
+
line,
|
|
40942
|
+
column: 1,
|
|
40943
|
+
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.'
|
|
40944
|
+
});
|
|
40945
|
+
}
|
|
40946
|
+
return issues;
|
|
40947
|
+
}
|
|
40948
|
+
});
|
|
40949
|
+
}
|
|
40950
|
+
});
|
|
40951
|
+
|
|
39618
40952
|
// src/rules/test/utils.ts
|
|
39619
40953
|
function isTestFile(filePath) {
|
|
39620
40954
|
const normalized = filePath.replace(/\\/g, "/");
|
|
@@ -39751,7 +41085,7 @@ function extractSetupBlocks(source, minLines = 3) {
|
|
|
39751
41085
|
}).trim();
|
|
39752
41086
|
const lineCount = rawBody.split("\n").length;
|
|
39753
41087
|
if (lineCount < minLines) continue;
|
|
39754
|
-
const bodyHash = (0,
|
|
41088
|
+
const bodyHash = (0, import_node_crypto4.createHash)("sha1").update(normalizedBody).digest("hex");
|
|
39755
41089
|
const { line, column } = lineColumnOf(source, m.index);
|
|
39756
41090
|
hits.push({
|
|
39757
41091
|
kind,
|
|
@@ -39797,11 +41131,11 @@ function extractPlaceholderCandidates(source) {
|
|
|
39797
41131
|
}
|
|
39798
41132
|
return hits;
|
|
39799
41133
|
}
|
|
39800
|
-
var
|
|
41134
|
+
var import_node_crypto4, ASSERTION_MATCHERS, SETUP_RE, STRING_PROP_RE, NUMERIC_PROP_RE, DATE_PROP_RE;
|
|
39801
41135
|
var init_utils2 = __esm({
|
|
39802
41136
|
"src/rules/test/utils.ts"() {
|
|
39803
41137
|
"use strict";
|
|
39804
|
-
|
|
41138
|
+
import_node_crypto4 = require("crypto");
|
|
39805
41139
|
ASSERTION_MATCHERS = /* @__PURE__ */ new Set([
|
|
39806
41140
|
"toBe",
|
|
39807
41141
|
"toEqual",
|
|
@@ -45241,6 +46575,11 @@ var init_builtins = __esm({
|
|
|
45241
46575
|
init_multiple_components_per_file();
|
|
45242
46576
|
init_shadcn_prop_mismatch();
|
|
45243
46577
|
init_import_path_mismatch();
|
|
46578
|
+
init_c_style_cast();
|
|
46579
|
+
init_magic_numbers();
|
|
46580
|
+
init_printf_debug();
|
|
46581
|
+
init_raw_new_delete();
|
|
46582
|
+
init_using_namespace_std();
|
|
45244
46583
|
init_duplicate_index();
|
|
45245
46584
|
init_enum_sprawl();
|
|
45246
46585
|
init_missing_fk_index();
|
|
@@ -45256,6 +46595,8 @@ var init_builtins = __esm({
|
|
|
45256
46595
|
init_stale_function_reference();
|
|
45257
46596
|
init_stale_package_reference();
|
|
45258
46597
|
init_identical_block();
|
|
46598
|
+
init_near_duplicate();
|
|
46599
|
+
init_structural_clone2();
|
|
45259
46600
|
init_error_wrap_without_context();
|
|
45260
46601
|
init_nil_slice_vs_empty();
|
|
45261
46602
|
init_struct_tag_inconsistency();
|
|
@@ -45265,6 +46606,11 @@ var init_builtins = __esm({
|
|
|
45265
46606
|
init_raw_type_overuse();
|
|
45266
46607
|
init_string_concat_loop();
|
|
45267
46608
|
init_system_out_println();
|
|
46609
|
+
init_coroutine_global_scope();
|
|
46610
|
+
init_data_class_defaults_overuse();
|
|
46611
|
+
init_object_singleton_misuse();
|
|
46612
|
+
init_println_debug();
|
|
46613
|
+
init_string_concat_loop2();
|
|
45268
46614
|
init_gap_monopoly();
|
|
45269
46615
|
init_math_element_uniformity();
|
|
45270
46616
|
init_math_grid_uniformity();
|
|
@@ -45303,6 +46649,11 @@ var init_builtins = __esm({
|
|
|
45303
46649
|
init_sql_construction();
|
|
45304
46650
|
init_target_blank_no_noopener();
|
|
45305
46651
|
init_unsafe_html_render();
|
|
46652
|
+
init_fatal_error_thrown();
|
|
46653
|
+
init_force_unwrap();
|
|
46654
|
+
init_implicitly_unwrapped_optional();
|
|
46655
|
+
init_print_debug();
|
|
46656
|
+
init_strong_self_capture();
|
|
45306
46657
|
init_duplicate_setup();
|
|
45307
46658
|
init_fake_placeholder();
|
|
45308
46659
|
init_missing_edge_case();
|
|
@@ -45359,6 +46710,11 @@ var init_builtins = __esm({
|
|
|
45359
46710
|
multipleComponentsPerFileRule,
|
|
45360
46711
|
shadcnPropMismatchRule,
|
|
45361
46712
|
importPathMismatchRule,
|
|
46713
|
+
cppCStyleCastRule,
|
|
46714
|
+
cppMagicNumbersRule,
|
|
46715
|
+
cppPrintfDebugRule,
|
|
46716
|
+
cppRawNewDeleteRule,
|
|
46717
|
+
cppUsingNamespaceStdRule,
|
|
45362
46718
|
duplicateIndexRule,
|
|
45363
46719
|
enumSprawlRule,
|
|
45364
46720
|
missingFkIndexRule,
|
|
@@ -45374,6 +46730,8 @@ var init_builtins = __esm({
|
|
|
45374
46730
|
staleFunctionReferenceRule,
|
|
45375
46731
|
stalePackageReferenceRule,
|
|
45376
46732
|
dupIdenticalBlockRule,
|
|
46733
|
+
nearDuplicateRule,
|
|
46734
|
+
structuralCloneRule,
|
|
45377
46735
|
goErrorWrapWithoutContextRule,
|
|
45378
46736
|
goNilSliceVsEmptyRule,
|
|
45379
46737
|
goStructTagInconsistencyRule,
|
|
@@ -45383,6 +46741,11 @@ var init_builtins = __esm({
|
|
|
45383
46741
|
javaRawTypeOveruseRule,
|
|
45384
46742
|
javaStringConcatLoopRule,
|
|
45385
46743
|
javaSystemOutPrintlnRule,
|
|
46744
|
+
kotlinCoroutineGlobalScopeRule,
|
|
46745
|
+
kotlinDataClassDefaultsOveruseRule,
|
|
46746
|
+
kotlinObjectSingletonMisuseRule,
|
|
46747
|
+
kotlinPrintlnDebugRule,
|
|
46748
|
+
kotlinStringConcatLoopRule,
|
|
45386
46749
|
gapMonopolyRule,
|
|
45387
46750
|
mathElementUniformityRule,
|
|
45388
46751
|
mathGridUniformityRule,
|
|
@@ -45421,6 +46784,11 @@ var init_builtins = __esm({
|
|
|
45421
46784
|
sqlConstructionRule,
|
|
45422
46785
|
targetBlankNoNoopenerRule,
|
|
45423
46786
|
unsafeHtmlRenderRule,
|
|
46787
|
+
swiftFatalErrorThrownRule,
|
|
46788
|
+
swiftForceUnwrapRule,
|
|
46789
|
+
swiftImplicitlyUnwrappedOptionalRule,
|
|
46790
|
+
swiftPrintDebugRule,
|
|
46791
|
+
swiftStrongSelfCaptureRule,
|
|
45424
46792
|
duplicateSetupRule,
|
|
45425
46793
|
fakePlaceholderRule,
|
|
45426
46794
|
missingEdgeCaseRule,
|
|
@@ -45479,7 +46847,7 @@ function tokenizeClassLike(source) {
|
|
|
45479
46847
|
if (current.length > 0) tokens.push(current);
|
|
45480
46848
|
return tokens;
|
|
45481
46849
|
}
|
|
45482
|
-
function
|
|
46850
|
+
function stripComments3(source) {
|
|
45483
46851
|
let out = source.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
45484
46852
|
out = out.replace(/(^|\s)\/\/[^\n]*/g, " ");
|
|
45485
46853
|
return out;
|
|
@@ -45491,7 +46859,7 @@ function normalizeCalc(source) {
|
|
|
45491
46859
|
});
|
|
45492
46860
|
}
|
|
45493
46861
|
function canonicalizeStyleSource(source) {
|
|
45494
|
-
let normalized =
|
|
46862
|
+
let normalized = stripComments3(source);
|
|
45495
46863
|
normalized = normalizeCalc(normalized);
|
|
45496
46864
|
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
45497
46865
|
if (normalized.includes(" ") && !/[{};]/.test(normalized)) {
|
|
@@ -47506,7 +48874,7 @@ function saveCache(cachePath3, cache) {
|
|
|
47506
48874
|
}
|
|
47507
48875
|
function computeFileHash(filePath) {
|
|
47508
48876
|
const content = (0, import_node_fs12.readFileSync)(filePath);
|
|
47509
|
-
return (0,
|
|
48877
|
+
return (0, import_node_crypto5.createHash)("md5").update(content).digest("hex");
|
|
47510
48878
|
}
|
|
47511
48879
|
function partitionByCache(files, cache) {
|
|
47512
48880
|
const toScan = [];
|
|
@@ -47540,11 +48908,11 @@ function emptyCache() {
|
|
|
47540
48908
|
files: {}
|
|
47541
48909
|
};
|
|
47542
48910
|
}
|
|
47543
|
-
var
|
|
48911
|
+
var import_node_crypto5, import_node_fs12, import_node_path11;
|
|
47544
48912
|
var init_cache_incremental = __esm({
|
|
47545
48913
|
"src/engine/cache-incremental.ts"() {
|
|
47546
48914
|
"use strict";
|
|
47547
|
-
|
|
48915
|
+
import_node_crypto5 = require("crypto");
|
|
47548
48916
|
import_node_fs12 = require("fs");
|
|
47549
48917
|
import_node_path11 = require("path");
|
|
47550
48918
|
init_types();
|
|
@@ -48444,9 +49812,9 @@ var init_signal_strength = __esm({
|
|
|
48444
49812
|
ratio: 1,
|
|
48445
49813
|
precision: 0,
|
|
48446
49814
|
lastCalibratedAt: "2026-07-01T00:00:00Z",
|
|
48447
|
-
verdict: "
|
|
49815
|
+
verdict: "OK",
|
|
48448
49816
|
defaultOff: false,
|
|
48449
|
-
_calibrationNote: "v0.19: new rule
|
|
49817
|
+
_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.",
|
|
48450
49818
|
aiSpecific: false,
|
|
48451
49819
|
_v7Verdict: "DORMANT",
|
|
48452
49820
|
_v7Lift: 1,
|
|
@@ -48454,8 +49822,39 @@ var init_signal_strength = __esm({
|
|
|
48454
49822
|
_v7FpRate: 0,
|
|
48455
49823
|
_v7Precision: 0,
|
|
48456
49824
|
_v8Verdict: "DORMANT",
|
|
48457
|
-
_v8Lift: 1
|
|
48458
|
-
|
|
49825
|
+
_v8Lift: 1
|
|
49826
|
+
},
|
|
49827
|
+
"dup/near-duplicate": {
|
|
49828
|
+
recall: 0,
|
|
49829
|
+
fpRate: 0,
|
|
49830
|
+
ratio: 1,
|
|
49831
|
+
precision: 0,
|
|
49832
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
49833
|
+
verdict: "DORMANT",
|
|
49834
|
+
defaultOff: true,
|
|
49835
|
+
_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.",
|
|
49836
|
+
aiSpecific: true,
|
|
49837
|
+
_v7Verdict: "DORMANT",
|
|
49838
|
+
_v7Lift: 1,
|
|
49839
|
+
_v7Recall: 0,
|
|
49840
|
+
_v7FpRate: 0,
|
|
49841
|
+
_v7Precision: 0
|
|
49842
|
+
},
|
|
49843
|
+
"dup/structural-clone": {
|
|
49844
|
+
recall: 0,
|
|
49845
|
+
fpRate: 0,
|
|
49846
|
+
ratio: 1,
|
|
49847
|
+
precision: 0,
|
|
49848
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
49849
|
+
verdict: "DORMANT",
|
|
49850
|
+
defaultOff: true,
|
|
49851
|
+
_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.",
|
|
49852
|
+
aiSpecific: true,
|
|
49853
|
+
_v7Verdict: "DORMANT",
|
|
49854
|
+
_v7Lift: 1,
|
|
49855
|
+
_v7Recall: 0,
|
|
49856
|
+
_v7FpRate: 0,
|
|
49857
|
+
_v7Precision: 0
|
|
48459
49858
|
},
|
|
48460
49859
|
"go/error-wrap-without-context": {
|
|
48461
49860
|
recall: 0,
|
|
@@ -49861,6 +51260,276 @@ var init_signal_strength = __esm({
|
|
|
49861
51260
|
_v8Verdict: "DORMANT",
|
|
49862
51261
|
_v8Lift: 1,
|
|
49863
51262
|
defaultOff: true
|
|
51263
|
+
},
|
|
51264
|
+
"kotlin/data-class-defaults-overuse": {
|
|
51265
|
+
recall: 0,
|
|
51266
|
+
fpRate: 0,
|
|
51267
|
+
ratio: 1,
|
|
51268
|
+
precision: 0,
|
|
51269
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51270
|
+
verdict: "DORMANT",
|
|
51271
|
+
_calibrationNote: "v0.24: new rule (data class with 3+ literal defaults). DORMANT until v9 Kotlin corpus calibration.",
|
|
51272
|
+
aiSpecific: true,
|
|
51273
|
+
_v7Verdict: "DORMANT",
|
|
51274
|
+
_v7Lift: 1,
|
|
51275
|
+
_v7Recall: 0,
|
|
51276
|
+
_v7FpRate: 0,
|
|
51277
|
+
_v7Precision: 0,
|
|
51278
|
+
_v8Verdict: "DORMANT",
|
|
51279
|
+
_v8Lift: 1,
|
|
51280
|
+
defaultOff: true
|
|
51281
|
+
},
|
|
51282
|
+
"kotlin/coroutine-global-scope": {
|
|
51283
|
+
recall: 0,
|
|
51284
|
+
fpRate: 0,
|
|
51285
|
+
ratio: 1,
|
|
51286
|
+
precision: 0,
|
|
51287
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51288
|
+
verdict: "DORMANT",
|
|
51289
|
+
_calibrationNote: "v0.24: new rule (GlobalScope.launch/async/runBlocking \u2014 bypasses structured concurrency). DORMANT until v9 Kotlin corpus calibration.",
|
|
51290
|
+
aiSpecific: true,
|
|
51291
|
+
_v7Verdict: "DORMANT",
|
|
51292
|
+
_v7Lift: 1,
|
|
51293
|
+
_v7Recall: 0,
|
|
51294
|
+
_v7FpRate: 0,
|
|
51295
|
+
_v7Precision: 0,
|
|
51296
|
+
_v8Verdict: "DORMANT",
|
|
51297
|
+
_v8Lift: 1,
|
|
51298
|
+
defaultOff: true
|
|
51299
|
+
},
|
|
51300
|
+
"kotlin/println-debug": {
|
|
51301
|
+
recall: 0,
|
|
51302
|
+
fpRate: 0,
|
|
51303
|
+
ratio: 1,
|
|
51304
|
+
precision: 0,
|
|
51305
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51306
|
+
verdict: "DORMANT",
|
|
51307
|
+
_calibrationNote: "v0.24: new rule (multi-println in Kotlin source \u2014 use Timber / android.util.Log). DORMANT until v9 Kotlin corpus calibration.",
|
|
51308
|
+
aiSpecific: true,
|
|
51309
|
+
_v7Verdict: "DORMANT",
|
|
51310
|
+
_v7Lift: 1,
|
|
51311
|
+
_v7Recall: 0,
|
|
51312
|
+
_v7FpRate: 0,
|
|
51313
|
+
_v7Precision: 0,
|
|
51314
|
+
_v8Verdict: "DORMANT",
|
|
51315
|
+
_v8Lift: 1,
|
|
51316
|
+
defaultOff: true
|
|
51317
|
+
},
|
|
51318
|
+
"kotlin/object-singleton-misuse": {
|
|
51319
|
+
recall: 0,
|
|
51320
|
+
fpRate: 0,
|
|
51321
|
+
ratio: 1,
|
|
51322
|
+
precision: 0,
|
|
51323
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51324
|
+
verdict: "DORMANT",
|
|
51325
|
+
_calibrationNote: "v0.24: new rule (Kotlin `object` singleton holding `var` \u2014 hidden shared state). DORMANT until v9 Kotlin corpus calibration.",
|
|
51326
|
+
aiSpecific: true,
|
|
51327
|
+
_v7Verdict: "DORMANT",
|
|
51328
|
+
_v7Lift: 1,
|
|
51329
|
+
_v7Recall: 0,
|
|
51330
|
+
_v7FpRate: 0,
|
|
51331
|
+
_v7Precision: 0,
|
|
51332
|
+
_v8Verdict: "DORMANT",
|
|
51333
|
+
_v8Lift: 1,
|
|
51334
|
+
defaultOff: true
|
|
51335
|
+
},
|
|
51336
|
+
"kotlin/string-concat-loop": {
|
|
51337
|
+
recall: 0,
|
|
51338
|
+
fpRate: 0,
|
|
51339
|
+
ratio: 1,
|
|
51340
|
+
precision: 0,
|
|
51341
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51342
|
+
verdict: "DORMANT",
|
|
51343
|
+
_calibrationNote: "v0.24: new rule (string concat inside a Kotlin loop \u2014 use buildString { ... }). DORMANT until v9 Kotlin corpus calibration.",
|
|
51344
|
+
aiSpecific: true,
|
|
51345
|
+
_v7Verdict: "DORMANT",
|
|
51346
|
+
_v7Lift: 1,
|
|
51347
|
+
_v7Recall: 0,
|
|
51348
|
+
_v7FpRate: 0,
|
|
51349
|
+
_v7Precision: 0,
|
|
51350
|
+
_v8Verdict: "DORMANT",
|
|
51351
|
+
_v8Lift: 1,
|
|
51352
|
+
defaultOff: true
|
|
51353
|
+
},
|
|
51354
|
+
"swift/force-unwrap": {
|
|
51355
|
+
recall: 0,
|
|
51356
|
+
fpRate: 0,
|
|
51357
|
+
ratio: 1,
|
|
51358
|
+
precision: 0,
|
|
51359
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51360
|
+
verdict: "DORMANT",
|
|
51361
|
+
_calibrationNote: "v0.24: new rule (Swift force-unwrap \u2014 `as!`, `try!`, `var!.prop` in production). DORMANT until v9 Swift corpus calibration.",
|
|
51362
|
+
aiSpecific: true,
|
|
51363
|
+
_v7Verdict: "DORMANT",
|
|
51364
|
+
_v7Lift: 1,
|
|
51365
|
+
_v7Recall: 0,
|
|
51366
|
+
_v7FpRate: 0,
|
|
51367
|
+
_v7Precision: 0,
|
|
51368
|
+
_v8Verdict: "DORMANT",
|
|
51369
|
+
_v8Lift: 1,
|
|
51370
|
+
defaultOff: true
|
|
51371
|
+
},
|
|
51372
|
+
"swift/print-debug": {
|
|
51373
|
+
recall: 0,
|
|
51374
|
+
fpRate: 0,
|
|
51375
|
+
ratio: 1,
|
|
51376
|
+
precision: 0,
|
|
51377
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51378
|
+
verdict: "DORMANT",
|
|
51379
|
+
_calibrationNote: "v0.24: new rule (multi-print in Swift source \u2014 use Logger / os.log). DORMANT until v9 Swift corpus calibration.",
|
|
51380
|
+
aiSpecific: true,
|
|
51381
|
+
_v7Verdict: "DORMANT",
|
|
51382
|
+
_v7Lift: 1,
|
|
51383
|
+
_v7Recall: 0,
|
|
51384
|
+
_v7FpRate: 0,
|
|
51385
|
+
_v7Precision: 0,
|
|
51386
|
+
_v8Verdict: "DORMANT",
|
|
51387
|
+
_v8Lift: 1,
|
|
51388
|
+
defaultOff: true
|
|
51389
|
+
},
|
|
51390
|
+
"swift/fatal-error-thrown": {
|
|
51391
|
+
recall: 0,
|
|
51392
|
+
fpRate: 0,
|
|
51393
|
+
ratio: 1,
|
|
51394
|
+
precision: 0,
|
|
51395
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51396
|
+
verdict: "DORMANT",
|
|
51397
|
+
_calibrationNote: "v0.24: new rule (`fatalError` / `preconditionFailure` in production Swift \u2014 replaces with typed default). DORMANT until v9 Swift corpus calibration.",
|
|
51398
|
+
aiSpecific: true,
|
|
51399
|
+
_v7Verdict: "DORMANT",
|
|
51400
|
+
_v7Lift: 1,
|
|
51401
|
+
_v7Recall: 0,
|
|
51402
|
+
_v7FpRate: 0,
|
|
51403
|
+
_v7Precision: 0,
|
|
51404
|
+
_v8Verdict: "DORMANT",
|
|
51405
|
+
_v8Lift: 1,
|
|
51406
|
+
defaultOff: true
|
|
51407
|
+
},
|
|
51408
|
+
"swift/implicitly-unwrapped-optional": {
|
|
51409
|
+
recall: 0,
|
|
51410
|
+
fpRate: 0,
|
|
51411
|
+
ratio: 1,
|
|
51412
|
+
precision: 0,
|
|
51413
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51414
|
+
verdict: "DORMANT",
|
|
51415
|
+
_calibrationNote: "v0.24: new rule (Swift implicitly unwrapped optional \u2014 declare as Type? and unwrap with guard let). DORMANT until v9 Swift corpus calibration.",
|
|
51416
|
+
aiSpecific: true,
|
|
51417
|
+
_v7Verdict: "DORMANT",
|
|
51418
|
+
_v7Lift: 1,
|
|
51419
|
+
_v7Recall: 0,
|
|
51420
|
+
_v7FpRate: 0,
|
|
51421
|
+
_v7Precision: 0,
|
|
51422
|
+
_v8Verdict: "DORMANT",
|
|
51423
|
+
_v8Lift: 1,
|
|
51424
|
+
defaultOff: true
|
|
51425
|
+
},
|
|
51426
|
+
"swift/strong-self-capture": {
|
|
51427
|
+
recall: 0,
|
|
51428
|
+
fpRate: 0,
|
|
51429
|
+
ratio: 1,
|
|
51430
|
+
precision: 0,
|
|
51431
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51432
|
+
verdict: "DORMANT",
|
|
51433
|
+
_calibrationNote: "v0.24: new rule (Swift closure captures `self` strongly \u2014 retain cycle. Use [weak self] / [unowned self]). DORMANT until v9 Swift corpus calibration.",
|
|
51434
|
+
aiSpecific: true,
|
|
51435
|
+
_v7Verdict: "DORMANT",
|
|
51436
|
+
_v7Lift: 1,
|
|
51437
|
+
_v7Recall: 0,
|
|
51438
|
+
_v7FpRate: 0,
|
|
51439
|
+
_v7Precision: 0,
|
|
51440
|
+
_v8Verdict: "DORMANT",
|
|
51441
|
+
_v8Lift: 1,
|
|
51442
|
+
defaultOff: true
|
|
51443
|
+
},
|
|
51444
|
+
"cpp/using-namespace-std": {
|
|
51445
|
+
recall: 0,
|
|
51446
|
+
fpRate: 0,
|
|
51447
|
+
ratio: 1,
|
|
51448
|
+
precision: 0,
|
|
51449
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51450
|
+
verdict: "DORMANT",
|
|
51451
|
+
_calibrationNote: "v0.24: new rule (`using namespace std;` in C++ header \u2014 pollutes every consumer). DORMANT until v9 C++ corpus calibration.",
|
|
51452
|
+
aiSpecific: true,
|
|
51453
|
+
_v7Verdict: "DORMANT",
|
|
51454
|
+
_v7Lift: 1,
|
|
51455
|
+
_v7Recall: 0,
|
|
51456
|
+
_v7FpRate: 0,
|
|
51457
|
+
_v7Precision: 0,
|
|
51458
|
+
_v8Verdict: "DORMANT",
|
|
51459
|
+
_v8Lift: 1,
|
|
51460
|
+
defaultOff: true
|
|
51461
|
+
},
|
|
51462
|
+
"cpp/raw-new-delete": {
|
|
51463
|
+
recall: 0,
|
|
51464
|
+
fpRate: 0,
|
|
51465
|
+
ratio: 1,
|
|
51466
|
+
precision: 0,
|
|
51467
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51468
|
+
verdict: "DORMANT",
|
|
51469
|
+
_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.",
|
|
51470
|
+
aiSpecific: true,
|
|
51471
|
+
_v7Verdict: "DORMANT",
|
|
51472
|
+
_v7Lift: 1,
|
|
51473
|
+
_v7Recall: 0,
|
|
51474
|
+
_v7FpRate: 0,
|
|
51475
|
+
_v7Precision: 0,
|
|
51476
|
+
_v8Verdict: "DORMANT",
|
|
51477
|
+
_v8Lift: 1,
|
|
51478
|
+
defaultOff: true
|
|
51479
|
+
},
|
|
51480
|
+
"cpp/c-style-cast": {
|
|
51481
|
+
recall: 0,
|
|
51482
|
+
fpRate: 0,
|
|
51483
|
+
ratio: 1,
|
|
51484
|
+
precision: 0,
|
|
51485
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51486
|
+
verdict: "DORMANT",
|
|
51487
|
+
_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.",
|
|
51488
|
+
aiSpecific: true,
|
|
51489
|
+
_v7Verdict: "DORMANT",
|
|
51490
|
+
_v7Lift: 1,
|
|
51491
|
+
_v7Recall: 0,
|
|
51492
|
+
_v7FpRate: 0,
|
|
51493
|
+
_v7Precision: 0,
|
|
51494
|
+
_v8Verdict: "DORMANT",
|
|
51495
|
+
_v8Lift: 1,
|
|
51496
|
+
defaultOff: true
|
|
51497
|
+
},
|
|
51498
|
+
"cpp/printf-debug": {
|
|
51499
|
+
recall: 0,
|
|
51500
|
+
fpRate: 0,
|
|
51501
|
+
ratio: 1,
|
|
51502
|
+
precision: 0,
|
|
51503
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51504
|
+
verdict: "DORMANT",
|
|
51505
|
+
_calibrationNote: "v0.24: new rule (printf family / std::cout debug printing in C++ \u2014 use spdlog / glog / AbslLog). DORMANT until v9 C++ corpus calibration.",
|
|
51506
|
+
aiSpecific: true,
|
|
51507
|
+
_v7Verdict: "DORMANT",
|
|
51508
|
+
_v7Lift: 1,
|
|
51509
|
+
_v7Recall: 0,
|
|
51510
|
+
_v7FpRate: 0,
|
|
51511
|
+
_v7Precision: 0,
|
|
51512
|
+
_v8Verdict: "DORMANT",
|
|
51513
|
+
_v8Lift: 1,
|
|
51514
|
+
defaultOff: true
|
|
51515
|
+
},
|
|
51516
|
+
"cpp/magic-numbers": {
|
|
51517
|
+
recall: 0,
|
|
51518
|
+
fpRate: 0,
|
|
51519
|
+
ratio: 1,
|
|
51520
|
+
precision: 0,
|
|
51521
|
+
lastCalibratedAt: "2026-07-02T00:00:00Z",
|
|
51522
|
+
verdict: "DORMANT",
|
|
51523
|
+
_calibrationNote: "v0.24: new rule (magic-number literal in C++ comparison / return \u2014 name it `constexpr int MAX = ...`). DORMANT until v9 C++ corpus calibration.",
|
|
51524
|
+
aiSpecific: true,
|
|
51525
|
+
_v7Verdict: "DORMANT",
|
|
51526
|
+
_v7Lift: 1,
|
|
51527
|
+
_v7Recall: 0,
|
|
51528
|
+
_v7FpRate: 0,
|
|
51529
|
+
_v7Precision: 0,
|
|
51530
|
+
_v8Verdict: "DORMANT",
|
|
51531
|
+
_v8Lift: 1,
|
|
51532
|
+
defaultOff: true
|
|
49864
51533
|
}
|
|
49865
51534
|
};
|
|
49866
51535
|
}
|
|
@@ -49919,7 +51588,23 @@ function applyRuleOverrides(issues, rules) {
|
|
|
49919
51588
|
}
|
|
49920
51589
|
return result;
|
|
49921
51590
|
}
|
|
51591
|
+
function isExcludedBySelfScan(filePath, cwd, excludePaths) {
|
|
51592
|
+
if (!excludePaths || excludePaths.length === 0) return false;
|
|
51593
|
+
const rel = (0, import_node_path13.relative)(cwd, filePath).split(import_node_path13.sep).join("/");
|
|
51594
|
+
return excludePaths.some((pattern) => (0, import_minimatch2.minimatch)(rel, pattern, { dot: true }));
|
|
51595
|
+
}
|
|
49922
51596
|
async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
51597
|
+
if (isExcludedBySelfScan(filePath, cwd, config.selfScan?.excludePaths)) {
|
|
51598
|
+
return {
|
|
51599
|
+
filePath,
|
|
51600
|
+
componentCount: 0,
|
|
51601
|
+
issues: [],
|
|
51602
|
+
gapValues: [],
|
|
51603
|
+
styleSources: [],
|
|
51604
|
+
elementTags: [],
|
|
51605
|
+
unmatchedStringLiterals: []
|
|
51606
|
+
};
|
|
51607
|
+
}
|
|
49923
51608
|
const cache = buildParserCacheConfig(cwd);
|
|
49924
51609
|
const ext = (0, import_node_path12.extname)(filePath).toLowerCase();
|
|
49925
51610
|
const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
|
|
@@ -49934,7 +51619,6 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
49934
51619
|
".h",
|
|
49935
51620
|
".hpp",
|
|
49936
51621
|
".hxx",
|
|
49937
|
-
".java",
|
|
49938
51622
|
".rb",
|
|
49939
51623
|
".php"
|
|
49940
51624
|
]);
|
|
@@ -50037,13 +51721,14 @@ function collectStyleSources(facts) {
|
|
|
50037
51721
|
}
|
|
50038
51722
|
return sources;
|
|
50039
51723
|
}
|
|
50040
|
-
var import_node_worker_threads, import_node_path12, import_node_path13;
|
|
51724
|
+
var import_node_worker_threads, import_node_path12, import_node_path13, import_minimatch2;
|
|
50041
51725
|
var init_worker = __esm({
|
|
50042
51726
|
"src/engine/worker.ts"() {
|
|
50043
51727
|
"use strict";
|
|
50044
51728
|
import_node_worker_threads = require("worker_threads");
|
|
50045
51729
|
import_node_path12 = require("path");
|
|
50046
51730
|
import_node_path13 = require("path");
|
|
51731
|
+
import_minimatch2 = require("minimatch");
|
|
50047
51732
|
init_dist2();
|
|
50048
51733
|
init_visitor();
|
|
50049
51734
|
init_registry();
|
|
@@ -50210,13 +51895,13 @@ function saveFlywheelState(cwd, state) {
|
|
|
50210
51895
|
(0, import_node_fs14.writeFileSync)((0, import_node_path14.join)(dir, STATE_FILE), JSON.stringify(state, null, 2));
|
|
50211
51896
|
}
|
|
50212
51897
|
function hashFile(filePath) {
|
|
50213
|
-
return (0,
|
|
51898
|
+
return (0, import_node_crypto6.createHash)("sha256").update(filePath).digest("hex").slice(0, 16);
|
|
50214
51899
|
}
|
|
50215
|
-
var
|
|
51900
|
+
var import_node_crypto6, import_node_fs14, import_node_path14, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
|
|
50216
51901
|
var init_flywheel = __esm({
|
|
50217
51902
|
"src/engine/flywheel.ts"() {
|
|
50218
51903
|
"use strict";
|
|
50219
|
-
|
|
51904
|
+
import_node_crypto6 = require("crypto");
|
|
50220
51905
|
import_node_fs14 = require("fs");
|
|
50221
51906
|
import_node_path14 = require("path");
|
|
50222
51907
|
FLYWHEEL_DIR = ".slopbrick/flywheel";
|
|
@@ -50226,45 +51911,6 @@ var init_flywheel = __esm({
|
|
|
50226
51911
|
}
|
|
50227
51912
|
});
|
|
50228
51913
|
|
|
50229
|
-
// src/engine/ai-security-risk.ts
|
|
50230
|
-
function computeAiSecurityRisk(issues) {
|
|
50231
|
-
const findings = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
50232
|
-
for (const issue of issues) {
|
|
50233
|
-
switch (issue.severity) {
|
|
50234
|
-
case "high":
|
|
50235
|
-
findings.high += 1;
|
|
50236
|
-
break;
|
|
50237
|
-
case "medium":
|
|
50238
|
-
findings.medium += 1;
|
|
50239
|
-
break;
|
|
50240
|
-
case "low":
|
|
50241
|
-
findings.low += 1;
|
|
50242
|
-
break;
|
|
50243
|
-
default:
|
|
50244
|
-
findings.low += 1;
|
|
50245
|
-
}
|
|
50246
|
-
}
|
|
50247
|
-
let risk = "low";
|
|
50248
|
-
if (findings.critical >= 1 || findings.high >= 3) {
|
|
50249
|
-
risk = "critical";
|
|
50250
|
-
} else if (findings.high >= 1 || findings.medium >= 3) {
|
|
50251
|
-
risk = "high";
|
|
50252
|
-
} else if (findings.medium >= 1) {
|
|
50253
|
-
risk = "medium";
|
|
50254
|
-
}
|
|
50255
|
-
return { risk, findings };
|
|
50256
|
-
}
|
|
50257
|
-
function formatAiSecurityRiskLine(risk, findings) {
|
|
50258
|
-
const total = findings.critical + findings.high + findings.medium + findings.low;
|
|
50259
|
-
if (total === 0) return "AI Security Risk: low";
|
|
50260
|
-
return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
|
|
50261
|
-
}
|
|
50262
|
-
var init_ai_security_risk = __esm({
|
|
50263
|
-
"src/engine/ai-security-risk.ts"() {
|
|
50264
|
-
"use strict";
|
|
50265
|
-
}
|
|
50266
|
-
});
|
|
50267
|
-
|
|
50268
51914
|
// src/engine/test-quality.ts
|
|
50269
51915
|
var test_quality_exports = {};
|
|
50270
51916
|
__export(test_quality_exports, {
|
|
@@ -50440,14 +52086,10 @@ function aggregateReport(scores, issueGroups, config, compositeScores) {
|
|
|
50440
52086
|
});
|
|
50441
52087
|
}
|
|
50442
52088
|
}
|
|
50443
|
-
const
|
|
50444
|
-
|
|
50445
|
-
|
|
50446
|
-
|
|
50447
|
-
high: 33,
|
|
50448
|
-
critical: 0
|
|
50449
|
-
};
|
|
50450
|
-
const security = securityFromRisk[risk];
|
|
52089
|
+
const securityIssueCount = flatIssues.filter(
|
|
52090
|
+
(i) => i.category === "security" || (i.ruleId ?? "").startsWith("security/")
|
|
52091
|
+
).length;
|
|
52092
|
+
const security = Math.max(0, 100 / (1 + securityIssueCount / 5));
|
|
50451
52093
|
const engineeringCategoryScores = [
|
|
50452
52094
|
categoryScores.arch ?? 0,
|
|
50453
52095
|
categoryScores.logic ?? 0,
|
|
@@ -50506,7 +52148,6 @@ var init_metrics = __esm({
|
|
|
50506
52148
|
"src/engine/metrics.ts"() {
|
|
50507
52149
|
"use strict";
|
|
50508
52150
|
import_node_path15 = require("path");
|
|
50509
|
-
init_ai_security_risk();
|
|
50510
52151
|
init_test_quality();
|
|
50511
52152
|
SEVERITY_WEIGHTS = {
|
|
50512
52153
|
low: 1,
|
|
@@ -50641,7 +52282,7 @@ function pickBaselineConfig(config) {
|
|
|
50641
52282
|
return picked;
|
|
50642
52283
|
}
|
|
50643
52284
|
function hashConfig(config) {
|
|
50644
|
-
return (0,
|
|
52285
|
+
return (0, import_node_crypto7.createHash)("sha256").update(JSON.stringify(sanitizeForHash(pickBaselineConfig(config)))).digest("hex");
|
|
50645
52286
|
}
|
|
50646
52287
|
function baselinePath(projectPath) {
|
|
50647
52288
|
return (0, import_node_path16.join)(projectPath, ".slopbrick", "cache", "baseline.json");
|
|
@@ -50721,11 +52362,11 @@ function validateBaseline(cache, configHash, gitHead) {
|
|
|
50721
52362
|
if (cache.git_head !== gitHead) return { valid: false, reason: "git_head mismatch" };
|
|
50722
52363
|
return { valid: true };
|
|
50723
52364
|
}
|
|
50724
|
-
var
|
|
52365
|
+
var import_node_crypto7, import_node_fs15, import_node_path16, BASELINE_VERSION, BASELINE_HASH_KEYS;
|
|
50725
52366
|
var init_cache = __esm({
|
|
50726
52367
|
"src/engine/cache.ts"() {
|
|
50727
52368
|
"use strict";
|
|
50728
|
-
|
|
52369
|
+
import_node_crypto7 = require("crypto");
|
|
50729
52370
|
import_node_fs15 = require("fs");
|
|
50730
52371
|
import_node_path16 = require("path");
|
|
50731
52372
|
init_types();
|
|
@@ -52438,6 +54079,45 @@ var init_business_logic = __esm({
|
|
|
52438
54079
|
}
|
|
52439
54080
|
});
|
|
52440
54081
|
|
|
54082
|
+
// src/engine/ai-security-risk.ts
|
|
54083
|
+
function computeAiSecurityRisk(issues) {
|
|
54084
|
+
const findings = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
54085
|
+
for (const issue of issues) {
|
|
54086
|
+
switch (issue.severity) {
|
|
54087
|
+
case "high":
|
|
54088
|
+
findings.high += 1;
|
|
54089
|
+
break;
|
|
54090
|
+
case "medium":
|
|
54091
|
+
findings.medium += 1;
|
|
54092
|
+
break;
|
|
54093
|
+
case "low":
|
|
54094
|
+
findings.low += 1;
|
|
54095
|
+
break;
|
|
54096
|
+
default:
|
|
54097
|
+
findings.low += 1;
|
|
54098
|
+
}
|
|
54099
|
+
}
|
|
54100
|
+
let risk = "low";
|
|
54101
|
+
if (findings.critical >= 1 || findings.high >= 3) {
|
|
54102
|
+
risk = "critical";
|
|
54103
|
+
} else if (findings.high >= 1 || findings.medium >= 3) {
|
|
54104
|
+
risk = "high";
|
|
54105
|
+
} else if (findings.medium >= 1) {
|
|
54106
|
+
risk = "medium";
|
|
54107
|
+
}
|
|
54108
|
+
return { risk, findings };
|
|
54109
|
+
}
|
|
54110
|
+
function formatAiSecurityRiskLine(risk, findings) {
|
|
54111
|
+
const total = findings.critical + findings.high + findings.medium + findings.low;
|
|
54112
|
+
if (total === 0) return "AI Security Risk: low";
|
|
54113
|
+
return `AI Security Risk: ${risk.toUpperCase()} (${total} findings)`;
|
|
54114
|
+
}
|
|
54115
|
+
var init_ai_security_risk = __esm({
|
|
54116
|
+
"src/engine/ai-security-risk.ts"() {
|
|
54117
|
+
"use strict";
|
|
54118
|
+
}
|
|
54119
|
+
});
|
|
54120
|
+
|
|
52441
54121
|
// src/engine/maintenance-cost.ts
|
|
52442
54122
|
var maintenance_cost_exports = {};
|
|
52443
54123
|
__export(maintenance_cost_exports, {
|
|
@@ -53177,7 +54857,7 @@ function computeDomainScores(issues) {
|
|
|
53177
54857
|
performance2.score = issueCountToScore(performance2.issueCount);
|
|
53178
54858
|
const security = tally(SECURITY_CATEGORIES);
|
|
53179
54859
|
security.name = "security";
|
|
53180
|
-
security.score =
|
|
54860
|
+
security.score = Math.max(0, 100 / (1 + security.issueCount / 5));
|
|
53181
54861
|
return { codeHygiene, accessibility, performance: performance2, security };
|
|
53182
54862
|
}
|
|
53183
54863
|
var COHERENCE_WEIGHTS, AI_DEBT_NUMERIC, CODE_HYGIENE_CATEGORIES, ACCESSIBILITY_CATEGORIES, PERFORMANCE_CATEGORIES, SECURITY_CATEGORIES;
|
|
@@ -53604,7 +55284,7 @@ function telemetryPath2(cwd) {
|
|
|
53604
55284
|
return (0, import_node_path21.join)(cwd, TELEMETRY_DIR, TELEMETRY_FILE2);
|
|
53605
55285
|
}
|
|
53606
55286
|
function hashString(input) {
|
|
53607
|
-
return (0,
|
|
55287
|
+
return (0, import_node_crypto8.createHash)("sha256").update(input).digest("hex").slice(0, 16);
|
|
53608
55288
|
}
|
|
53609
55289
|
function safeRelative(cwd, filePath) {
|
|
53610
55290
|
try {
|
|
@@ -53724,12 +55404,12 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
53724
55404
|
(0, import_node_fs20.appendFileSync)(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
53725
55405
|
return payload;
|
|
53726
55406
|
}
|
|
53727
|
-
var import_node_fs20,
|
|
55407
|
+
var import_node_fs20, import_node_crypto8, import_node_path21, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
|
|
53728
55408
|
var init_telemetry = __esm({
|
|
53729
55409
|
"src/engine/telemetry.ts"() {
|
|
53730
55410
|
"use strict";
|
|
53731
55411
|
import_node_fs20 = require("fs");
|
|
53732
|
-
|
|
55412
|
+
import_node_crypto8 = require("crypto");
|
|
53733
55413
|
import_node_path21 = require("path");
|
|
53734
55414
|
TELEMETRY_DIR = (0, import_node_path21.join)(".slopbrick", "flywheel");
|
|
53735
55415
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
@@ -54378,7 +56058,7 @@ function buildHelpUri(ruleId, category) {
|
|
|
54378
56058
|
}
|
|
54379
56059
|
function computeFingerprint(input) {
|
|
54380
56060
|
const payload = `${input.ruleId}|${input.fileUri}|${input.line}|${input.column}|${input.category}`;
|
|
54381
|
-
return (0,
|
|
56061
|
+
return (0, import_node_crypto9.createHash)("sha256").update(payload).digest("hex").slice(0, 16);
|
|
54382
56062
|
}
|
|
54383
56063
|
function buildRuleFromIssue(issue) {
|
|
54384
56064
|
return {
|
|
@@ -54485,11 +56165,11 @@ function formatSarif(report, options) {
|
|
|
54485
56165
|
};
|
|
54486
56166
|
return JSON.stringify(log, null, 2);
|
|
54487
56167
|
}
|
|
54488
|
-
var
|
|
56168
|
+
var import_node_crypto9, import_node_fs23, import_node_path25, REPO_INFORMATION_URI, RULES_BASE_URL;
|
|
54489
56169
|
var init_sarif = __esm({
|
|
54490
56170
|
"src/report/sarif.ts"() {
|
|
54491
56171
|
"use strict";
|
|
54492
|
-
|
|
56172
|
+
import_node_crypto9 = require("crypto");
|
|
54493
56173
|
import_node_fs23 = require("fs");
|
|
54494
56174
|
import_node_path25 = require("path");
|
|
54495
56175
|
REPO_INFORMATION_URI = "https://github.com/brickdotdev/slopbrick";
|
|
@@ -56168,6 +57848,7 @@ __export(scan_exports, {
|
|
|
56168
57848
|
async function runScan(options, explicitPaths) {
|
|
56169
57849
|
setLoggerQuiet(!!options.quiet);
|
|
56170
57850
|
const startTime = Date.now();
|
|
57851
|
+
const scanId = (0, import_node_crypto10.randomUUID)();
|
|
56171
57852
|
const cwd = (0, import_node_path30.resolve)(options.workspace ?? process.cwd());
|
|
56172
57853
|
if (!(0, import_node_fs28.existsSync)(cwd)) {
|
|
56173
57854
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
@@ -56210,7 +57891,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56210
57891
|
let files;
|
|
56211
57892
|
if (explicitPaths && explicitPaths.length > 0) {
|
|
56212
57893
|
const { globby: globby4 } = await import("globby");
|
|
56213
|
-
const { minimatch:
|
|
57894
|
+
const { minimatch: minimatch4 } = await import("minimatch");
|
|
56214
57895
|
const resolved = explicitPaths.map((p) => (0, import_node_path30.resolve)(cwd, p));
|
|
56215
57896
|
const expanded = [];
|
|
56216
57897
|
for (const p of resolved) {
|
|
@@ -56219,10 +57900,10 @@ async function runScan(options, explicitPaths) {
|
|
|
56219
57900
|
for (const f of found) {
|
|
56220
57901
|
if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path30.extname)(f).toLowerCase())) continue;
|
|
56221
57902
|
const rel = (0, import_node_path30.relative)(cwd, f).split(import_node_path30.sep).join("/");
|
|
56222
|
-
if (config.include.length > 0 && !config.include.some((pattern) =>
|
|
57903
|
+
if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
|
|
56223
57904
|
continue;
|
|
56224
57905
|
}
|
|
56225
|
-
if (config.exclude.some((pattern) =>
|
|
57906
|
+
if (config.exclude.some((pattern) => minimatch4(rel, pattern, { dot: true }))) {
|
|
56226
57907
|
continue;
|
|
56227
57908
|
}
|
|
56228
57909
|
expanded.push(f);
|
|
@@ -56421,19 +58102,31 @@ async function runScan(options, explicitPaths) {
|
|
|
56421
58102
|
config,
|
|
56422
58103
|
noIncreaseFailure,
|
|
56423
58104
|
baseline,
|
|
56424
|
-
machineReadableStdout
|
|
58105
|
+
machineReadableStdout,
|
|
58106
|
+
// v0.24.0 (Workstream C): stats for the opt-in network beacon.
|
|
58107
|
+
// `fileCount` = files that ran through the worker pool (excludes
|
|
58108
|
+
// incremental-cache skips). `ruleCount` = builtin rules, or 1
|
|
58109
|
+
// when `--rule <id>` narrows the run. `durationMs` = wall-clock
|
|
58110
|
+
// since `runScan` entry (matches `report.scanDurationMs`).
|
|
58111
|
+
scanStats: {
|
|
58112
|
+
scanId,
|
|
58113
|
+
fileCount: results.length,
|
|
58114
|
+
ruleCount: options.rule ? 1 : builtinRules.length,
|
|
58115
|
+
durationMs: Date.now() - startTime
|
|
58116
|
+
}
|
|
56425
58117
|
};
|
|
56426
58118
|
}
|
|
56427
58119
|
async function scanProject(options) {
|
|
56428
58120
|
const { report } = await runScan({ ...options, workspace: options.cwd });
|
|
56429
58121
|
return report;
|
|
56430
58122
|
}
|
|
56431
|
-
var import_node_fs28, import_node_path30;
|
|
58123
|
+
var import_node_fs28, import_node_path30, import_node_crypto10;
|
|
56432
58124
|
var init_scan2 = __esm({
|
|
56433
58125
|
"src/cli/scan.ts"() {
|
|
56434
58126
|
"use strict";
|
|
56435
58127
|
import_node_fs28 = require("fs");
|
|
56436
58128
|
import_node_path30 = require("path");
|
|
58129
|
+
import_node_crypto10 = require("crypto");
|
|
56437
58130
|
init_render();
|
|
56438
58131
|
init_threshold();
|
|
56439
58132
|
init_config2();
|
|
@@ -57862,13 +59555,35 @@ var RULE_HINTS = {
|
|
|
57862
59555
|
"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.",
|
|
57863
59556
|
// v0.19.0 — Duplication detector
|
|
57864
59557
|
"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.",
|
|
59558
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
59559
|
+
"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.)",
|
|
59560
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
59561
|
+
"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.)",
|
|
57865
59562
|
// v0.20.0 — Java-specific rules
|
|
57866
59563
|
"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.",
|
|
57867
59564
|
"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.',
|
|
57868
59565
|
"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.",
|
|
57869
59566
|
"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.",
|
|
57870
59567
|
"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.",
|
|
57871
|
-
"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)."
|
|
59568
|
+
"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).",
|
|
59569
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
59570
|
+
"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.",
|
|
59571
|
+
"kotlin/coroutine-global-scope": "Replace with `viewModelScope.launch { ... }`, `lifecycleScope.launch { ... }`, or `coroutineScope { ... }`. GlobalScope bypasses cancellation. (v0.24.0 \u2014 DORMANT.)",
|
|
59572
|
+
"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.)",
|
|
59573
|
+
"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.)",
|
|
59574
|
+
"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.)",
|
|
59575
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
59576
|
+
"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.)",
|
|
59577
|
+
"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.)",
|
|
59578
|
+
"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.)",
|
|
59579
|
+
"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.)",
|
|
59580
|
+
"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.)",
|
|
59581
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
59582
|
+
"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.)",
|
|
59583
|
+
"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.)",
|
|
59584
|
+
"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.)",
|
|
59585
|
+
"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.)",
|
|
59586
|
+
"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.)"
|
|
57872
59587
|
};
|
|
57873
59588
|
|
|
57874
59589
|
// src/cli/commands/explain.ts
|
|
@@ -59642,7 +61357,7 @@ var import_node_fs39 = require("fs");
|
|
|
59642
61357
|
var import_node_path52 = require("path");
|
|
59643
61358
|
var import_node_child_process3 = require("child_process");
|
|
59644
61359
|
var import_node_util2 = require("util");
|
|
59645
|
-
var
|
|
61360
|
+
var import_minimatch3 = require("minimatch");
|
|
59646
61361
|
init_git();
|
|
59647
61362
|
init_worker();
|
|
59648
61363
|
init_metrics();
|
|
@@ -59689,10 +61404,10 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
59689
61404
|
const ext = (0, import_node_path52.extname)(abs).toLowerCase();
|
|
59690
61405
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
59691
61406
|
const rel = (0, import_node_path52.relative)(cwd, abs).split("\\").join("/");
|
|
59692
|
-
if (config.include.length > 0 && !config.include.some((pattern) => (0,
|
|
61407
|
+
if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
|
|
59693
61408
|
continue;
|
|
59694
61409
|
}
|
|
59695
|
-
if (config.exclude.some((pattern) => (0,
|
|
61410
|
+
if (config.exclude.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
|
|
59696
61411
|
continue;
|
|
59697
61412
|
}
|
|
59698
61413
|
sourceFiles.push(abs);
|
|
@@ -62119,7 +63834,11 @@ var OPTION_CATEGORY = {
|
|
|
62119
63834
|
// Tokens
|
|
62120
63835
|
"--tokens": "tokens",
|
|
62121
63836
|
// Telemetry
|
|
62122
|
-
"--no-telemetry": "telemetry"
|
|
63837
|
+
"--no-telemetry": "telemetry",
|
|
63838
|
+
// v0.24.0 (Workstream C): opt-in network beacon. Distinct from
|
|
63839
|
+
// --no-telemetry (which gates the local flywheel); this gates a
|
|
63840
|
+
// single POST at scan end.
|
|
63841
|
+
"--report-usage": "telemetry"
|
|
62123
63842
|
};
|
|
62124
63843
|
var CATEGORY_ORDER2 = [
|
|
62125
63844
|
"file",
|
|
@@ -62188,6 +63907,109 @@ function formatGroupedHelp(cmd) {
|
|
|
62188
63907
|
return lines.join("\n");
|
|
62189
63908
|
}
|
|
62190
63909
|
|
|
63910
|
+
// src/beacon/endpoint.ts
|
|
63911
|
+
var http = __toESM(require("http"), 1);
|
|
63912
|
+
var https = __toESM(require("https"), 1);
|
|
63913
|
+
var import_node_url = require("url");
|
|
63914
|
+
var sendBeacon = (url, payload, timeoutMs) => {
|
|
63915
|
+
return new Promise((resolve42) => {
|
|
63916
|
+
let parsed;
|
|
63917
|
+
try {
|
|
63918
|
+
parsed = new import_node_url.URL(url);
|
|
63919
|
+
} catch {
|
|
63920
|
+
resolve42();
|
|
63921
|
+
return;
|
|
63922
|
+
}
|
|
63923
|
+
const isHttps = parsed.protocol === "https:";
|
|
63924
|
+
const lib = isHttps ? https : http;
|
|
63925
|
+
const body = JSON.stringify(payload);
|
|
63926
|
+
const opts = {
|
|
63927
|
+
method: "POST",
|
|
63928
|
+
hostname: parsed.hostname,
|
|
63929
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
63930
|
+
path: `${parsed.pathname || "/"}${parsed.search || ""}`,
|
|
63931
|
+
headers: {
|
|
63932
|
+
"content-type": "application/json",
|
|
63933
|
+
"content-length": Buffer.byteLength(body),
|
|
63934
|
+
"user-agent": `slopbrick/${payload.slopbrick_version}`
|
|
63935
|
+
}
|
|
63936
|
+
};
|
|
63937
|
+
let settled = false;
|
|
63938
|
+
const settle = () => {
|
|
63939
|
+
if (settled) return;
|
|
63940
|
+
settled = true;
|
|
63941
|
+
resolve42();
|
|
63942
|
+
};
|
|
63943
|
+
let req;
|
|
63944
|
+
try {
|
|
63945
|
+
req = lib.request(opts, (res) => {
|
|
63946
|
+
res.on("data", () => {
|
|
63947
|
+
});
|
|
63948
|
+
res.on("end", settle);
|
|
63949
|
+
res.on("error", settle);
|
|
63950
|
+
});
|
|
63951
|
+
} catch {
|
|
63952
|
+
settle();
|
|
63953
|
+
return;
|
|
63954
|
+
}
|
|
63955
|
+
req.on("error", settle);
|
|
63956
|
+
req.setTimeout(timeoutMs, () => {
|
|
63957
|
+
req.destroy();
|
|
63958
|
+
settle();
|
|
63959
|
+
});
|
|
63960
|
+
try {
|
|
63961
|
+
req.end(body);
|
|
63962
|
+
} catch {
|
|
63963
|
+
settle();
|
|
63964
|
+
}
|
|
63965
|
+
});
|
|
63966
|
+
};
|
|
63967
|
+
|
|
63968
|
+
// src/beacon/index.ts
|
|
63969
|
+
var BEACON_TIMEOUT_MS = 5e3;
|
|
63970
|
+
var BeaconEmitter = class {
|
|
63971
|
+
flag;
|
|
63972
|
+
envEndpoint;
|
|
63973
|
+
version;
|
|
63974
|
+
transport;
|
|
63975
|
+
constructor(opts) {
|
|
63976
|
+
this.flag = opts.flag;
|
|
63977
|
+
this.envEndpoint = opts.envEndpoint;
|
|
63978
|
+
this.version = opts.version;
|
|
63979
|
+
this.transport = opts.transport ?? sendBeacon;
|
|
63980
|
+
}
|
|
63981
|
+
/**
|
|
63982
|
+
* The two-gate opt-in. Returns true only when the user opted in
|
|
63983
|
+
* AND the endpoint env var is present and non-empty. Empty
|
|
63984
|
+
* string is treated as unset.
|
|
63985
|
+
*/
|
|
63986
|
+
shouldFire() {
|
|
63987
|
+
return this.flag && !!this.envEndpoint && this.envEndpoint.length > 0;
|
|
63988
|
+
}
|
|
63989
|
+
/**
|
|
63990
|
+
* Build the locked 8-field payload and hand it to the transport.
|
|
63991
|
+
* Never throws. The returned Promise resolves when the transport
|
|
63992
|
+
* resolves — which is always, by contract.
|
|
63993
|
+
*/
|
|
63994
|
+
async emit(stats) {
|
|
63995
|
+
if (!this.shouldFire()) return;
|
|
63996
|
+
const payload = {
|
|
63997
|
+
schema_version: "1",
|
|
63998
|
+
slopbrick_version: this.version,
|
|
63999
|
+
scan_id: stats.scanId,
|
|
64000
|
+
file_count: stats.fileCount,
|
|
64001
|
+
rule_count: stats.ruleCount,
|
|
64002
|
+
duration_ms: stats.durationMs,
|
|
64003
|
+
platform: process.platform,
|
|
64004
|
+
node_version: process.version
|
|
64005
|
+
};
|
|
64006
|
+
try {
|
|
64007
|
+
await this.transport(this.envEndpoint, payload, BEACON_TIMEOUT_MS);
|
|
64008
|
+
} catch {
|
|
64009
|
+
}
|
|
64010
|
+
}
|
|
64011
|
+
};
|
|
64012
|
+
|
|
62191
64013
|
// src/cli/program.ts
|
|
62192
64014
|
init_scan2();
|
|
62193
64015
|
process.on("uncaughtException", (err) => {
|
|
@@ -62196,7 +64018,7 @@ process.on("uncaughtException", (err) => {
|
|
|
62196
64018
|
});
|
|
62197
64019
|
async function runCli({ start }) {
|
|
62198
64020
|
try {
|
|
62199
|
-
const program = new import_commander2.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)");
|
|
64021
|
+
const program = new import_commander2.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)");
|
|
62200
64022
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
62201
64023
|
registerInit(program);
|
|
62202
64024
|
registerInstall(program);
|
|
@@ -62249,10 +64071,25 @@ async function runCli({ start }) {
|
|
|
62249
64071
|
config,
|
|
62250
64072
|
noIncreaseFailure,
|
|
62251
64073
|
baseline,
|
|
62252
|
-
machineReadableStdout
|
|
64074
|
+
machineReadableStdout,
|
|
64075
|
+
scanStats
|
|
62253
64076
|
} = await runScan(options, paths);
|
|
62254
64077
|
const scanElapsed = Math.round(performance.now() - scanStart);
|
|
62255
64078
|
const totalElapsed = Math.round(performance.now() - start);
|
|
64079
|
+
const beaconEnv = process.env.SLOPBRICK_TELEMETRY_ENDPOINT;
|
|
64080
|
+
if (options.reportUsage && beaconEnv && command.name() === "scan") {
|
|
64081
|
+
const beacon = new BeaconEmitter({
|
|
64082
|
+
flag: true,
|
|
64083
|
+
envEndpoint: beaconEnv,
|
|
64084
|
+
version: VERSION
|
|
64085
|
+
});
|
|
64086
|
+
void beacon.emit({
|
|
64087
|
+
scanId: scanStats.scanId,
|
|
64088
|
+
fileCount: scanStats.fileCount,
|
|
64089
|
+
ruleCount: scanStats.ruleCount,
|
|
64090
|
+
durationMs: scanStats.durationMs
|
|
64091
|
+
});
|
|
64092
|
+
}
|
|
62256
64093
|
if (options.baseline) {
|
|
62257
64094
|
const cwd2 = (0, import_node_path70.resolve)(options.workspace ?? process.cwd());
|
|
62258
64095
|
const configHash = hashConfig(config);
|