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.
@@ -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
- const result = effectiveParser();
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." : "Trim comments that just restate the next line. Keep comments that explain WHY, not WHAT."
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 = 200;
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({ text, line, column, index: m.index });
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({ target, line, column });
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
- register(ruleOrId, factory) {
41099
- if (typeof ruleOrId === "string") {
41100
- if (!factory) {
41101
- throw new Error("Factory is required when registering by id");
41102
- }
41103
- const rule = {
41104
- id: ruleOrId,
41105
- category: factory.category,
41106
- severity: factory.severity,
41107
- aiSpecific: factory.aiSpecific,
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);