slopbrick 0.41.0 → 0.43.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 +45 -55
- package/bin/slopbrick.js +23 -2
- package/dist/engine/worker.cjs +198 -40
- package/dist/engine/worker.js +198 -40
- package/dist/index.cjs +1792 -715
- package/dist/index.d.cts +141 -15
- package/dist/index.d.ts +141 -15
- package/dist/index.js +1740 -664
- package/package.json +2 -2
package/dist/engine/worker.js
CHANGED
|
@@ -6868,6 +6868,26 @@ function dispatchNode(node, parent, path, vctx) {
|
|
|
6868
6868
|
// src/engine/parser-rust.ts
|
|
6869
6869
|
import Parser from "tree-sitter";
|
|
6870
6870
|
import Rust from "tree-sitter-rust";
|
|
6871
|
+
|
|
6872
|
+
// src/engine/parser-shared.ts
|
|
6873
|
+
function parseTreeSitterSource(parserResult, source) {
|
|
6874
|
+
if (!parserResult.ok) return null;
|
|
6875
|
+
if (!source || source.trim() === "") return null;
|
|
6876
|
+
try {
|
|
6877
|
+
const rawTree = parserResult.parser.parse(source);
|
|
6878
|
+
if (!rawTree) return null;
|
|
6879
|
+
const tree = rawTree;
|
|
6880
|
+
if (!tree || !tree.rootNode) return null;
|
|
6881
|
+
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
6882
|
+
return null;
|
|
6883
|
+
}
|
|
6884
|
+
return tree;
|
|
6885
|
+
} catch {
|
|
6886
|
+
return null;
|
|
6887
|
+
}
|
|
6888
|
+
}
|
|
6889
|
+
|
|
6890
|
+
// src/engine/parser-rust.ts
|
|
6871
6891
|
var cachedParser = null;
|
|
6872
6892
|
var rustLanguage = null;
|
|
6873
6893
|
var loadError = null;
|
|
@@ -6894,21 +6914,7 @@ function effectiveParser() {
|
|
|
6894
6914
|
return getRustParser();
|
|
6895
6915
|
}
|
|
6896
6916
|
function parseRust(source) {
|
|
6897
|
-
|
|
6898
|
-
if (!result.ok) return null;
|
|
6899
|
-
if (!source || source.trim() === "") return null;
|
|
6900
|
-
try {
|
|
6901
|
-
const rawTree = result.parser.parse(source);
|
|
6902
|
-
if (!rawTree) return null;
|
|
6903
|
-
const tree = rawTree;
|
|
6904
|
-
if (!tree || !tree.rootNode) return null;
|
|
6905
|
-
if (tree.rootNode.type === "ERROR" && tree.rootNode.childCount === 0) {
|
|
6906
|
-
return null;
|
|
6907
|
-
}
|
|
6908
|
-
return tree;
|
|
6909
|
-
} catch {
|
|
6910
|
-
return null;
|
|
6911
|
-
}
|
|
6917
|
+
return parseTreeSitterSource(effectiveParser(), source);
|
|
6912
6918
|
}
|
|
6913
6919
|
function isRustParserAvailable() {
|
|
6914
6920
|
const result = getRustParser();
|
|
@@ -7522,8 +7528,6 @@ function mergeTemplateClassNames(filePath, source, facts, templateClassNames) {
|
|
|
7522
7528
|
}
|
|
7523
7529
|
function extractFacts(filePath, ast, source, supportsRsc = true, framework = "react", config) {
|
|
7524
7530
|
const lineOffsets = buildLineOffsets(source);
|
|
7525
|
-
const isTypeScript = /\.tsx?$/i.test(filePath);
|
|
7526
|
-
const NON_JSX_FRAMEWORKS = /* @__PURE__ */ new Set(["vue", "svelte", "astro"]);
|
|
7527
7531
|
const facts = {
|
|
7528
7532
|
filePath,
|
|
7529
7533
|
components: [],
|
|
@@ -32947,7 +32951,7 @@ var aiCommentRatioRule = createRule({
|
|
|
32947
32951
|
message: `Comment-line ratio is ${direction} (${ratio.toFixed(2)} vs corpus mean ${mean3}). Reductive LLMs (GPT-3.5, Claude 3 Haiku) skip comments; expansive LLMs (Claude Haiku 4.5, GPT-OSS) over-comment when prompted for "well-commented code".`,
|
|
32948
32952
|
line: 1,
|
|
32949
32953
|
column: 1,
|
|
32950
|
-
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "
|
|
32954
|
+
advice: direction === "low" ? "Add doc comments explaining intent (not just restating the next line) for non-obvious logic." : "High comment ratio \u2014 consider whether the comments are intentional (api docs, type defs, tutorials). For functional code, trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
|
|
32951
32955
|
}
|
|
32952
32956
|
];
|
|
32953
32957
|
return issues;
|
|
@@ -33721,7 +33725,6 @@ function buildTrigramLM(samples, alpha = 1) {
|
|
|
33721
33725
|
logProb(context, t) {
|
|
33722
33726
|
const ctxKey = context.slice(-2).join("|");
|
|
33723
33727
|
const triKey = `${ctxKey}|${t}`;
|
|
33724
|
-
const biKey = context.slice(-1).join("|") + "|" + t;
|
|
33725
33728
|
const triCount = tri.get(triKey) ?? 0;
|
|
33726
33729
|
const biCount = bi.get(ctxKey) ?? 0;
|
|
33727
33730
|
const V = uni.size;
|
|
@@ -35514,7 +35517,7 @@ var RESERVED = /* @__PURE__ */ new Set([
|
|
|
35514
35517
|
]);
|
|
35515
35518
|
var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
35516
35519
|
var SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
35517
|
-
var CAP =
|
|
35520
|
+
var CAP = 4e3;
|
|
35518
35521
|
function walk(dir, out, cap) {
|
|
35519
35522
|
if (!existsSync(dir) || out.length >= cap) return;
|
|
35520
35523
|
let entries;
|
|
@@ -35594,6 +35597,7 @@ var staleFunctionReferenceRule = createRule({
|
|
|
35594
35597
|
if (!source) return issues;
|
|
35595
35598
|
for (const span of extractInlineCodeSpans(source)) {
|
|
35596
35599
|
const text = span.text;
|
|
35600
|
+
if (span.inBlockComment || span.inComment) continue;
|
|
35597
35601
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
35598
35602
|
if (text.length < 3) continue;
|
|
35599
35603
|
if (RESERVED.has(text.toLowerCase())) continue;
|
|
@@ -35756,6 +35760,7 @@ var stalePackageReferenceRule = createRule({
|
|
|
35756
35760
|
if (!source) return issues;
|
|
35757
35761
|
const spans = extractInlineCodeSpans(source);
|
|
35758
35762
|
for (const span of spans) {
|
|
35763
|
+
if (span.inComment) continue;
|
|
35759
35764
|
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
35760
35765
|
const lineEnd = source.indexOf("\n", span.index);
|
|
35761
35766
|
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
@@ -35801,6 +35806,8 @@ var stalePackageReferenceRule = createRule({
|
|
|
35801
35806
|
function extractInlineCodeSpans(source) {
|
|
35802
35807
|
const hits = [];
|
|
35803
35808
|
const re = /`([^`\n]+?)`/g;
|
|
35809
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
35810
|
+
const commentLines = findCommentLineRanges(source);
|
|
35804
35811
|
let m;
|
|
35805
35812
|
while ((m = re.exec(source)) !== null) {
|
|
35806
35813
|
const text = m[1] ?? "";
|
|
@@ -35808,13 +35815,22 @@ function extractInlineCodeSpans(source) {
|
|
|
35808
35815
|
const line = upTo.split("\n").length;
|
|
35809
35816
|
const lastNl = upTo.lastIndexOf("\n");
|
|
35810
35817
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
35811
|
-
hits.push({
|
|
35818
|
+
hits.push({
|
|
35819
|
+
text,
|
|
35820
|
+
line,
|
|
35821
|
+
column,
|
|
35822
|
+
index: m.index,
|
|
35823
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
35824
|
+
inComment: commentLines.has(line)
|
|
35825
|
+
});
|
|
35812
35826
|
}
|
|
35813
35827
|
return hits;
|
|
35814
35828
|
}
|
|
35815
35829
|
function extractMarkdownLinks(source) {
|
|
35816
35830
|
const hits = [];
|
|
35817
35831
|
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
35832
|
+
const blockRanges = findBlockCommentRanges(source);
|
|
35833
|
+
const commentLines = findCommentLineRanges(source);
|
|
35818
35834
|
let m;
|
|
35819
35835
|
while ((m = re.exec(source)) !== null) {
|
|
35820
35836
|
const target = m[2] ?? "";
|
|
@@ -35822,10 +35838,55 @@ function extractMarkdownLinks(source) {
|
|
|
35822
35838
|
const line = upTo.split("\n").length;
|
|
35823
35839
|
const lastNl = upTo.lastIndexOf("\n");
|
|
35824
35840
|
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
35825
|
-
hits.push({
|
|
35841
|
+
hits.push({
|
|
35842
|
+
target,
|
|
35843
|
+
line,
|
|
35844
|
+
column,
|
|
35845
|
+
index: m.index,
|
|
35846
|
+
inBlockComment: isInBlockComment(blockRanges, m.index),
|
|
35847
|
+
inComment: commentLines.has(line)
|
|
35848
|
+
});
|
|
35826
35849
|
}
|
|
35827
35850
|
return hits;
|
|
35828
35851
|
}
|
|
35852
|
+
function findBlockCommentRanges(source) {
|
|
35853
|
+
const ranges = [];
|
|
35854
|
+
const stack = [];
|
|
35855
|
+
let i = 0;
|
|
35856
|
+
while (i < source.length) {
|
|
35857
|
+
if (source[i] === "/" && source[i + 1] === "/") {
|
|
35858
|
+
const eol = source.indexOf("\n", i);
|
|
35859
|
+
i = eol === -1 ? source.length : eol + 1;
|
|
35860
|
+
continue;
|
|
35861
|
+
}
|
|
35862
|
+
if (source[i] === "/" && source[i + 1] === "*") {
|
|
35863
|
+
stack.push(i);
|
|
35864
|
+
i += 2;
|
|
35865
|
+
continue;
|
|
35866
|
+
}
|
|
35867
|
+
if (source[i] === "*" && source[i + 1] === "/" && stack.length > 0) {
|
|
35868
|
+
const start = stack.pop();
|
|
35869
|
+
ranges.push([start, i + 2]);
|
|
35870
|
+
i += 2;
|
|
35871
|
+
continue;
|
|
35872
|
+
}
|
|
35873
|
+
i += 1;
|
|
35874
|
+
}
|
|
35875
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
35876
|
+
return ranges;
|
|
35877
|
+
}
|
|
35878
|
+
function isInBlockComment(ranges, pos) {
|
|
35879
|
+
let lo = 0;
|
|
35880
|
+
let hi = ranges.length - 1;
|
|
35881
|
+
while (lo <= hi) {
|
|
35882
|
+
const mid = lo + hi >>> 1;
|
|
35883
|
+
const [start, end] = ranges[mid];
|
|
35884
|
+
if (pos < start) hi = mid - 1;
|
|
35885
|
+
else if (pos >= end) lo = mid + 1;
|
|
35886
|
+
else return true;
|
|
35887
|
+
}
|
|
35888
|
+
return false;
|
|
35889
|
+
}
|
|
35829
35890
|
function declaredPackages(cwd) {
|
|
35830
35891
|
const out = /* @__PURE__ */ new Set();
|
|
35831
35892
|
const pkgPath = join5(cwd, "package.json");
|
|
@@ -35845,8 +35906,63 @@ function declaredPackages(cwd) {
|
|
|
35845
35906
|
}
|
|
35846
35907
|
return out;
|
|
35847
35908
|
}
|
|
35909
|
+
function findCommentLineRanges(source) {
|
|
35910
|
+
const out = /* @__PURE__ */ new Set();
|
|
35911
|
+
const stack = [];
|
|
35912
|
+
let inLineComment = false;
|
|
35913
|
+
const lines = source.split("\n");
|
|
35914
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35915
|
+
const line = lines[i];
|
|
35916
|
+
const lineNo = i + 1;
|
|
35917
|
+
let j = 0;
|
|
35918
|
+
while (j < line.length) {
|
|
35919
|
+
const c = line[j];
|
|
35920
|
+
const next = line[j + 1];
|
|
35921
|
+
if (!stack.length && c === "/" && next === "/") {
|
|
35922
|
+
inLineComment = true;
|
|
35923
|
+
out.add(lineNo);
|
|
35924
|
+
break;
|
|
35925
|
+
}
|
|
35926
|
+
if (c === "/" && next === "*") {
|
|
35927
|
+
stack.push(lineNo);
|
|
35928
|
+
out.add(lineNo);
|
|
35929
|
+
j += 2;
|
|
35930
|
+
continue;
|
|
35931
|
+
}
|
|
35932
|
+
if (c === "*" && next === "/" && stack.length > 0) {
|
|
35933
|
+
stack.pop();
|
|
35934
|
+
j += 2;
|
|
35935
|
+
continue;
|
|
35936
|
+
}
|
|
35937
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
35938
|
+
const quote = c;
|
|
35939
|
+
j += 1;
|
|
35940
|
+
while (j < line.length) {
|
|
35941
|
+
if (line[j] === "\\") {
|
|
35942
|
+
j += 2;
|
|
35943
|
+
continue;
|
|
35944
|
+
}
|
|
35945
|
+
if (line[j] === quote) {
|
|
35946
|
+
j += 1;
|
|
35947
|
+
break;
|
|
35948
|
+
}
|
|
35949
|
+
j += 1;
|
|
35950
|
+
}
|
|
35951
|
+
continue;
|
|
35952
|
+
}
|
|
35953
|
+
j += 1;
|
|
35954
|
+
}
|
|
35955
|
+
if (stack.length > 0) {
|
|
35956
|
+
out.add(lineNo);
|
|
35957
|
+
}
|
|
35958
|
+
}
|
|
35959
|
+
return out;
|
|
35960
|
+
}
|
|
35848
35961
|
|
|
35849
35962
|
// src/rules/docs/broken-link.ts
|
|
35963
|
+
function looksLikeRegexSyntax(target) {
|
|
35964
|
+
return /\[[^\]]*\]/.test(target) || /^\s*[?:=!]/.test(target) || /\\(?:s|d|w|S|D|W|t|n|r|[^\\])/.test(target) || /[{}|]/.test(target);
|
|
35965
|
+
}
|
|
35850
35966
|
var brokenLinkRule = createRule({
|
|
35851
35967
|
id: "docs/broken-link",
|
|
35852
35968
|
category: "docs",
|
|
@@ -35869,6 +35985,8 @@ var brokenLinkRule = createRule({
|
|
|
35869
35985
|
if (target.startsWith("#")) continue;
|
|
35870
35986
|
if (target.startsWith("//")) continue;
|
|
35871
35987
|
if (target.startsWith("/")) continue;
|
|
35988
|
+
if (link.inBlockComment) continue;
|
|
35989
|
+
if (looksLikeRegexSyntax(target)) continue;
|
|
35872
35990
|
const filePart = target.split("#")[0] ?? target;
|
|
35873
35991
|
if (filePart === "") continue;
|
|
35874
35992
|
const resolved = join6(docDir, filePart);
|
|
@@ -37193,8 +37311,16 @@ var DEFAULT_CONFIG3 = {
|
|
|
37193
37311
|
// rule definitions are meta-code
|
|
37194
37312
|
"**/snippet/**",
|
|
37195
37313
|
// RULE_HINTS examples are intentional bad code
|
|
37196
|
-
"**/tests/**"
|
|
37314
|
+
"**/tests/**",
|
|
37197
37315
|
// all test files (fixtures + unit + integration)
|
|
37316
|
+
// v0.42.0 (post-self-scan cleanup): language visitors are
|
|
37317
|
+
// intentional clones — see the §3 v0.42.0 release notes for
|
|
37318
|
+
// the design rationale. `dup/identical-block` fires 100+
|
|
37319
|
+
// times during self-scan because each visitor shares the
|
|
37320
|
+
// same service-suffix arrays and import-walking preamble.
|
|
37321
|
+
// Suppress these from the self-scan noise floor; they
|
|
37322
|
+
// remain on for user codebases.
|
|
37323
|
+
"**/src/engine/visitors/**"
|
|
37198
37324
|
]
|
|
37199
37325
|
}
|
|
37200
37326
|
};
|
|
@@ -41095,23 +41221,16 @@ var builtinRules = [
|
|
|
41095
41221
|
// src/rules/registry.ts
|
|
41096
41222
|
var RuleRegistry = class {
|
|
41097
41223
|
rules = /* @__PURE__ */ new Map();
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41108
|
-
create: factory.create,
|
|
41109
|
-
analyze: factory.analyze
|
|
41110
|
-
};
|
|
41111
|
-
this.rules.set(ruleOrId, rule);
|
|
41112
|
-
} else {
|
|
41113
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
41114
|
-
}
|
|
41224
|
+
/**
|
|
41225
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
41226
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
41227
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
41228
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
41229
|
+
* External consumers that needed `register-by-id` semantics can
|
|
41230
|
+
* construct a `Rule` directly and pass it in.
|
|
41231
|
+
*/
|
|
41232
|
+
register(rule) {
|
|
41233
|
+
this.rules.set(rule.id, rule);
|
|
41115
41234
|
}
|
|
41116
41235
|
loadBuiltins(onlyRuleId) {
|
|
41117
41236
|
for (const rule of builtinRules) {
|
|
@@ -41140,6 +41259,15 @@ var RuleRegistry = class {
|
|
|
41140
41259
|
all() {
|
|
41141
41260
|
return Array.from(this.rules.values());
|
|
41142
41261
|
}
|
|
41262
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
41263
|
+
* Returns undefined if not registered. */
|
|
41264
|
+
get(id) {
|
|
41265
|
+
return this.rules.get(id);
|
|
41266
|
+
}
|
|
41267
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
41268
|
+
has(id) {
|
|
41269
|
+
return this.rules.has(id);
|
|
41270
|
+
}
|
|
41143
41271
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
41144
41272
|
const context = {
|
|
41145
41273
|
config,
|
|
@@ -44834,6 +44962,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
44834
44962
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
44835
44963
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
44836
44964
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
44965
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
44966
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
44967
|
+
compositeFireSet: fireSet
|
|
44968
|
+
});
|
|
44969
|
+
for (const { rule } of rules) {
|
|
44970
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
44971
|
+
try {
|
|
44972
|
+
const compositeIssues = rule.analyze(
|
|
44973
|
+
rule.create({
|
|
44974
|
+
config,
|
|
44975
|
+
filePath,
|
|
44976
|
+
cwd,
|
|
44977
|
+
framework: config.framework,
|
|
44978
|
+
uiLibraries: config.uiLibraries,
|
|
44979
|
+
hasTailwind: config.hasTailwind,
|
|
44980
|
+
supportsRsc: config.supportsRsc,
|
|
44981
|
+
hotspotIssues: []
|
|
44982
|
+
}),
|
|
44983
|
+
factsWithFireSet
|
|
44984
|
+
);
|
|
44985
|
+
if (compositeIssues.length > 0) {
|
|
44986
|
+
issues.push(...compositeIssues);
|
|
44987
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
44988
|
+
}
|
|
44989
|
+
} catch (err) {
|
|
44990
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
44991
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
44992
|
+
}
|
|
44993
|
+
}
|
|
44994
|
+
}
|
|
44837
44995
|
const gapValues = collectGapValues(facts);
|
|
44838
44996
|
const styleSources = collectStyleSources(facts);
|
|
44839
44997
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|