slopbrick 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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.41.0";
39
+ VERSION = "0.42.0";
40
40
  }
41
41
  });
42
42
 
@@ -46884,11 +46884,11 @@ var init_pool = __esm({
46884
46884
  for (let i = 0; i < this.threadCount; i++) {
46885
46885
  spawnWorker();
46886
46886
  }
46887
- return new Promise((resolve42) => {
46887
+ return new Promise((resolve43) => {
46888
46888
  const check = setInterval(() => {
46889
46889
  if (resolved) {
46890
46890
  clearInterval(check);
46891
- resolve42(results);
46891
+ resolve43(results);
46892
46892
  }
46893
46893
  }, 10);
46894
46894
  });
@@ -46905,23 +46905,16 @@ var init_registry = __esm({
46905
46905
  init_builtins();
46906
46906
  RuleRegistry = class {
46907
46907
  rules = /* @__PURE__ */ new Map();
46908
- register(ruleOrId, factory) {
46909
- if (typeof ruleOrId === "string") {
46910
- if (!factory) {
46911
- throw new Error("Factory is required when registering by id");
46912
- }
46913
- const rule = {
46914
- id: ruleOrId,
46915
- category: factory.category,
46916
- severity: factory.severity,
46917
- aiSpecific: factory.aiSpecific,
46918
- create: factory.create,
46919
- analyze: factory.analyze
46920
- };
46921
- this.rules.set(ruleOrId, rule);
46922
- } else {
46923
- this.rules.set(ruleOrId.id, ruleOrId);
46924
- }
46908
+ /**
46909
+ * Register a rule. The 2-arg overload `register(id, factory)` was
46910
+ * deleted in v0.42.0 (architecture review F6): no callers used it,
46911
+ * every `Rule` carries its own `id`, and the per-id registration
46912
+ * was ergonomically redundant with the 1-arg form (`register(rule)`).
46913
+ * External consumers that needed `register-by-id` semantics can
46914
+ * construct a `Rule` directly and pass it in.
46915
+ */
46916
+ register(rule) {
46917
+ this.rules.set(rule.id, rule);
46925
46918
  }
46926
46919
  loadBuiltins(onlyRuleId) {
46927
46920
  for (const rule of builtinRules) {
@@ -46950,6 +46943,15 @@ var init_registry = __esm({
46950
46943
  all() {
46951
46944
  return Array.from(this.rules.values());
46952
46945
  }
46946
+ /** v0.42.0 (§3b.5): look up a registered rule by id.
46947
+ * Returns undefined if not registered. */
46948
+ get(id) {
46949
+ return this.rules.get(id);
46950
+ }
46951
+ /** v0.42.0 (§3b.5): check whether a rule id is registered. */
46952
+ has(id) {
46953
+ return this.rules.has(id);
46954
+ }
46953
46955
  createContexts(config, filePath, cwd, hotspotIssues = []) {
46954
46956
  const context = {
46955
46957
  config,
@@ -50601,6 +50603,10 @@ var init_signal_strength2 = __esm({
50601
50603
  });
50602
50604
 
50603
50605
  // src/engine/worker.ts
50606
+ var worker_exports = {};
50607
+ __export(worker_exports, {
50608
+ scanFile: () => scanFile
50609
+ });
50604
50610
  function buildParserCacheConfig(cwd) {
50605
50611
  const envVal = process.env.SLOP_AUDIT_CACHE;
50606
50612
  const enabled = envVal === "1" || envVal === "true";
@@ -50677,6 +50683,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
50677
50683
  const issues = applyRuleOverrides(rawIssues, config.rules);
50678
50684
  const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
50679
50685
  const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
50686
+ const fireSet = new Set(triggeredRuleIds);
50687
+ const factsWithFireSet = Object.assign(facts, {
50688
+ compositeFireSet: fireSet
50689
+ });
50690
+ for (const { rule } of rules) {
50691
+ if (!rule.id.startsWith("composite/")) continue;
50692
+ try {
50693
+ const compositeIssues = rule.analyze(
50694
+ rule.create({
50695
+ config,
50696
+ filePath,
50697
+ cwd,
50698
+ framework: config.framework,
50699
+ uiLibraries: config.uiLibraries,
50700
+ hasTailwind: config.hasTailwind,
50701
+ supportsRsc: config.supportsRsc,
50702
+ hotspotIssues: []
50703
+ }),
50704
+ factsWithFireSet
50705
+ );
50706
+ if (compositeIssues.length > 0) {
50707
+ issues.push(...compositeIssues);
50708
+ for (const ci of compositeIssues) fireSet.add(ci.ruleId);
50709
+ }
50710
+ } catch (err) {
50711
+ if (process.env.SLOP_AUDIT_DEBUG === "1") {
50712
+ console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
50713
+ }
50714
+ }
50715
+ }
50680
50716
  const gapValues = collectGapValues(facts);
50681
50717
  const styleSources = collectStyleSources(facts);
50682
50718
  const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
@@ -54542,29 +54578,29 @@ function renderStructureMarkdown(inventory, constitution) {
54542
54578
  return lines.join("\n");
54543
54579
  }
54544
54580
  async function writeStructureMarkdown(workspaceDir, md) {
54545
- await new Promise((resolve42, reject) => {
54581
+ await new Promise((resolve43, reject) => {
54546
54582
  try {
54547
54583
  const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
54548
54584
  (0, import_node_fs21.mkdirSync)((0, import_node_path23.dirname)(path), { recursive: true });
54549
54585
  (0, import_node_fs21.writeFileSync)(path, md, "utf-8");
54550
- resolve42();
54586
+ resolve43();
54551
54587
  } catch (err) {
54552
54588
  reject(err instanceof Error ? err : new Error(String(err)));
54553
54589
  }
54554
54590
  });
54555
54591
  }
54556
54592
  async function readStructureMarkdown(workspaceDir) {
54557
- return new Promise((resolve42) => {
54593
+ return new Promise((resolve43) => {
54558
54594
  try {
54559
54595
  const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
54560
54596
  if (!(0, import_node_fs21.existsSync)(path)) {
54561
- resolve42(null);
54597
+ resolve43(null);
54562
54598
  return;
54563
54599
  }
54564
54600
  const content = (0, import_node_fs21.readFileSync)(path, "utf-8");
54565
- resolve42(content);
54601
+ resolve43(content);
54566
54602
  } catch {
54567
- resolve42(null);
54603
+ resolve43(null);
54568
54604
  }
54569
54605
  });
54570
54606
  }
@@ -54614,6 +54650,388 @@ var init_structure_md = __esm({
54614
54650
  }
54615
54651
  });
54616
54652
 
54653
+ // src/snippet/data.ts
54654
+ var CATEGORY_DIRECTIVES, RULE_HINTS;
54655
+ var init_data = __esm({
54656
+ "src/snippet/data.ts"() {
54657
+ "use strict";
54658
+ CATEGORY_DIRECTIVES = {
54659
+ visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
54660
+ logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
54661
+ wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
54662
+ security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
54663
+ perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
54664
+ typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
54665
+ layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
54666
+ component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
54667
+ arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
54668
+ test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
54669
+ };
54670
+ RULE_HINTS = {
54671
+ // v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
54672
+ // remaining key matches a rule in src/rules/builtins.ts. No orphans.
54673
+ // (v0.16.0 previously moved 35 out-of-scope orphan hints to
54674
+ // docs/research/backlog-rule-hints.md
54675
+ // for future implementers to paste back when the corresponding rules ship.)
54676
+ "security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
54677
+ "security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
54678
+ "security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
54679
+ "security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
54680
+ "security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
54681
+ "security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
54682
+ "security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
54683
+ "security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
54684
+ "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
54685
+ "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
54686
+ "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
54687
+ // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
54688
+ "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
54689
+ "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
54690
+ "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
54691
+ "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
54692
+ "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
54693
+ "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
54694
+ "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
54695
+ "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
54696
+ "component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
54697
+ "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
54698
+ "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
54699
+ "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
54700
+ "layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
54701
+ "layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
54702
+ "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
54703
+ "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
54704
+ "logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
54705
+ "logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
54706
+ "logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
54707
+ "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
54708
+ "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
54709
+ "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
54710
+ "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
54711
+ "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
54712
+ "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
54713
+ "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
54714
+ "dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
54715
+ "dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
54716
+ "dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
54717
+ "dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
54718
+ "dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
54719
+ "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
54720
+ "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
54721
+ "typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
54722
+ "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
54723
+ "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
54724
+ "visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
54725
+ "visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
54726
+ "visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
54727
+ "visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
54728
+ "visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
54729
+ "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
54730
+ "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
54731
+ "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
54732
+ "test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
54733
+ "test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
54734
+ "product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
54735
+ "product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
54736
+ // v0.13.0 — AI-specific rules (peer-reviewed signals).
54737
+ "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
54738
+ "ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
54739
+ "ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
54740
+ "ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
54741
+ "ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
54742
+ "ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
54743
+ "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
54744
+ "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
54745
+ "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
54746
+ // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
54747
+ // reclassified post-v7 calibration in v0.14.5d)
54748
+ "ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
54749
+ "ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
54750
+ "ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
54751
+ "ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
54752
+ "ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
54753
+ "ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
54754
+ // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
54755
+ "db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
54756
+ // v0.17.0 — docs/* rules (markdown drift detection)
54757
+ "docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
54758
+ "docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
54759
+ "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
54760
+ // v0.18.9 — Rust-specific rules (tree-sitter-backed)
54761
+ "rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
54762
+ "rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
54763
+ "rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
54764
+ "rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
54765
+ // v0.19.0 — TypeScript-specific rules
54766
+ "ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
54767
+ "ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
54768
+ "ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
54769
+ "ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
54770
+ // v0.19.0 — Go-specific rules
54771
+ "go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
54772
+ "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.",
54773
+ // v0.19.0 — Duplication detector
54774
+ "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.",
54775
+ // v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
54776
+ "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.)",
54777
+ // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
54778
+ "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.)",
54779
+ // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
54780
+ // v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
54781
+ // v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
54782
+ "java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
54783
+ "java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
54784
+ // v0.35.0 — content-based detection (CoCoNUTS-inspired)
54785
+ "java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
54786
+ // v0.35.1 — Raidar-inspired content-based detection
54787
+ "java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
54788
+ // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
54789
+ "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.)",
54790
+ "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.)",
54791
+ "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.)",
54792
+ "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.)",
54793
+ "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.)",
54794
+ // v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
54795
+ "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.)",
54796
+ "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.)",
54797
+ "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.)",
54798
+ "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.)"
54799
+ };
54800
+ }
54801
+ });
54802
+
54803
+ // src/snippet/generators.ts
54804
+ function wrapWithMarkers(level, body) {
54805
+ const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
54806
+ return `${head}${MARKER_BEGIN}
54807
+ ${body}
54808
+ ${MARKER_END}
54809
+ `;
54810
+ }
54811
+ function generateCursorSnippet(rules) {
54812
+ const lines = [];
54813
+ lines.push("# slopbrick cursor rules");
54814
+ lines.push("");
54815
+ lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
54816
+ lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
54817
+ lines.push("");
54818
+ lines.push("## Category-level directives");
54819
+ lines.push("");
54820
+ for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
54821
+ lines.push("- **" + category + "**: " + directive);
54822
+ }
54823
+ lines.push("");
54824
+ lines.push("## Per-rule directives");
54825
+ lines.push("");
54826
+ const ai = aiSpecificRules(rules);
54827
+ const byCategory = categorizeRules(ai);
54828
+ for (const [category, list] of byCategory) {
54829
+ lines.push("### " + category);
54830
+ lines.push("");
54831
+ for (const rule of list) {
54832
+ const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
54833
+ lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
54834
+ }
54835
+ lines.push("");
54836
+ }
54837
+ lines.push("## Verification");
54838
+ lines.push("");
54839
+ lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
54840
+ lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
54841
+ lines.push("");
54842
+ return lines.join("\n");
54843
+ }
54844
+ function generateAgentsMdSnippet(rules) {
54845
+ return wrapWithMarkers(
54846
+ 1,
54847
+ baseContent(
54848
+ rules,
54849
+ "AGENTS.md",
54850
+ "Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
54851
+ ) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
54852
+ );
54853
+ }
54854
+ function generateClaudeMdSnippet(rules) {
54855
+ return wrapWithMarkers(
54856
+ 1,
54857
+ baseContent(
54858
+ rules,
54859
+ "CLAUDE.md",
54860
+ "Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
54861
+ ) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n"
54862
+ );
54863
+ }
54864
+ function generateAiderSnippet(rules) {
54865
+ return baseContent(
54866
+ rules,
54867
+ "CONVENTIONS.md",
54868
+ "Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
54869
+ );
54870
+ }
54871
+ function generateWindsurfSnippet(rules) {
54872
+ return baseContent(
54873
+ rules,
54874
+ ".windsurfrules",
54875
+ "Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
54876
+ );
54877
+ }
54878
+ function generateClineRules(rules) {
54879
+ return baseContent(
54880
+ rules,
54881
+ "AGENTS.md",
54882
+ "Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
54883
+ );
54884
+ }
54885
+ function generateCursorrulesLegacySnippet(rules) {
54886
+ return baseContent(
54887
+ rules,
54888
+ ".cursorrules",
54889
+ "Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
54890
+ );
54891
+ }
54892
+ function generateGeminiSnippet(rules) {
54893
+ return baseContent(
54894
+ rules,
54895
+ "GEMINI.md",
54896
+ "Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
54897
+ );
54898
+ }
54899
+ function generateCopilotSnippet(rules) {
54900
+ return baseContent(
54901
+ rules,
54902
+ "copilot-instructions.md",
54903
+ "Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
54904
+ );
54905
+ }
54906
+ var MARKER_BEGIN, MARKER_END;
54907
+ var init_generators = __esm({
54908
+ "src/snippet/generators.ts"() {
54909
+ "use strict";
54910
+ init_data();
54911
+ init_render2();
54912
+ MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
54913
+ MARKER_END = "<!-- slopbrick:end:v3 -->";
54914
+ }
54915
+ });
54916
+
54917
+ // src/snippet/render.ts
54918
+ function aiSpecificRules(rules) {
54919
+ return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
54920
+ }
54921
+ function categorizeRules(rules) {
54922
+ const byCategory = /* @__PURE__ */ new Map();
54923
+ for (const r of rules) {
54924
+ const list = byCategory.get(r.category) ?? [];
54925
+ list.push(r);
54926
+ byCategory.set(r.category, list);
54927
+ }
54928
+ for (const list of byCategory.values()) {
54929
+ list.sort((a, b) => a.id.localeCompare(b.id));
54930
+ }
54931
+ return byCategory;
54932
+ }
54933
+ function baseContent(rules, toolLabel, intro) {
54934
+ const lines = [];
54935
+ lines.push("# " + toolLabel);
54936
+ lines.push("");
54937
+ lines.push(intro);
54938
+ lines.push("");
54939
+ const ai = aiSpecificRules(rules);
54940
+ const byCategory = categorizeRules(ai);
54941
+ lines.push("## Category-level directives");
54942
+ lines.push("");
54943
+ for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
54944
+ lines.push("- **" + category + "**: " + directive);
54945
+ }
54946
+ lines.push("");
54947
+ lines.push("## Per-rule directives");
54948
+ lines.push("");
54949
+ for (const [category, list] of byCategory) {
54950
+ lines.push("### " + category);
54951
+ lines.push("");
54952
+ for (const rule of list) {
54953
+ const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
54954
+ lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
54955
+ }
54956
+ lines.push("");
54957
+ }
54958
+ return lines.join("\n");
54959
+ }
54960
+ function safeRewrite(existing, freshBlockBody) {
54961
+ const beginIdx = existing.indexOf(MARKER_BEGIN);
54962
+ const endIdx = existing.indexOf(MARKER_END);
54963
+ if (beginIdx === -1 && endIdx === -1) {
54964
+ return { content: existing, rewritten: false, failClosed: true, mismatched: false };
54965
+ }
54966
+ if (beginIdx === -1 || endIdx === -1) {
54967
+ return { content: existing, rewritten: false, failClosed: false, mismatched: true };
54968
+ }
54969
+ if (endIdx <= beginIdx) {
54970
+ return { content: existing, rewritten: false, failClosed: false, mismatched: true };
54971
+ }
54972
+ const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
54973
+ const after = existing.slice(endIdx);
54974
+ const next = `${before}
54975
+ ${freshBlockBody}
54976
+ ${after}`;
54977
+ return { content: next, rewritten: true, failClosed: false, mismatched: false };
54978
+ }
54979
+ var init_render2 = __esm({
54980
+ "src/snippet/render.ts"() {
54981
+ "use strict";
54982
+ init_data();
54983
+ init_generators();
54984
+ }
54985
+ });
54986
+
54987
+ // src/snippet/refresh.ts
54988
+ var refresh_exports = {};
54989
+ __export(refresh_exports, {
54990
+ SNIPPET_TARGETS: () => SNIPPET_TARGETS,
54991
+ refreshSnippets: () => refreshSnippets
54992
+ });
54993
+ function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
54994
+ const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
54995
+ for (const filename of targets) {
54996
+ const path = (0, import_node_path24.join)(cwd, filename);
54997
+ if (!(0, import_node_fs22.existsSync)(path)) {
54998
+ result.absent.push(filename);
54999
+ continue;
55000
+ }
55001
+ const existing = (0, import_node_fs22.readFileSync)(path, "utf-8");
55002
+ const agentName = filename.replace(/\.md$/i, "").toLowerCase();
55003
+ const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
55004
+ const newBlock = generator([...rules]).trim();
55005
+ const outcome = safeRewrite(existing, newBlock);
55006
+ if (outcome.rewritten) {
55007
+ const tmp = `${path}.tmp`;
55008
+ (0, import_node_fs22.writeFileSync)(tmp, outcome.content, "utf-8");
55009
+ const fs = require("fs");
55010
+ fs.renameSync(tmp, path);
55011
+ result.rewritten.push(filename);
55012
+ } else if (outcome.failClosed) {
55013
+ result.failClosed.push(filename);
55014
+ } else if (outcome.mismatched) {
55015
+ result.mismatched.push(filename);
55016
+ }
55017
+ }
55018
+ return result;
55019
+ }
55020
+ var import_node_fs22, import_node_path24, SNIPPET_TARGETS;
55021
+ var init_refresh = __esm({
55022
+ "src/snippet/refresh.ts"() {
55023
+ "use strict";
55024
+ import_node_fs22 = require("fs");
55025
+ import_node_path24 = require("path");
55026
+ init_render2();
55027
+ init_generators();
55028
+ SNIPPET_TARGETS = [
55029
+ "AGENTS.md",
55030
+ "CLAUDE.md"
55031
+ ];
55032
+ }
55033
+ });
55034
+
54617
55035
  // src/cli/report/persistRun.ts
54618
55036
  async function persistRun(input) {
54619
55037
  const {
@@ -54676,7 +55094,7 @@ async function persistRun(input) {
54676
55094
  const recentTopHashes = telemetryPayloads.map(
54677
55095
  (payload) => [...payload.files].sort((a, b) => b.score - a.score).slice(0, 10).map((file) => file.hash)
54678
55096
  );
54679
- const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0, import_node_path24.relative)(cwd, c.filePath)) }));
55097
+ const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0, import_node_path25.relative)(cwd, c.filePath)) }));
54680
55098
  const unmatchedStringLiterals = results.flatMap((r) => r.unmatchedStringLiterals ?? []);
54681
55099
  const flywheelOutput = computeFlywheelOutput(
54682
55100
  runs,
@@ -54696,10 +55114,10 @@ async function persistRun(input) {
54696
55114
  report.research = state.research;
54697
55115
  }
54698
55116
  if (flywheelOutput.suggestions.length > 0) {
54699
- const suggestionsDir = (0, import_node_path24.join)(cwd, ".slopbrick", "flywheel");
54700
- if (!(0, import_node_fs22.existsSync)(suggestionsDir)) (0, import_node_fs22.mkdirSync)(suggestionsDir, { recursive: true });
54701
- (0, import_node_fs22.writeFileSync)(
54702
- (0, import_node_path24.join)(suggestionsDir, "rule-suggestions.json"),
55117
+ const suggestionsDir = (0, import_node_path25.join)(cwd, ".slopbrick", "flywheel");
55118
+ if (!(0, import_node_fs23.existsSync)(suggestionsDir)) (0, import_node_fs23.mkdirSync)(suggestionsDir, { recursive: true });
55119
+ (0, import_node_fs23.writeFileSync)(
55120
+ (0, import_node_path25.join)(suggestionsDir, "rule-suggestions.json"),
54703
55121
  JSON.stringify(flywheelOutput.suggestions, null, 2)
54704
55122
  );
54705
55123
  }
@@ -54744,13 +55162,28 @@ async function persistRun(input) {
54744
55162
  }
54745
55163
  }
54746
55164
  recordTelemetry(cwd, report, results, config, patternInventory);
55165
+ if (input.autoRefreshSnippets) {
55166
+ try {
55167
+ const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
55168
+ const outcome = refreshSnippets2(cwd, registry.getRules());
55169
+ if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
55170
+ logger.info(
55171
+ `Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
55172
+ );
55173
+ }
55174
+ } catch (err) {
55175
+ if (options.quiet !== true && !machineReadableStdout) {
55176
+ logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
55177
+ }
55178
+ }
55179
+ }
54747
55180
  }
54748
- var import_node_fs22, import_node_path24;
55181
+ var import_node_fs23, import_node_path25;
54749
55182
  var init_persistRun = __esm({
54750
55183
  "src/cli/report/persistRun.ts"() {
54751
55184
  "use strict";
54752
- import_node_fs22 = require("fs");
54753
- import_node_path24 = require("path");
55185
+ import_node_fs23 = require("fs");
55186
+ import_node_path25 = require("path");
54754
55187
  init_logger();
54755
55188
  init_cache_incremental();
54756
55189
  init_flywheel();
@@ -54889,7 +55322,7 @@ var init_finalizeReport = __esm({
54889
55322
  function buildBaselineCache(report, configHash, gitHead, cwd) {
54890
55323
  const scores = {};
54891
55324
  for (const component of report.components) {
54892
- scores[(0, import_node_path25.relative)(cwd, component.filePath)] = {
55325
+ scores[(0, import_node_path26.relative)(cwd, component.filePath)] = {
54893
55326
  baselineScore: component.componentScore,
54894
55327
  componentCount: component.componentCount
54895
55328
  };
@@ -54904,11 +55337,11 @@ function buildBaselineCache(report, configHash, gitHead, cwd) {
54904
55337
  scores
54905
55338
  };
54906
55339
  }
54907
- var import_node_path25;
55340
+ var import_node_path26;
54908
55341
  var init_baseline_cache = __esm({
54909
55342
  "src/cli/report/baseline-cache.ts"() {
54910
55343
  "use strict";
54911
- import_node_path25 = require("path");
55344
+ import_node_path26 = require("path");
54912
55345
  init_types();
54913
55346
  }
54914
55347
  });
@@ -54972,24 +55405,24 @@ function buildArtifactUri(filePath, cwd) {
54972
55405
  return ".";
54973
55406
  }
54974
55407
  if (cwd) {
54975
- const absoluteFilePath = (0, import_node_path26.isAbsolute)(filePath) ? filePath : (0, import_node_path26.resolve)(cwd, filePath);
54976
- const rel = (0, import_node_path26.relative)(cwd, absoluteFilePath);
55408
+ const absoluteFilePath = (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
55409
+ const rel = (0, import_node_path27.relative)(cwd, absoluteFilePath);
54977
55410
  if (rel.startsWith("..")) {
54978
- return (0, import_node_path26.basename)(filePath);
55411
+ return (0, import_node_path27.basename)(filePath);
54979
55412
  }
54980
55413
  return rel;
54981
55414
  }
54982
- if ((0, import_node_path26.isAbsolute)(filePath)) {
54983
- return (0, import_node_path26.basename)(filePath);
55415
+ if ((0, import_node_path27.isAbsolute)(filePath)) {
55416
+ return (0, import_node_path27.basename)(filePath);
54984
55417
  }
54985
55418
  return filePath;
54986
55419
  }
54987
55420
  function resolveSourcePath(filePath, cwd) {
54988
55421
  if (!filePath) return null;
54989
55422
  if (cwd) {
54990
- return (0, import_node_path26.isAbsolute)(filePath) ? filePath : (0, import_node_path26.resolve)(cwd, filePath);
55423
+ return (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
54991
55424
  }
54992
- return (0, import_node_path26.isAbsolute)(filePath) ? filePath : null;
55425
+ return (0, import_node_path27.isAbsolute)(filePath) ? filePath : null;
54993
55426
  }
54994
55427
  function severityToSarifLevel(severity) {
54995
55428
  switch (severity) {
@@ -55072,7 +55505,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
55072
55505
  let content = fileContentCache.get(sourcePath);
55073
55506
  if (content === void 0) {
55074
55507
  try {
55075
- content = (0, import_node_fs23.readFileSync)(sourcePath, "utf-8");
55508
+ content = (0, import_node_fs24.readFileSync)(sourcePath, "utf-8");
55076
55509
  fileContentCache.set(sourcePath, content);
55077
55510
  } catch {
55078
55511
  fileContentCache.set(sourcePath, "");
@@ -55146,13 +55579,13 @@ function formatSarif(report, options) {
55146
55579
  };
55147
55580
  return JSON.stringify(log, null, 2);
55148
55581
  }
55149
- var import_node_crypto9, import_node_fs23, import_node_path26, REPO_INFORMATION_URI, RULES_BASE_URL;
55582
+ var import_node_crypto9, import_node_fs24, import_node_path27, REPO_INFORMATION_URI, RULES_BASE_URL;
55150
55583
  var init_sarif = __esm({
55151
55584
  "src/report/sarif.ts"() {
55152
55585
  "use strict";
55153
55586
  import_node_crypto9 = require("crypto");
55154
- import_node_fs23 = require("fs");
55155
- import_node_path26 = require("path");
55587
+ import_node_fs24 = require("fs");
55588
+ import_node_path27 = require("path");
55156
55589
  REPO_INFORMATION_URI = "https://github.com/usebrick/platform";
55157
55590
  RULES_BASE_URL = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
55158
55591
  }
@@ -56337,18 +56770,18 @@ function applyReplaceFixes(content, fixes) {
56337
56770
  return { content, applied, skipped, reasons };
56338
56771
  }
56339
56772
  function applyLayoutTokenFix(filePath, fixes) {
56340
- let content = (0, import_node_fs24.readFileSync)(filePath, "utf-8");
56773
+ let content = (0, import_node_fs25.readFileSync)(filePath, "utf-8");
56341
56774
  const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
56342
56775
  if (patched !== content) {
56343
- (0, import_node_fs24.writeFileSync)(filePath, patched);
56776
+ (0, import_node_fs25.writeFileSync)(filePath, patched);
56344
56777
  }
56345
56778
  return { applied, skipped, reasons };
56346
56779
  }
56347
- var import_node_fs24;
56780
+ var import_node_fs25;
56348
56781
  var init_layout_token = __esm({
56349
56782
  "src/fix/layout-token.ts"() {
56350
56783
  "use strict";
56351
- import_node_fs24 = require("fs");
56784
+ import_node_fs25 = require("fs");
56352
56785
  }
56353
56786
  });
56354
56787
 
@@ -56428,8 +56861,8 @@ function formatUnifiedDiff(report, cwd) {
56428
56861
  const parts = [];
56429
56862
  let hasHunk = false;
56430
56863
  for (const [filePath, fixes] of byFile) {
56431
- if (!(0, import_node_fs25.existsSync)(filePath)) continue;
56432
- const original = (0, import_node_fs25.readFileSync)(filePath, "utf-8");
56864
+ if (!(0, import_node_fs26.existsSync)(filePath)) continue;
56865
+ const original = (0, import_node_fs26.readFileSync)(filePath, "utf-8");
56433
56866
  const patched = applyFixesToString(original, fixes);
56434
56867
  if (patched === original) continue;
56435
56868
  if (!hasHunk) {
@@ -56438,7 +56871,7 @@ function formatUnifiedDiff(report, cwd) {
56438
56871
  parts.push("");
56439
56872
  hasHunk = true;
56440
56873
  }
56441
- const rel = (0, import_node_path27.relative)(cwd, filePath);
56874
+ const rel = (0, import_node_path28.relative)(cwd, filePath);
56442
56875
  parts.push(`--- a/${rel}`);
56443
56876
  parts.push(`+++ b/${rel}`);
56444
56877
  parts.push(...formatHunk(original, patched));
@@ -56449,12 +56882,12 @@ function formatUnifiedDiff(report, cwd) {
56449
56882
  }
56450
56883
  return parts.join("\n");
56451
56884
  }
56452
- var import_node_fs25, import_node_path27;
56885
+ var import_node_fs26, import_node_path28;
56453
56886
  var init_unified_diff = __esm({
56454
56887
  "src/report/unified-diff.ts"() {
56455
56888
  "use strict";
56456
- import_node_fs25 = require("fs");
56457
- import_node_path27 = require("path");
56889
+ import_node_fs26 = require("fs");
56890
+ import_node_path28 = require("path");
56458
56891
  init_layout_token();
56459
56892
  }
56460
56893
  });
@@ -56480,7 +56913,7 @@ async function buildHeatmap(report, cwd, helpers = { getFileEditCount, getFileLa
56480
56913
  const reference = new Date(report.generatedAt);
56481
56914
  const entries = await Promise.all(
56482
56915
  report.components.map(async (component) => {
56483
- const relPath = (0, import_node_path28.relative)(cwd, component.filePath) || component.filePath;
56916
+ const relPath = (0, import_node_path29.relative)(cwd, component.filePath) || component.filePath;
56484
56917
  const [edits, lastModified] = await Promise.all([
56485
56918
  helpers.getFileEditCount(cwd, relPath, RECENCY_DAYS),
56486
56919
  helpers.getFileLastModifiedDate(cwd, relPath)
@@ -56525,11 +56958,11 @@ function formatHeatmap(entries, options = {}) {
56525
56958
  );
56526
56959
  return [header, ...rows].join("\n");
56527
56960
  }
56528
- var import_node_path28, RECENCY_DAYS, MAX_EDITS;
56961
+ var import_node_path29, RECENCY_DAYS, MAX_EDITS;
56529
56962
  var init_heatmap = __esm({
56530
56963
  "src/report/heatmap.ts"() {
56531
56964
  "use strict";
56532
- import_node_path28 = require("path");
56965
+ import_node_path29 = require("path");
56533
56966
  init_git();
56534
56967
  RECENCY_DAYS = 30;
56535
56968
  MAX_EDITS = 10;
@@ -56570,7 +57003,7 @@ function renderOutput(report, options, cwd) {
56570
57003
  if (options.html) {
56571
57004
  const html = formatHtml(report);
56572
57005
  if (typeof options.html === "string") {
56573
- (0, import_node_fs26.writeFileSync)((0, import_node_path29.resolve)(options.html), html);
57006
+ (0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.html), html);
56574
57007
  if (!options.quiet) {
56575
57008
  logger.info(`Wrote HTML report to ${options.html}`);
56576
57009
  }
@@ -56582,7 +57015,7 @@ function renderOutput(report, options, cwd) {
56582
57015
  if (options.json) {
56583
57016
  const json = formatJson(report);
56584
57017
  if (typeof options.json === "string") {
56585
- (0, import_node_fs26.writeFileSync)((0, import_node_path29.resolve)(options.json), json);
57018
+ (0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.json), json);
56586
57019
  if (!options.quiet) {
56587
57020
  logger.info(`Wrote JSON report to ${options.json}`);
56588
57021
  }
@@ -56596,7 +57029,7 @@ function renderOutput(report, options, cwd) {
56596
57029
  return;
56597
57030
  }
56598
57031
  if (options.format === "sarif") {
56599
- const cwdSarif = (0, import_node_path29.resolve)(options.workspace ?? process.cwd());
57032
+ const cwdSarif = (0, import_node_path30.resolve)(options.workspace ?? process.cwd());
56600
57033
  logger.info(formatSarif(report, { cwd: cwdSarif }));
56601
57034
  return;
56602
57035
  }
@@ -56617,12 +57050,12 @@ async function outputScanResults(report, options, cwd) {
56617
57050
  }
56618
57051
  renderOutput(report, options, cwd);
56619
57052
  }
56620
- var import_node_path29, import_node_fs26, VALID_FORMATS;
57053
+ var import_node_path30, import_node_fs27, VALID_FORMATS;
56621
57054
  var init_renderOutput = __esm({
56622
57055
  "src/cli/report/renderOutput.ts"() {
56623
57056
  "use strict";
56624
- import_node_path29 = require("path");
56625
- import_node_fs26 = require("fs");
57057
+ import_node_path30 = require("path");
57058
+ import_node_fs27 = require("fs");
56626
57059
  init_logger();
56627
57060
  init_json();
56628
57061
  init_pretty();
@@ -56647,7 +57080,7 @@ async function watchProject(options, cwd, paths) {
56647
57080
  let currentBaseline;
56648
57081
  function getBaselineMtime() {
56649
57082
  try {
56650
- return (0, import_node_fs27.statSync)(baselinePath(cwd)).mtimeMs;
57083
+ return (0, import_node_fs28.statSync)(baselinePath(cwd)).mtimeMs;
56651
57084
  } catch {
56652
57085
  return void 0;
56653
57086
  }
@@ -56755,12 +57188,12 @@ async function watchProject(options, cwd, paths) {
56755
57188
  await doScan(false);
56756
57189
  if (closed) return;
56757
57190
  const currentFiles = new Set(issueGroupsMap.keys());
56758
- watcher = (0, import_node_fs27.watch)(
57191
+ watcher = (0, import_node_fs28.watch)(
56759
57192
  cwd,
56760
57193
  { recursive: true },
56761
57194
  (_eventType, filename) => {
56762
57195
  if (closed || !filename) return;
56763
- const changedPath = (0, import_node_path30.resolve)(cwd, filename.toString());
57196
+ const changedPath = (0, import_node_path31.resolve)(cwd, filename.toString());
56764
57197
  if (debounceTimer) clearTimeout(debounceTimer);
56765
57198
  debounceTimer = setTimeout(() => {
56766
57199
  debounceTimer = void 0;
@@ -56795,12 +57228,12 @@ async function watchProject(options, cwd, paths) {
56795
57228
  }
56796
57229
  );
56797
57230
  }
56798
- var import_node_fs27, import_node_path30;
57231
+ var import_node_fs28, import_node_path31;
56799
57232
  var init_watch = __esm({
56800
57233
  "src/cli/watch.ts"() {
56801
57234
  "use strict";
56802
- import_node_fs27 = require("fs");
56803
- import_node_path30 = require("path");
57235
+ import_node_fs28 = require("fs");
57236
+ import_node_path31 = require("path");
56804
57237
  init_metrics();
56805
57238
  init_worker();
56806
57239
  init_threshold();
@@ -56820,11 +57253,11 @@ async function runScan(options, explicitPaths) {
56820
57253
  setLoggerQuiet(!!options.quiet);
56821
57254
  const startTime = Date.now();
56822
57255
  const scanId = (0, import_node_crypto10.randomUUID)();
56823
- const cwd = (0, import_node_path31.resolve)(options.workspace ?? process.cwd());
56824
- if (!(0, import_node_fs28.existsSync)(cwd)) {
57256
+ const cwd = (0, import_node_path32.resolve)(options.workspace ?? process.cwd());
57257
+ if (!(0, import_node_fs29.existsSync)(cwd)) {
56825
57258
  throw new Error(`Workspace not found: ${cwd}`);
56826
57259
  }
56827
- const cwdStat = (0, import_node_fs28.statSync)(cwd);
57260
+ const cwdStat = (0, import_node_fs29.statSync)(cwd);
56828
57261
  if (!cwdStat.isDirectory()) {
56829
57262
  throw new Error(`Workspace is not a directory: ${cwd}`);
56830
57263
  }
@@ -56851,7 +57284,7 @@ async function runScan(options, explicitPaths) {
56851
57284
  process.env.SLOP_AUDIT_CACHE = "1";
56852
57285
  }
56853
57286
  if (options.tokens) {
56854
- const tokenResult = readDtcgTokensFile((0, import_node_path31.resolve)(cwd, options.tokens));
57287
+ const tokenResult = readDtcgTokensFile((0, import_node_path32.resolve)(cwd, options.tokens));
56855
57288
  if (tokenResult.ok) {
56856
57289
  const extra = tokensToAllowlist(tokenResult.tree);
56857
57290
  config.arbitraryValueAllowlist = [...config.arbitraryValueAllowlist, ...extra];
@@ -56863,14 +57296,14 @@ async function runScan(options, explicitPaths) {
56863
57296
  if (explicitPaths && explicitPaths.length > 0) {
56864
57297
  const { globby: globby4 } = await import("globby");
56865
57298
  const { minimatch: minimatch4 } = await import("minimatch");
56866
- const resolved = explicitPaths.map((p) => (0, import_node_path31.resolve)(cwd, p));
57299
+ const resolved = explicitPaths.map((p) => (0, import_node_path32.resolve)(cwd, p));
56867
57300
  const expanded = [];
56868
57301
  for (const p of resolved) {
56869
- if ((0, import_node_fs28.existsSync)(p) && (0, import_node_fs28.statSync)(p).isDirectory()) {
57302
+ if ((0, import_node_fs29.existsSync)(p) && (0, import_node_fs29.statSync)(p).isDirectory()) {
56870
57303
  const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
56871
57304
  for (const f of found) {
56872
- if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path31.extname)(f).toLowerCase())) continue;
56873
- const rel = (0, import_node_path31.relative)(cwd, f).split(import_node_path31.sep).join("/");
57305
+ if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path32.extname)(f).toLowerCase())) continue;
57306
+ const rel = (0, import_node_path32.relative)(cwd, f).split(import_node_path32.sep).join("/");
56874
57307
  if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
56875
57308
  continue;
56876
57309
  }
@@ -57020,7 +57453,7 @@ async function runScan(options, explicitPaths) {
57020
57453
  const scannedPaths = new Set(results.map((result) => result.filePath));
57021
57454
  for (const [filePath, cached] of Object.entries(baseline.scores)) {
57022
57455
  if (scannedPaths.has(filePath)) continue;
57023
- if (!(0, import_node_fs28.existsSync)(filePath)) continue;
57456
+ if (!(0, import_node_fs29.existsSync)(filePath)) continue;
57024
57457
  scores.push({
57025
57458
  filePath,
57026
57459
  rawScore: 0,
@@ -57059,7 +57492,12 @@ async function runScan(options, explicitPaths) {
57059
57492
  const { report, noIncreaseFailure } = await finalizeReport({
57060
57493
  cwd,
57061
57494
  config,
57062
- options,
57495
+ options: {
57496
+ ...options,
57497
+ // v0.42.0 (§3a.4): forward the opt-in flag through to
57498
+ // `persistRun`, which calls `refreshSnippets` post-scan.
57499
+ autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
57500
+ },
57063
57501
  results,
57064
57502
  aggregated,
57065
57503
  allIssues,
@@ -57098,12 +57536,12 @@ async function scanProject(options) {
57098
57536
  const { report } = await runScan({ ...options, workspace: options.cwd });
57099
57537
  return report;
57100
57538
  }
57101
- var import_node_fs28, import_node_path31, import_node_crypto10;
57539
+ var import_node_fs29, import_node_path32, import_node_crypto10;
57102
57540
  var init_scan2 = __esm({
57103
57541
  "src/cli/scan.ts"() {
57104
57542
  "use strict";
57105
- import_node_fs28 = require("fs");
57106
- import_node_path31 = require("path");
57543
+ import_node_fs29 = require("fs");
57544
+ import_node_path32 = require("path");
57107
57545
  import_node_crypto10 = require("crypto");
57108
57546
  init_render();
57109
57547
  init_threshold();
@@ -57131,6 +57569,50 @@ var init_scan2 = __esm({
57131
57569
  }
57132
57570
  });
57133
57571
 
57572
+ // src/rules/composite-seed.ts
57573
+ var composite_seed_exports = {};
57574
+ __export(composite_seed_exports, {
57575
+ HAND_CURATED_SEED: () => HAND_CURATED_SEED,
57576
+ maybeAppendSeed: () => maybeAppendSeed
57577
+ });
57578
+ function maybeAppendSeed(entries) {
57579
+ if (entries.length === 0) {
57580
+ return [...HAND_CURATED_SEED];
57581
+ }
57582
+ return entries;
57583
+ }
57584
+ var HAND_CURATED_SEED;
57585
+ var init_composite_seed = __esm({
57586
+ "src/rules/composite-seed.ts"() {
57587
+ "use strict";
57588
+ HAND_CURATED_SEED = [
57589
+ {
57590
+ id: "composite/ai-fingerprint-cluster",
57591
+ ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
57592
+ minMatch: 2,
57593
+ severity: "medium",
57594
+ defaultOff: true,
57595
+ description: "Composite rule: at least 2 of {ai/compression-profile+ai/comment-ratio+...} fire on the same file. Hand-curated seed from the v0.42.0 brief; superseded by `slopbrick composite discover` once empirical clusters dominate the ledger.",
57596
+ calibration: {
57597
+ recall: 0,
57598
+ FP: 0,
57599
+ precision: 0,
57600
+ F1: 0,
57601
+ nFiles: 0
57602
+ },
57603
+ provenance: {
57604
+ seed: "hand-curated-by-brief",
57605
+ discoveredAt: "2026-07-05T00:00:00.000Z",
57606
+ nFiles: 0,
57607
+ members: 3,
57608
+ npmi: 0,
57609
+ fisherP: 1
57610
+ }
57611
+ }
57612
+ ];
57613
+ }
57614
+ });
57615
+
57134
57616
  // src/index.ts
57135
57617
  var src_exports = {};
57136
57618
  __export(src_exports, {
@@ -57217,7 +57699,7 @@ init_config2();
57217
57699
  init_dist2();
57218
57700
 
57219
57701
  // src/cli/program.ts
57220
- var import_node_path72 = require("path");
57702
+ var import_node_path75 = require("path");
57221
57703
  var import_commander3 = require("commander");
57222
57704
 
57223
57705
  // src/cli/options.ts
@@ -57260,8 +57742,8 @@ init_scan2();
57260
57742
  init_scan2();
57261
57743
 
57262
57744
  // src/cli/init.ts
57263
- var import_node_fs30 = require("fs");
57264
- var import_node_path33 = require("path");
57745
+ var import_node_fs31 = require("fs");
57746
+ var import_node_path34 = require("path");
57265
57747
  var import_node_readline = require("readline");
57266
57748
  init_config2();
57267
57749
  init_discover();
@@ -57270,8 +57752,8 @@ init_cache();
57270
57752
  init_logger();
57271
57753
 
57272
57754
  // src/rules/registry-loader.ts
57273
- var import_node_fs29 = require("fs");
57274
- var import_node_path32 = require("path");
57755
+ var import_node_fs30 = require("fs");
57756
+ var import_node_path33 = require("path");
57275
57757
 
57276
57758
  // src/data/shadcn-registry.json
57277
57759
  var shadcn_registry_default = {
@@ -57405,12 +57887,12 @@ var shadcn_registry_default = {
57405
57887
  var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
57406
57888
  var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
57407
57889
  function cachePath2(cwd) {
57408
- return (0, import_node_path32.join)(cwd, ".slopbrick", "cache", "registry-snapshot.json");
57890
+ return (0, import_node_path33.join)(cwd, ".slopbrick", "cache", "registry-snapshot.json");
57409
57891
  }
57410
57892
  function ensureCacheDir(cwd) {
57411
- const dir = (0, import_node_path32.dirname)(cachePath2(cwd));
57412
- if (!(0, import_node_fs29.existsSync)(dir)) {
57413
- (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
57893
+ const dir = (0, import_node_path33.dirname)(cachePath2(cwd));
57894
+ if (!(0, import_node_fs30.existsSync)(dir)) {
57895
+ (0, import_node_fs30.mkdirSync)(dir, { recursive: true });
57414
57896
  }
57415
57897
  }
57416
57898
  function isValidSnapshot(value) {
@@ -57422,9 +57904,9 @@ function isValidSnapshot(value) {
57422
57904
  }
57423
57905
  function isRegistryFresh(cwd) {
57424
57906
  const cached = cachePath2(cwd);
57425
- if (!(0, import_node_fs29.existsSync)(cached)) return false;
57907
+ if (!(0, import_node_fs30.existsSync)(cached)) return false;
57426
57908
  try {
57427
- const parsed = JSON.parse((0, import_node_fs29.readFileSync)(cached, "utf8"));
57909
+ const parsed = JSON.parse((0, import_node_fs30.readFileSync)(cached, "utf8"));
57428
57910
  if (!isValidSnapshot(parsed)) return false;
57429
57911
  return parsed.version === BUNDLED_REGISTRY_VERSION;
57430
57912
  } catch {
@@ -57435,7 +57917,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
57435
57917
  try {
57436
57918
  const cacheFile = cachePath2(cwd);
57437
57919
  ensureCacheDir(cwd);
57438
- (0, import_node_fs29.writeFileSync)(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
57920
+ (0, import_node_fs30.writeFileSync)(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
57439
57921
  } catch {
57440
57922
  }
57441
57923
  return {
@@ -57447,7 +57929,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
57447
57929
  }
57448
57930
  function copyBundledSnapshotToCache(cwd) {
57449
57931
  ensureCacheDir(cwd);
57450
- (0, import_node_fs29.writeFileSync)(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
57932
+ (0, import_node_fs30.writeFileSync)(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
57451
57933
  }
57452
57934
 
57453
57935
  // src/cli/init.ts
@@ -57476,17 +57958,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
57476
57958
  var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
57477
57959
  var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
57478
57960
  function promptText(rl, question, detected) {
57479
- return new Promise((resolve42) => {
57961
+ return new Promise((resolve43) => {
57480
57962
  const lines = [
57481
57963
  `? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
57482
57964
  ];
57483
57965
  rl.question(lines.join("\n") + "\n", (answer) => {
57484
57966
  const trimmed = answer.trim();
57485
57967
  if (trimmed === "") {
57486
- resolve42(void 0);
57968
+ resolve43(void 0);
57487
57969
  return;
57488
57970
  }
57489
- resolve42(trimmed);
57971
+ resolve43(trimmed);
57490
57972
  });
57491
57973
  });
57492
57974
  }
@@ -57494,7 +57976,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57494
57976
  const defaultIndex = options.indexOf(defaultValue);
57495
57977
  const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
57496
57978
  const safeDefaultValue = options[safeDefaultIndex];
57497
- return new Promise((resolve42) => {
57979
+ return new Promise((resolve43) => {
57498
57980
  function ask() {
57499
57981
  const lines = [
57500
57982
  `? ${question} (detected: ${safeDefaultValue}):`,
@@ -57504,7 +57986,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57504
57986
  rl.question(lines.join("\n") + "\n", (answer) => {
57505
57987
  const trimmed = answer.trim();
57506
57988
  if (trimmed === "") {
57507
- resolve42(safeDefaultValue);
57989
+ resolve43(safeDefaultValue);
57508
57990
  return;
57509
57991
  }
57510
57992
  const num = parseInt(trimmed, 10);
@@ -57513,7 +57995,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57513
57995
  ask();
57514
57996
  return;
57515
57997
  }
57516
- resolve42(options[num - 1]);
57998
+ resolve43(options[num - 1]);
57517
57999
  });
57518
58000
  }
57519
58001
  ask();
@@ -57523,7 +58005,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57523
58005
  const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
57524
58006
  const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
57525
58007
  const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
57526
- return new Promise((resolve42) => {
58008
+ return new Promise((resolve43) => {
57527
58009
  function ask() {
57528
58010
  const lines = [
57529
58011
  `? ${question} (detected: ${defaultDisplay}):`,
@@ -57534,7 +58016,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57534
58016
  rl.question(lines.join("\n") + "\n", (answer) => {
57535
58017
  const trimmed = answer.trim();
57536
58018
  if (trimmed === "") {
57537
- resolve42(defaultValue.length > 0 ? defaultValue : []);
58019
+ resolve43(defaultValue.length > 0 ? defaultValue : []);
57538
58020
  return;
57539
58021
  }
57540
58022
  const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
@@ -57544,11 +58026,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57544
58026
  return;
57545
58027
  }
57546
58028
  if (numbers.includes(1)) {
57547
- resolve42([]);
58029
+ resolve43([]);
57548
58030
  return;
57549
58031
  }
57550
58032
  const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
57551
- resolve42(selected);
58033
+ resolve43(selected);
57552
58034
  });
57553
58035
  }
57554
58036
  ask();
@@ -57636,11 +58118,11 @@ async function runDoctor(cwd) {
57636
58118
  }
57637
58119
  try {
57638
58120
  const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
57639
- const testFile = (0, import_node_path33.join)(cwd, ".slopbrick", ".doctor-test.ts");
57640
- (0, import_node_fs30.mkdirSync)((0, import_node_path33.dirname)(testFile), { recursive: true });
57641
- (0, import_node_fs30.writeFileSync)(testFile, "export const x = 1;\n");
58121
+ const testFile = (0, import_node_path34.join)(cwd, ".slopbrick", ".doctor-test.ts");
58122
+ (0, import_node_fs31.mkdirSync)((0, import_node_path34.dirname)(testFile), { recursive: true });
58123
+ (0, import_node_fs31.writeFileSync)(testFile, "export const x = 1;\n");
57642
58124
  await tryParse(testFile);
57643
- (0, import_node_fs30.rmSync)(testFile, { force: true });
58125
+ (0, import_node_fs31.rmSync)(testFile, { force: true });
57644
58126
  ok("Parser is working.");
57645
58127
  } catch (err) {
57646
58128
  const message = err instanceof Error ? err.message : String(err);
@@ -57729,7 +58211,7 @@ async function runDoctor(cwd) {
57729
58211
  }
57730
58212
 
57731
58213
  // src/cli/commands/badge.ts
57732
- var import_node_path34 = require("path");
58214
+ var import_node_path35 = require("path");
57733
58215
  init_render();
57734
58216
  init_logger();
57735
58217
  init_scan2();
@@ -57738,7 +58220,7 @@ function registerBadge(program) {
57738
58220
  "print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
57739
58221
  ).action(async (_cmdOptions, command) => {
57740
58222
  const options = command.optsWithGlobals();
57741
- const cwd = (0, import_node_path34.resolve)(options.workspace ?? process.cwd());
58223
+ const cwd = (0, import_node_path35.resolve)(options.workspace ?? process.cwd());
57742
58224
  const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
57743
58225
  const health = loadHealth2(cwd);
57744
58226
  if (health) {
@@ -57755,7 +58237,7 @@ function registerBadge(program) {
57755
58237
  }
57756
58238
 
57757
58239
  // src/cli/commands/suggest.ts
57758
- var import_node_path35 = require("path");
58240
+ var import_node_path36 = require("path");
57759
58241
  init_advice();
57760
58242
  init_unified_diff();
57761
58243
  init_logger();
@@ -57764,7 +58246,7 @@ function registerSuggest(program) {
57764
58246
  program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
57765
58247
  const options = command.optsWithGlobals();
57766
58248
  const { report } = await runScan(options);
57767
- const cwd = (0, import_node_path35.resolve)(options.workspace ?? process.cwd());
58249
+ const cwd = (0, import_node_path36.resolve)(options.workspace ?? process.cwd());
57768
58250
  logger.info(formatAdvice(report));
57769
58251
  const diff = formatUnifiedDiff(report, cwd);
57770
58252
  if (diff) logger.info(diff);
@@ -57772,6 +58254,470 @@ function registerSuggest(program) {
57772
58254
  });
57773
58255
  }
57774
58256
 
58257
+ // src/cli/commands/composite.ts
58258
+ var import_node_path38 = require("path");
58259
+ init_logger();
58260
+ init_telemetry();
58261
+
58262
+ // src/engine/cluster.ts
58263
+ var import_node_crypto11 = require("crypto");
58264
+ var DEFAULT_CLUSTER_PARAMS = {
58265
+ minSupport: 0.05,
58266
+ // 5% of files
58267
+ minNPMI: 0.3,
58268
+ // Cohen's-Landis-and-Koch "fair" agreement
58269
+ fisherAlpha: 0.01,
58270
+ // Bonferroni-style floor for many-pair tests
58271
+ minClusterSize: 2
58272
+ };
58273
+ function adaptiveMinSupport(fileCount) {
58274
+ if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
58275
+ return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
58276
+ }
58277
+ function computeRuleSupport(fireMatrix) {
58278
+ const support = /* @__PURE__ */ new Map();
58279
+ for (const ruleIds of fireMatrix.values()) {
58280
+ for (const ruleId of ruleIds) {
58281
+ support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
58282
+ }
58283
+ }
58284
+ return support;
58285
+ }
58286
+ function prefilterBySupport(support, totalFiles, minSupport) {
58287
+ const kept = /* @__PURE__ */ new Set();
58288
+ const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
58289
+ for (const [ruleId, count] of support) {
58290
+ if (count >= minCount) kept.add(ruleId);
58291
+ }
58292
+ return kept;
58293
+ }
58294
+ function buildContingency(rA, rB, totalFiles) {
58295
+ let aAndB = 0;
58296
+ let aOnly = 0;
58297
+ let bOnly = 0;
58298
+ for (const file of rA) {
58299
+ if (rB.has(file)) aAndB++;
58300
+ else aOnly++;
58301
+ }
58302
+ for (const file of rB) {
58303
+ if (!rA.has(file)) bOnly++;
58304
+ }
58305
+ return {
58306
+ aAndB,
58307
+ aOnly,
58308
+ bOnly,
58309
+ neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
58310
+ };
58311
+ }
58312
+ function fisherExactTwoSided(c) {
58313
+ const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
58314
+ const K1 = c.aAndB + c.aOnly;
58315
+ const K2 = c.aAndB + c.bOnly;
58316
+ const n = K1;
58317
+ const kObs = c.aAndB;
58318
+ const logPmf = (k) => {
58319
+ if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
58320
+ return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
58321
+ };
58322
+ const pObs = Math.exp(logPmf(kObs));
58323
+ const kMin = Math.max(0, K1 + K2 - N);
58324
+ const kMax = Math.min(K1, K2);
58325
+ let p = 0;
58326
+ for (let k = kMin; k <= kMax; k++) {
58327
+ const pk = Math.exp(logPmf(k));
58328
+ if (pk <= pObs + 1e-12) {
58329
+ p += pk;
58330
+ }
58331
+ }
58332
+ return Math.min(1, p);
58333
+ }
58334
+ function lnChoose(n, k) {
58335
+ if (k < 0 || k > n) return -Infinity;
58336
+ if (k === 0 || k === n) return 0;
58337
+ k = Math.min(k, n - k);
58338
+ let s = 0;
58339
+ for (let i = 1; i <= k; i++) {
58340
+ s += Math.log((n - k + i) / i);
58341
+ }
58342
+ return s;
58343
+ }
58344
+ function npmi(c, total) {
58345
+ if (total === 0 || c.aAndB === 0) return -1;
58346
+ const pXY = c.aAndB / total;
58347
+ const pX = (c.aAndB + c.aOnly) / total;
58348
+ const pY = (c.aAndB + c.bOnly) / total;
58349
+ if (pX === 0 || pY === 0 || pXY === 0) return -1;
58350
+ const pmi = Math.log2(pXY / (pX * pY));
58351
+ const norm = -Math.log2(pXY);
58352
+ return norm === 0 ? 1 : pmi / norm;
58353
+ }
58354
+ function computeEdges(fireMatrix, rules, total, params) {
58355
+ const ruleToFiles = /* @__PURE__ */ new Map();
58356
+ for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
58357
+ for (const [file, fired] of fireMatrix) {
58358
+ for (const ruleId of fired) {
58359
+ if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
58360
+ }
58361
+ }
58362
+ const ruleList = Array.from(rules).sort();
58363
+ const edges = [];
58364
+ for (let i = 0; i < ruleList.length; i++) {
58365
+ for (let j = i + 1; j < ruleList.length; j++) {
58366
+ const a = ruleList[i];
58367
+ const b = ruleList[j];
58368
+ const rA = ruleToFiles.get(a);
58369
+ const rB = ruleToFiles.get(b);
58370
+ const c = buildContingency(rA, rB, total);
58371
+ const n = npmi(c, total);
58372
+ if (n < params.minNPMI) continue;
58373
+ const p = fisherExactTwoSided(c);
58374
+ if (p > params.fisherAlpha) continue;
58375
+ edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
58376
+ }
58377
+ }
58378
+ return edges;
58379
+ }
58380
+ function singleLinkageCluster(edges, minClusterSize) {
58381
+ const parent = /* @__PURE__ */ new Map();
58382
+ const find = (x) => {
58383
+ if (!parent.has(x)) {
58384
+ parent.set(x, x);
58385
+ return x;
58386
+ }
58387
+ if (parent.get(x) === x) return x;
58388
+ const root = find(parent.get(x));
58389
+ parent.set(x, root);
58390
+ return root;
58391
+ };
58392
+ const union = (a, b) => {
58393
+ const ra = find(a);
58394
+ const rb = find(b);
58395
+ if (ra !== rb) parent.set(ra, rb);
58396
+ };
58397
+ for (const e of edges) {
58398
+ union(e.ruleIds[0], e.ruleIds[1]);
58399
+ }
58400
+ const groups = /* @__PURE__ */ new Map();
58401
+ for (const k of parent.keys()) {
58402
+ const root = find(k);
58403
+ if (!groups.has(root)) groups.set(root, []);
58404
+ groups.get(root).push(k);
58405
+ }
58406
+ const clusters = [];
58407
+ for (const members of groups.values()) {
58408
+ if (members.length < minClusterSize) continue;
58409
+ members.sort();
58410
+ clusters.push(members);
58411
+ }
58412
+ clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
58413
+ return clusters;
58414
+ }
58415
+ function filesForComposite(fireMatrix, members, minMatch) {
58416
+ const fired = /* @__PURE__ */ new Set();
58417
+ for (const [file, ids] of fireMatrix) {
58418
+ let n = 0;
58419
+ for (const m of members) if (ids.has(m)) n++;
58420
+ if (n >= minMatch) fired.add(file);
58421
+ }
58422
+ return fired;
58423
+ }
58424
+ var DEFAULT_CALIBRATION = {
58425
+ // 1.5× is the documented floor.
58426
+ recallFpRatio: 1.5,
58427
+ // 0.05 absolute FP — the same `min` value most rules get via the
58428
+ // v0.18.x calibration contract.
58429
+ minFpFloor: 0.05
58430
+ };
58431
+ function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
58432
+ if (cluster2.length < 2) return null;
58433
+ const clusterSet = new Set(cluster2);
58434
+ const clusterEdges = edges.filter(
58435
+ (e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
58436
+ );
58437
+ if (clusterEdges.length === 0) return null;
58438
+ const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
58439
+ let best = null;
58440
+ for (let k = 1; k <= cluster2.length; k++) {
58441
+ const firedFiles = filesForComposite(fireMatrix, cluster2, k);
58442
+ const nFiles = firedFiles.size;
58443
+ if (nFiles === 0) continue;
58444
+ let recall = 0;
58445
+ let fp = 0;
58446
+ let precision = 0;
58447
+ let canCalibrate = false;
58448
+ if (positiveFiles && positiveFiles.size > 0) {
58449
+ let tp = 0;
58450
+ let fpc = 0;
58451
+ for (const file of firedFiles) {
58452
+ if (positiveFiles.has(file)) tp++;
58453
+ else fpc++;
58454
+ }
58455
+ const totalPos = positiveFiles.size;
58456
+ const totalNeg = Math.max(1, totalFiles - totalPos);
58457
+ recall = totalPos > 0 ? tp / totalPos : 0;
58458
+ fp = fpc / totalNeg;
58459
+ precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
58460
+ canCalibrate = true;
58461
+ }
58462
+ const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
58463
+ if (canCalibrate) {
58464
+ if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
58465
+ if (fp < thresholds.minFpFloor && recall < 0.5) continue;
58466
+ }
58467
+ if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
58468
+ best = {
58469
+ minMatch: k,
58470
+ npmi: strongest.weight,
58471
+ fisherP: strongest.fisherP,
58472
+ recall,
58473
+ FP: fp,
58474
+ precision,
58475
+ F1,
58476
+ nFiles
58477
+ };
58478
+ }
58479
+ }
58480
+ return best;
58481
+ }
58482
+ function compositeId(members) {
58483
+ const sorted = [...members].sort();
58484
+ return "composite/" + (0, import_node_crypto11.createHash)("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
58485
+ }
58486
+ function worstSeverity(members, memberSeverities) {
58487
+ let worst = "low";
58488
+ const order = { low: 0, medium: 1, high: 2 };
58489
+ for (const m of members) {
58490
+ const s = memberSeverities?.get(m);
58491
+ if (s && order[s] > order[worst]) worst = s;
58492
+ }
58493
+ return worst;
58494
+ }
58495
+ function describeComposite(members, minMatch) {
58496
+ const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
58497
+ return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
58498
+ }
58499
+ function runClusterer(input) {
58500
+ const total = input.fireMatrix.size;
58501
+ const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
58502
+ const params = {
58503
+ minSupport,
58504
+ minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
58505
+ fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
58506
+ minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
58507
+ };
58508
+ const support = computeRuleSupport(input.fireMatrix);
58509
+ const rulesKept = prefilterBySupport(support, total, params.minSupport);
58510
+ const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
58511
+ const clusters = singleLinkageCluster(edges, params.minClusterSize);
58512
+ const entries = [];
58513
+ const clusterMaxNPMI = /* @__PURE__ */ new Map();
58514
+ for (const cluster2 of clusters) {
58515
+ const cal = calibrateCluster(
58516
+ cluster2,
58517
+ edges,
58518
+ input.fireMatrix,
58519
+ input.positiveFiles,
58520
+ total
58521
+ );
58522
+ if (!cal) continue;
58523
+ const id = compositeId(cluster2);
58524
+ const severity = worstSeverity(cluster2, input.memberSeverities);
58525
+ entries.push({
58526
+ id,
58527
+ ruleIds: [...cluster2].sort(),
58528
+ minMatch: cal.minMatch,
58529
+ severity,
58530
+ defaultOff: true,
58531
+ description: describeComposite(cluster2, cal.minMatch),
58532
+ calibration: {
58533
+ recall: round3(cal.recall),
58534
+ FP: round3(cal.FP),
58535
+ precision: round3(cal.precision),
58536
+ F1: round3(cal.F1),
58537
+ nFiles: cal.nFiles
58538
+ },
58539
+ provenance: {
58540
+ seed: "auto-cluster",
58541
+ discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
58542
+ nFiles: total,
58543
+ members: cluster2.length,
58544
+ npmi: round3(cal.npmi),
58545
+ fisherP: round3(cal.fisherP)
58546
+ }
58547
+ });
58548
+ clusterMaxNPMI.set(id, cal.npmi);
58549
+ }
58550
+ entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
58551
+ return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
58552
+ }
58553
+ function round3(n) {
58554
+ return Math.round(n * 1e3) / 1e3;
58555
+ }
58556
+
58557
+ // src/rules/composite-loader.ts
58558
+ var import_node_fs32 = require("fs");
58559
+ var import_node_path37 = require("path");
58560
+ var COMPOSITES_FILE = "composites.json";
58561
+ function compositesPath(cwd) {
58562
+ return (0, import_node_path37.join)(cwd, COMPOSITES_FILE);
58563
+ }
58564
+ function readComposites(cwd) {
58565
+ const path = compositesPath(cwd);
58566
+ if (!(0, import_node_fs32.existsSync)(path)) return [];
58567
+ try {
58568
+ const raw = JSON.parse((0, import_node_fs32.readFileSync)(path, "utf-8"));
58569
+ if (!Array.isArray(raw)) return [];
58570
+ return raw.filter(isCompositeRuleEntry);
58571
+ } catch {
58572
+ return [];
58573
+ }
58574
+ }
58575
+ function isCompositeRuleEntry(value) {
58576
+ if (!value || typeof value !== "object") return false;
58577
+ const v = value;
58578
+ return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
58579
+ }
58580
+ function writeComposites(cwd, entries) {
58581
+ const path = compositesPath(cwd);
58582
+ (0, import_node_fs32.mkdirSync)((0, import_node_path37.dirname)(path), { recursive: true });
58583
+ const tmp = `${path}.tmp`;
58584
+ (0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(entries, null, 2), "utf-8");
58585
+ const fs = require("fs");
58586
+ fs.renameSync(tmp, path);
58587
+ }
58588
+
58589
+ // src/cli/commands/composite.ts
58590
+ function registerComposite(program) {
58591
+ const cmd = program.command("composite").description(
58592
+ "discover and enable composite rules (Sprint 3, \xA73b). Run `slopbrick composite discover` to scan scans.jsonl, audit the candidates, then `slopbrick composite enable <id>` to opt a specific composite into slopbrick.config.mjs."
58593
+ );
58594
+ cmd.command("discover").description(
58595
+ "run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
58596
+ ).action(async (_cmdOptions, command) => {
58597
+ const options = command.optsWithGlobals();
58598
+ const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
58599
+ const payloads = readTelemetry(cwd);
58600
+ if (payloads.length === 0) {
58601
+ logger.error(
58602
+ "`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
58603
+ );
58604
+ process.exit(1);
58605
+ }
58606
+ const fireMatrix = /* @__PURE__ */ new Map();
58607
+ for (const payload of payloads) {
58608
+ for (const f of payload.files) {
58609
+ const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
58610
+ for (const id of f.ruleIds) set.add(id);
58611
+ if (set.size > 0) fireMatrix.set(f.hash, set);
58612
+ }
58613
+ }
58614
+ const out = runClusterer({
58615
+ fireMatrix,
58616
+ // No labeled positive set; let the clusterer surface for audit.
58617
+ positiveFiles: void 0,
58618
+ now: (/* @__PURE__ */ new Date()).toISOString()
58619
+ });
58620
+ const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
58621
+ writeComposites(cwd, finalEntries);
58622
+ if (out.entries.length === 0) {
58623
+ logger.info("Composite discovery: no clusters passed the empirical gate.");
58624
+ logger.info(
58625
+ " (Try lowering --min-support, or run more scans to densify the ledger.)"
58626
+ );
58627
+ return;
58628
+ }
58629
+ logger.info(
58630
+ `Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
58631
+ );
58632
+ const lines = [];
58633
+ for (const e of out.entries) {
58634
+ lines.push("");
58635
+ lines.push(
58636
+ ` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
58637
+ );
58638
+ lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
58639
+ lines.push(
58640
+ ` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
58641
+ );
58642
+ }
58643
+ logger.info(lines.join("\n"));
58644
+ logger.info(
58645
+ "\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
58646
+ );
58647
+ });
58648
+ cmd.command("enable <id>").description(
58649
+ "opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
58650
+ ).action(async (id, _cmdOptions, command) => {
58651
+ const options = command.optsWithGlobals();
58652
+ const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
58653
+ const composites = readComposites(cwd);
58654
+ const target = composites.find((c) => c.id === id);
58655
+ if (!target) {
58656
+ logger.error(
58657
+ `Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
58658
+ );
58659
+ process.exit(1);
58660
+ }
58661
+ const cfgPath = (0, import_node_path38.resolve)(cwd, "slopbrick.config.mjs");
58662
+ const fs = require("fs");
58663
+ if (!fs.existsSync(cfgPath)) {
58664
+ logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
58665
+ process.exit(1);
58666
+ }
58667
+ const src = fs.readFileSync(cfgPath, "utf-8");
58668
+ const compositeSnippet = JSON.stringify(
58669
+ {
58670
+ id: target.id,
58671
+ category: "ai",
58672
+ severity: target.severity,
58673
+ aiSpecific: true,
58674
+ description: target.description,
58675
+ defaultOff: true,
58676
+ ruleIds: target.ruleIds,
58677
+ minMatch: target.minMatch,
58678
+ create: "() => ({})",
58679
+ analyze: "() => []"
58680
+ },
58681
+ null,
58682
+ 2
58683
+ );
58684
+ if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
58685
+ logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
58686
+ return;
58687
+ }
58688
+ const updated = src.replace(
58689
+ /(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
58690
+ (_match, head, tail) => `${head} compositeRules: [
58691
+ ${compositeSnippet}
58692
+ ],
58693
+ ${tail}`
58694
+ );
58695
+ fs.writeFileSync(cfgPath, updated, "utf-8");
58696
+ logger.info(`Composite "${id}" enabled.`);
58697
+ logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
58698
+ });
58699
+ cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
58700
+ const options = command.optsWithGlobals();
58701
+ const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
58702
+ const composites = readComposites(cwd);
58703
+ if (composites.length === 0) {
58704
+ logger.info("No composites registered. Run `slopbrick composite discover` first.");
58705
+ return;
58706
+ }
58707
+ const lines = ["Registered composites:"];
58708
+ for (const e of composites) {
58709
+ lines.push(
58710
+ `
58711
+ ${e.id}
58712
+ ${describeComposite(e.ruleIds, e.minMatch)}
58713
+ members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
58714
+ F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
58715
+ );
58716
+ }
58717
+ logger.info(lines.join("\n"));
58718
+ });
58719
+ }
58720
+
57775
58721
  // src/cli/explain.ts
57776
58722
  var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
57777
58723
  function ruleIdToFilename2(ruleId) {
@@ -57821,152 +58767,7 @@ function formatExplain(result) {
57821
58767
  // src/cli/commands/explain.ts
57822
58768
  init_logger();
57823
58769
  init_builtins();
57824
-
57825
- // src/snippet/data.ts
57826
- var CATEGORY_DIRECTIVES = {
57827
- visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
57828
- logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
57829
- wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
57830
- security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
57831
- perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
57832
- typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
57833
- layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
57834
- component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
57835
- arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
57836
- test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
57837
- };
57838
- var RULE_HINTS = {
57839
- // v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
57840
- // remaining key matches a rule in src/rules/builtins.ts. No orphans.
57841
- // (v0.16.0 previously moved 35 out-of-scope orphan hints to
57842
- // docs/research/backlog-rule-hints.md
57843
- // for future implementers to paste back when the corresponding rules ship.)
57844
- "security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
57845
- "security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
57846
- "security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
57847
- "security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
57848
- "security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
57849
- "security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
57850
- "security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
57851
- "security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
57852
- "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
57853
- "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
57854
- "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
57855
- // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
57856
- "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
57857
- "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
57858
- "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
57859
- "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
57860
- "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
57861
- "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
57862
- "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
57863
- "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
57864
- "component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
57865
- "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
57866
- "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
57867
- "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
57868
- "layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
57869
- "layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
57870
- "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
57871
- "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
57872
- "logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
57873
- "logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
57874
- "logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
57875
- "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
57876
- "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
57877
- "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
57878
- "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
57879
- "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
57880
- "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
57881
- "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
57882
- "dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
57883
- "dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
57884
- "dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
57885
- "dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
57886
- "dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
57887
- "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
57888
- "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
57889
- "typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
57890
- "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
57891
- "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
57892
- "visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
57893
- "visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
57894
- "visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
57895
- "visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
57896
- "visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
57897
- "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
57898
- "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
57899
- "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
57900
- "test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
57901
- "test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
57902
- "product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
57903
- "product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
57904
- // v0.13.0 — AI-specific rules (peer-reviewed signals).
57905
- "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
57906
- "ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
57907
- "ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
57908
- "ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
57909
- "ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
57910
- "ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
57911
- "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
57912
- "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
57913
- "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
57914
- // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
57915
- // reclassified post-v7 calibration in v0.14.5d)
57916
- "ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
57917
- "ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
57918
- "ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
57919
- "ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
57920
- "ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
57921
- "ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
57922
- // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
57923
- "db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
57924
- // v0.17.0 — docs/* rules (markdown drift detection)
57925
- "docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
57926
- "docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
57927
- "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
57928
- // v0.18.9 — Rust-specific rules (tree-sitter-backed)
57929
- "rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
57930
- "rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
57931
- "rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
57932
- "rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
57933
- // v0.19.0 — TypeScript-specific rules
57934
- "ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
57935
- "ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
57936
- "ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
57937
- "ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
57938
- // v0.19.0 — Go-specific rules
57939
- "go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
57940
- "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.",
57941
- // v0.19.0 — Duplication detector
57942
- "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.",
57943
- // v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
57944
- "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.)",
57945
- // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
57946
- "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.)",
57947
- // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
57948
- // v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
57949
- // v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
57950
- "java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
57951
- "java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
57952
- // v0.35.0 — content-based detection (CoCoNUTS-inspired)
57953
- "java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
57954
- // v0.35.1 — Raidar-inspired content-based detection
57955
- "java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
57956
- // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
57957
- "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.)",
57958
- "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.)",
57959
- "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.)",
57960
- "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.)",
57961
- "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.)",
57962
- // v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
57963
- "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.)",
57964
- "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.)",
57965
- "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.)",
57966
- "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.)"
57967
- };
57968
-
57969
- // src/cli/commands/explain.ts
58770
+ init_data();
57970
58771
  function registerExplain(program) {
57971
58772
  program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
57972
58773
  const result = explainRule(ruleId, builtinRules, RULE_HINTS);
@@ -57976,7 +58777,7 @@ function registerExplain(program) {
57976
58777
  }
57977
58778
 
57978
58779
  // src/cli/commands/calibration.ts
57979
- var import_node_path36 = require("path");
58780
+ var import_node_path39 = require("path");
57980
58781
  var import_node_url = require("url");
57981
58782
 
57982
58783
  // src/rules/signal-strength.json
@@ -61569,7 +62370,7 @@ var signal_strength_default2 = {
61569
62370
  };
61570
62371
 
61571
62372
  // src/cli/commands/calibration.ts
61572
- var __dirname = (0, import_node_path36.dirname)((0, import_node_url.fileURLToPath)(__importMetaUrl));
62373
+ var __dirname = (0, import_node_path39.dirname)((0, import_node_url.fileURLToPath)(__importMetaUrl));
61573
62374
  function loadData() {
61574
62375
  return signal_strength_default2;
61575
62376
  }
@@ -61690,13 +62491,13 @@ function registerCalibration(program) {
61690
62491
  }
61691
62492
 
61692
62493
  // src/cli/commands/install.ts
61693
- var import_node_path38 = require("path");
62494
+ var import_node_path41 = require("path");
61694
62495
  init_logger();
61695
62496
  init_git();
61696
62497
 
61697
62498
  // src/cli/installer.ts
61698
- var import_node_fs31 = require("fs");
61699
- var import_node_path37 = require("path");
62499
+ var import_node_fs33 = require("fs");
62500
+ var import_node_path40 = require("path");
61700
62501
  var BEGIN_SENTINEL = "# slopbrick-hook-begin";
61701
62502
  var END_SENTINEL = "# slopbrick-hook-end";
61702
62503
  var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
@@ -61704,14 +62505,14 @@ npx slopbrick --staged
61704
62505
  ${END_SENTINEL}
61705
62506
  `;
61706
62507
  function hookPath(gitRoot) {
61707
- const huskyDir = (0, import_node_path37.join)(gitRoot, ".husky");
61708
- if ((0, import_node_fs31.existsSync)(huskyDir)) {
61709
- return (0, import_node_path37.join)(huskyDir, "pre-commit");
62508
+ const huskyDir = (0, import_node_path40.join)(gitRoot, ".husky");
62509
+ if ((0, import_node_fs33.existsSync)(huskyDir)) {
62510
+ return (0, import_node_path40.join)(huskyDir, "pre-commit");
61710
62511
  }
61711
- return (0, import_node_path37.join)(gitRoot, ".git", "hooks", "pre-commit");
62512
+ return (0, import_node_path40.join)(gitRoot, ".git", "hooks", "pre-commit");
61712
62513
  }
61713
62514
  function readHookContent(path) {
61714
- return (0, import_node_fs31.readFileSync)(path, "utf8");
62515
+ return (0, import_node_fs33.readFileSync)(path, "utf8");
61715
62516
  }
61716
62517
  function sentinelsPresent(content) {
61717
62518
  const lines = content.split(/\r?\n/);
@@ -61733,7 +62534,7 @@ function replaceSentinelBlock(content) {
61733
62534
  }
61734
62535
  function installHook(gitRoot) {
61735
62536
  const path = hookPath(gitRoot);
61736
- if ((0, import_node_fs31.existsSync)(path)) {
62537
+ if ((0, import_node_fs33.existsSync)(path)) {
61737
62538
  const content = readHookContent(path);
61738
62539
  const { begin, end } = sentinelsPresent(content);
61739
62540
  if (begin && end) {
@@ -61745,9 +62546,9 @@ function installHook(gitRoot) {
61745
62546
  exitCode: 0
61746
62547
  };
61747
62548
  }
61748
- (0, import_node_fs31.writeFileSync)(path, replaced.endsWith("\n") ? replaced : `${replaced}
62549
+ (0, import_node_fs33.writeFileSync)(path, replaced.endsWith("\n") ? replaced : `${replaced}
61749
62550
  `);
61750
- (0, import_node_fs31.chmodSync)(path, 493);
62551
+ (0, import_node_fs33.chmodSync)(path, 493);
61751
62552
  return {
61752
62553
  ok: true,
61753
62554
  message: "Replaced pre-commit hook block",
@@ -61765,17 +62566,17 @@ function installHook(gitRoot) {
61765
62566
  }
61766
62567
  const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
61767
62568
  ` : content;
61768
- (0, import_node_fs31.writeFileSync)(path, `${normalized}${SENTINEL_BLOCK}`);
61769
- (0, import_node_fs31.chmodSync)(path, 493);
62569
+ (0, import_node_fs33.writeFileSync)(path, `${normalized}${SENTINEL_BLOCK}`);
62570
+ (0, import_node_fs33.chmodSync)(path, 493);
61770
62571
  return {
61771
62572
  ok: true,
61772
62573
  message: "Installed pre-commit hook",
61773
62574
  exitCode: 0
61774
62575
  };
61775
62576
  }
61776
- (0, import_node_fs31.mkdirSync)((0, import_node_path37.dirname)(path), { recursive: true });
61777
- (0, import_node_fs31.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
61778
- (0, import_node_fs31.chmodSync)(path, 493);
62577
+ (0, import_node_fs33.mkdirSync)((0, import_node_path40.dirname)(path), { recursive: true });
62578
+ (0, import_node_fs33.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
62579
+ (0, import_node_fs33.chmodSync)(path, 493);
61779
62580
  return {
61780
62581
  ok: true,
61781
62582
  message: "Installed pre-commit hook",
@@ -61784,7 +62585,7 @@ function installHook(gitRoot) {
61784
62585
  }
61785
62586
  function uninstallHook(gitRoot) {
61786
62587
  const path = hookPath(gitRoot);
61787
- if (!(0, import_node_fs31.existsSync)(path)) {
62588
+ if (!(0, import_node_fs33.existsSync)(path)) {
61788
62589
  return {
61789
62590
  ok: true,
61790
62591
  message: "Hook not installed",
@@ -61820,7 +62621,7 @@ function uninstallHook(gitRoot) {
61820
62621
  }
61821
62622
  const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
61822
62623
  const result = remaining.join("\n");
61823
- (0, import_node_fs31.writeFileSync)(path, result.endsWith("\n") ? result : `${result}
62624
+ (0, import_node_fs33.writeFileSync)(path, result.endsWith("\n") ? result : `${result}
61824
62625
  `);
61825
62626
  return {
61826
62627
  ok: true,
@@ -61833,7 +62634,7 @@ function uninstallHook(gitRoot) {
61833
62634
  function registerInstall(program) {
61834
62635
  program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
61835
62636
  const options = command.optsWithGlobals();
61836
- const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
62637
+ const cwd = (0, import_node_path41.resolve)(options.workspace ?? process.cwd());
61837
62638
  const root = getGitRoot(cwd);
61838
62639
  if (!root) {
61839
62640
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -61848,13 +62649,13 @@ function registerInstall(program) {
61848
62649
  }
61849
62650
 
61850
62651
  // src/cli/commands/uninstall.ts
61851
- var import_node_path39 = require("path");
62652
+ var import_node_path42 = require("path");
61852
62653
  init_logger();
61853
62654
  init_git();
61854
62655
  function registerUninstall(program) {
61855
62656
  program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
61856
62657
  const options = command.optsWithGlobals();
61857
- const cwd = (0, import_node_path39.resolve)(options.workspace ?? process.cwd());
62658
+ const cwd = (0, import_node_path42.resolve)(options.workspace ?? process.cwd());
61858
62659
  const root = getGitRoot(cwd);
61859
62660
  if (!root) {
61860
62661
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -61877,8 +62678,8 @@ init_config2();
61877
62678
  init_header();
61878
62679
 
61879
62680
  // src/mcp/tools.ts
61880
- var import_node_fs32 = require("fs");
61881
- var import_node_path40 = require("path");
62681
+ var import_node_fs34 = require("fs");
62682
+ var import_node_path43 = require("path");
61882
62683
  init_worker();
61883
62684
  init_patterns();
61884
62685
  init_architecture_score();
@@ -62124,10 +62925,10 @@ async function runSuggest(args, ctx, options = {}) {
62124
62925
  function runCheckConstitution(args, ctx) {
62125
62926
  const path = args.path;
62126
62927
  if (!path) return toolError("Missing required argument: path");
62127
- const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
62928
+ const absPath = (0, import_node_path43.resolve)(ctx.cwd, path);
62128
62929
  let source;
62129
62930
  try {
62130
- source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
62931
+ source = (0, import_node_fs34.readFileSync)(absPath, "utf-8");
62131
62932
  } catch (err) {
62132
62933
  return toolError(
62133
62934
  `Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -62310,7 +63111,7 @@ function errorResponse(id, code, message, data2) {
62310
63111
  return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
62311
63112
  }
62312
63113
  async function runMcpServer(input, output, cwd) {
62313
- return new Promise((resolve42) => {
63114
+ return new Promise((resolve43) => {
62314
63115
  let buffer = "";
62315
63116
  input.setEncoding("utf-8");
62316
63117
  input.on("data", (chunk) => {
@@ -62348,8 +63149,8 @@ async function runMcpServer(input, output, cwd) {
62348
63149
  nlIdx = buffer.indexOf("\n");
62349
63150
  }
62350
63151
  });
62351
- input.on("end", () => resolve42());
62352
- input.on("close", () => resolve42());
63152
+ input.on("end", () => resolve43());
63153
+ input.on("close", () => resolve43());
62353
63154
  });
62354
63155
  }
62355
63156
 
@@ -62372,7 +63173,7 @@ function registerDoctor(program) {
62372
63173
  }
62373
63174
 
62374
63175
  // src/cli/commands/watch.ts
62375
- var import_node_path41 = require("path");
63176
+ var import_node_path44 = require("path");
62376
63177
  init_watch();
62377
63178
  function registerWatch(program, scanAction) {
62378
63179
  program.command("watch").description("re-run scan on every file change. Flags new violations as you write. The LockBrick prevention loop entry.").action(async (_cmdOptions, command) => {
@@ -62381,19 +63182,19 @@ function registerWatch(program, scanAction) {
62381
63182
  ...rawGlobals,
62382
63183
  noIncrease: rawGlobals.increase === false
62383
63184
  };
62384
- const cwd = (0, import_node_path41.resolve)(options.workspace ?? process.cwd());
63185
+ const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
62385
63186
  await scanAction([], options, command);
62386
63187
  await watchProject(options, cwd, []);
62387
63188
  });
62388
63189
  }
62389
63190
 
62390
63191
  // src/cli/commands/lock.ts
62391
- var import_node_path42 = require("path");
63192
+ var import_node_path45 = require("path");
62392
63193
  init_logger();
62393
63194
  function registerLock(program) {
62394
63195
  program.command("lock").description("install a Git pre-commit hook that runs `slopbrick scan --staged` on every commit. The LockBrick prevention loop: block AI-introduced slop from ever reaching the repo.").option("--uninstall", "remove the pre-commit hook instead of installing it").option("--husky", "force-install under .husky/pre-commit (Husky v9). Default auto-detects via .husky/ dir.").option("--workspace <path>", "workspace directory", process.cwd()).action(
62395
63196
  (cmdOptions) => {
62396
- const cwd = (0, import_node_path42.resolve)(cmdOptions.workspace ?? process.cwd());
63197
+ const cwd = (0, import_node_path45.resolve)(cmdOptions.workspace ?? process.cwd());
62397
63198
  if (cmdOptions.uninstall) {
62398
63199
  const result2 = uninstallHook(cwd);
62399
63200
  logger.info(result2.message);
@@ -62414,7 +63215,7 @@ function registerLock(program) {
62414
63215
  }
62415
63216
 
62416
63217
  // src/cli/commands/ci.ts
62417
- var import_node_path43 = require("path");
63218
+ var import_node_path46 = require("path");
62418
63219
  init_logger();
62419
63220
  init_dist();
62420
63221
  function registerCi(program, scanAction) {
@@ -62429,7 +63230,7 @@ function registerCi(program, scanAction) {
62429
63230
  // scan only changed files
62430
63231
  format: cmdOptions.format ?? "json"
62431
63232
  };
62432
- const cwd = (0, import_node_path43.resolve)(options.workspace ?? process.cwd());
63233
+ const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
62433
63234
  await scanAction([], options, command);
62434
63235
  const health = loadHealth(cwd);
62435
63236
  if (!health) {
@@ -62457,12 +63258,12 @@ function registerCi(program, scanAction) {
62457
63258
  }
62458
63259
 
62459
63260
  // src/cli/commands/memory.ts
62460
- var import_node_path44 = require("path");
63261
+ var import_node_path47 = require("path");
62461
63262
  init_logger();
62462
63263
  function registerMemory(program) {
62463
63264
  program.command("memory").description("show or regenerate .slopbrick/structure.md (the agent-readable repository summary) without re-scanning").option("--show", "print the current .slopbrick/structure.md to stdout (default if no flag is passed)").option("--regenerate", "re-render structure.md from the existing inventory.json + constitution.json (no scan)").option("--workspace <path>", "workspace directory", process.cwd()).action(
62464
63265
  async (cmdOptions) => {
62465
- const cwd = (0, import_node_path44.resolve)(cmdOptions.workspace ?? process.cwd());
63266
+ const cwd = (0, import_node_path47.resolve)(cmdOptions.workspace ?? process.cwd());
62466
63267
  const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
62467
63268
  const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
62468
63269
  if (cmdOptions.regenerate) {
@@ -62492,49 +63293,49 @@ function registerMemory(program) {
62492
63293
  }
62493
63294
 
62494
63295
  // src/cli/commands/migrate.ts
62495
- var import_node_path46 = require("path");
63296
+ var import_node_path49 = require("path");
62496
63297
  init_logger();
62497
63298
 
62498
63299
  // src/cli/migrate.ts
62499
- var import_node_fs33 = require("fs");
62500
- var import_node_path45 = require("path");
63300
+ var import_node_fs35 = require("fs");
63301
+ var import_node_path48 = require("path");
62501
63302
  init_logger();
62502
63303
  function planMigration(workspaceDir) {
62503
63304
  const moves = [];
62504
63305
  const rewrites = [];
62505
63306
  const gitignoreEdits = [];
62506
- const oldDir = (0, import_node_path45.join)(workspaceDir, ".slop-audit");
62507
- const newDir = (0, import_node_path45.join)(workspaceDir, ".slopbrick");
62508
- if ((0, import_node_fs33.existsSync)(oldDir)) {
63307
+ const oldDir = (0, import_node_path48.join)(workspaceDir, ".slop-audit");
63308
+ const newDir = (0, import_node_path48.join)(workspaceDir, ".slopbrick");
63309
+ if ((0, import_node_fs35.existsSync)(oldDir)) {
62509
63310
  moves.push({ from: oldDir, to: newDir, kind: "dir" });
62510
63311
  rewrites.push({
62511
- path: (0, import_node_path45.join)(newDir, "inventory.json"),
63312
+ path: (0, import_node_path48.join)(newDir, "inventory.json"),
62512
63313
  field: "version",
62513
63314
  from: '"1"',
62514
63315
  to: '"2"'
62515
63316
  });
62516
63317
  rewrites.push({
62517
- path: (0, import_node_path45.join)(newDir, "constitution.json"),
63318
+ path: (0, import_node_path48.join)(newDir, "constitution.json"),
62518
63319
  field: "version",
62519
63320
  from: '"1"',
62520
63321
  to: '"2"'
62521
63322
  });
62522
63323
  }
62523
- const oldCache = (0, import_node_path45.join)(workspaceDir, ".slop-audit-cache.json");
62524
- const newCache = (0, import_node_path45.join)(workspaceDir, ".slopbrick-cache.json");
62525
- if ((0, import_node_fs33.existsSync)(oldCache)) {
63324
+ const oldCache = (0, import_node_path48.join)(workspaceDir, ".slop-audit-cache.json");
63325
+ const newCache = (0, import_node_path48.join)(workspaceDir, ".slopbrick-cache.json");
63326
+ if ((0, import_node_fs35.existsSync)(oldCache)) {
62526
63327
  moves.push({ from: oldCache, to: newCache, kind: "file" });
62527
63328
  }
62528
63329
  for (const ext of ["mjs", "cjs", "js"]) {
62529
- const oldCfg = (0, import_node_path45.join)(workspaceDir, `slop-audit.config.${ext}`);
62530
- const newCfg = (0, import_node_path45.join)(workspaceDir, `slopbrick.config.${ext}`);
62531
- if ((0, import_node_fs33.existsSync)(oldCfg)) {
63330
+ const oldCfg = (0, import_node_path48.join)(workspaceDir, `slop-audit.config.${ext}`);
63331
+ const newCfg = (0, import_node_path48.join)(workspaceDir, `slopbrick.config.${ext}`);
63332
+ if ((0, import_node_fs35.existsSync)(oldCfg)) {
62532
63333
  moves.push({ from: oldCfg, to: newCfg, kind: "config" });
62533
63334
  }
62534
63335
  }
62535
- const gi = (0, import_node_path45.join)(workspaceDir, ".gitignore");
62536
- if ((0, import_node_fs33.existsSync)(gi)) {
62537
- const content = (0, import_node_fs33.readFileSync)(gi, "utf-8");
63336
+ const gi = (0, import_node_path48.join)(workspaceDir, ".gitignore");
63337
+ if ((0, import_node_fs35.existsSync)(gi)) {
63338
+ const content = (0, import_node_fs35.readFileSync)(gi, "utf-8");
62538
63339
  if (content.includes(".slop-audit/")) {
62539
63340
  gitignoreEdits.push({
62540
63341
  path: gi,
@@ -62553,28 +63354,28 @@ function planMigration(workspaceDir) {
62553
63354
  return { moves, rewrites, gitignoreEdits };
62554
63355
  }
62555
63356
  function isAlreadyMigrated(workspaceDir) {
62556
- return (0, import_node_fs33.existsSync)((0, import_node_path45.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs33.existsSync)((0, import_node_path45.join)(workspaceDir, ".slop-audit"));
63357
+ return (0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slop-audit"));
62557
63358
  }
62558
63359
  function applyMigration(plan, options = {}) {
62559
63360
  if (options.dryRun) return;
62560
63361
  for (const m of plan.moves) {
62561
- (0, import_node_fs33.renameSync)(m.from, m.to);
63362
+ (0, import_node_fs35.renameSync)(m.from, m.to);
62562
63363
  }
62563
63364
  for (const r of plan.rewrites) {
62564
- if (!(0, import_node_fs33.existsSync)(r.path)) continue;
62565
- const content = (0, import_node_fs33.readFileSync)(r.path, "utf-8");
63365
+ if (!(0, import_node_fs35.existsSync)(r.path)) continue;
63366
+ const content = (0, import_node_fs35.readFileSync)(r.path, "utf-8");
62566
63367
  const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
62567
- (0, import_node_fs33.writeFileSync)(r.path, next);
63368
+ (0, import_node_fs35.writeFileSync)(r.path, next);
62568
63369
  }
62569
63370
  for (const g of plan.gitignoreEdits) {
62570
- const content = (0, import_node_fs33.readFileSync)(g.path, "utf-8");
63371
+ const content = (0, import_node_fs35.readFileSync)(g.path, "utf-8");
62571
63372
  const next = content.replaceAll(g.from, g.to);
62572
- (0, import_node_fs33.writeFileSync)(g.path, next);
63373
+ (0, import_node_fs35.writeFileSync)(g.path, next);
62573
63374
  }
62574
63375
  }
62575
63376
  function runMigrate(options) {
62576
63377
  const { workspace, dryRun = false, force = false } = options;
62577
- if (!(0, import_node_fs33.existsSync)(workspace)) {
63378
+ if (!(0, import_node_fs35.existsSync)(workspace)) {
62578
63379
  return {
62579
63380
  ok: false,
62580
63381
  alreadyMigrated: false,
@@ -62584,9 +63385,9 @@ function runMigrate(options) {
62584
63385
  };
62585
63386
  }
62586
63387
  const alreadyMigrated = isAlreadyMigrated(workspace);
62587
- const newDir = (0, import_node_path45.join)(workspace, ".slopbrick");
62588
- const oldDir = (0, import_node_path45.join)(workspace, ".slop-audit");
62589
- if ((0, import_node_fs33.existsSync)(newDir) && (0, import_node_fs33.existsSync)(oldDir) && !force) {
63388
+ const newDir = (0, import_node_path48.join)(workspace, ".slopbrick");
63389
+ const oldDir = (0, import_node_path48.join)(workspace, ".slop-audit");
63390
+ if ((0, import_node_fs35.existsSync)(newDir) && (0, import_node_fs35.existsSync)(oldDir) && !force) {
62590
63391
  return {
62591
63392
  ok: false,
62592
63393
  alreadyMigrated: false,
@@ -62662,7 +63463,7 @@ function registerMigrate(program) {
62662
63463
  (cmdOptions, command) => {
62663
63464
  const globals = command.optsWithGlobals();
62664
63465
  const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
62665
- const cwd = (0, import_node_path46.resolve)(cmdOptions.workspace ?? process.cwd());
63466
+ const cwd = (0, import_node_path49.resolve)(cmdOptions.workspace ?? process.cwd());
62666
63467
  const result = runMigrate({
62667
63468
  workspace: cwd,
62668
63469
  dryRun: cmdOptions.dryRun,
@@ -62766,19 +63567,19 @@ function registerRules(program) {
62766
63567
  }
62767
63568
 
62768
63569
  // src/cli/commands/validate-config.ts
62769
- var import_node_fs34 = require("fs");
62770
- var import_node_path47 = require("path");
63570
+ var import_node_fs36 = require("fs");
63571
+ var import_node_path50 = require("path");
62771
63572
  init_logger();
62772
63573
  init_validation();
62773
63574
  function registerValidateConfig(program) {
62774
63575
  program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
62775
- const path = configPath ? (0, import_node_path47.resolve)(configPath) : (0, import_node_path47.resolve)(process.cwd(), "slopbrick.config.mjs");
62776
- if (!(0, import_node_fs34.existsSync)(path)) {
63576
+ const path = configPath ? (0, import_node_path50.resolve)(configPath) : (0, import_node_path50.resolve)(process.cwd(), "slopbrick.config.mjs");
63577
+ if (!(0, import_node_fs36.existsSync)(path)) {
62777
63578
  logger.error(`Error: config file not found: ${path}`);
62778
63579
  process.exit(2);
62779
63580
  }
62780
63581
  try {
62781
- const mod = (0, import_node_path47.extname)(path) === ".cjs" ? require(path) : await import(path);
63582
+ const mod = (0, import_node_path50.extname)(path) === ".cjs" ? require(path) : await import(path);
62782
63583
  const userConfig = mod.default ?? mod;
62783
63584
  const result = validateConfig(userConfig);
62784
63585
  if (result.errors.length === 0) {
@@ -62998,8 +63799,8 @@ ${formatMarkdown(result.report)}`);
62998
63799
  }
62999
63800
 
63000
63801
  // src/cli/commands/calibrate.ts
63001
- var import_node_fs37 = require("fs");
63002
- var import_node_path51 = require("path");
63802
+ var import_node_fs39 = require("fs");
63803
+ var import_node_path54 = require("path");
63003
63804
  init_logger();
63004
63805
 
63005
63806
  // src/research/providers/openai.ts
@@ -63055,8 +63856,8 @@ function createProvider(config) {
63055
63856
  }
63056
63857
 
63057
63858
  // src/research/generator.ts
63058
- var import_node_fs35 = require("fs");
63059
- var import_node_path48 = require("path");
63859
+ var import_node_fs37 = require("fs");
63860
+ var import_node_path51 = require("path");
63060
63861
 
63061
63862
  // src/research/prompts.ts
63062
63863
  var DEFAULT_PROMPT_TEMPLATES = [
@@ -63119,14 +63920,14 @@ async function generateSamples(options) {
63119
63920
  }
63120
63921
  const samples = [];
63121
63922
  const ext = extForFramework(framework);
63122
- const dir = (0, import_node_path48.join)(outputDir, framework, componentType);
63123
- (0, import_node_fs35.mkdirSync)(dir, { recursive: true });
63923
+ const dir = (0, import_node_path51.join)(outputDir, framework, componentType);
63924
+ (0, import_node_fs37.mkdirSync)(dir, { recursive: true });
63124
63925
  for (let i = 1; i <= count; i += 1) {
63125
63926
  const raw = await provider.generateSample(renderPrompt(template), { temperature });
63126
63927
  const code = extractCodeFromMarkdown(raw);
63127
63928
  const fileName = `sample-${i}${ext}`;
63128
- const filePath = (0, import_node_path48.join)(dir, fileName);
63129
- (0, import_node_fs35.writeFileSync)(filePath, code, "utf8");
63929
+ const filePath = (0, import_node_path51.join)(dir, fileName);
63930
+ (0, import_node_fs37.writeFileSync)(filePath, code, "utf8");
63130
63931
  const sample = {
63131
63932
  filePath,
63132
63933
  framework,
@@ -63137,8 +63938,8 @@ async function generateSamples(options) {
63137
63938
  };
63138
63939
  samples.push(sample);
63139
63940
  }
63140
- const metadataPath = (0, import_node_path48.join)(dir, "metadata.json");
63141
- (0, import_node_fs35.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
63941
+ const metadataPath = (0, import_node_path51.join)(dir, "metadata.json");
63942
+ (0, import_node_fs37.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
63142
63943
  return samples;
63143
63944
  }
63144
63945
 
@@ -63344,46 +64145,46 @@ function slugify(value) {
63344
64145
 
63345
64146
  // src/research/calibrator.ts
63346
64147
  var import_node_child_process2 = require("child_process");
63347
- var import_node_fs36 = require("fs");
63348
- var import_node_path50 = require("path");
64148
+ var import_node_fs38 = require("fs");
64149
+ var import_node_path53 = require("path");
63349
64150
 
63350
64151
  // src/corpus-paths.ts
63351
- var import_node_path49 = require("path");
64152
+ var import_node_path52 = require("path");
63352
64153
  var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
63353
- var POSITIVE_DIR = (0, import_node_path49.join)(CORPUS_ROOT, "positive");
63354
- var NEGATIVE_DIR = (0, import_node_path49.join)(CORPUS_ROOT, "negative");
63355
- var FILELISTS_DIR = (0, import_node_path49.join)(CORPUS_ROOT, "filelists");
64154
+ var POSITIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "positive");
64155
+ var NEGATIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "negative");
64156
+ var FILELISTS_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "filelists");
63356
64157
 
63357
64158
  // src/research/calibrator.ts
63358
64159
  var DEFAULT_POSITIVE = POSITIVE_DIR;
63359
64160
  var DEFAULT_NEGATIVE = NEGATIVE_DIR;
63360
64161
  function buildFileList(dir, extensions) {
63361
- const tmpList = (0, import_node_path50.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
64162
+ const tmpList = (0, import_node_path53.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
63362
64163
  const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
63363
64164
  (0, import_node_child_process2.execFileSync)("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
63364
- const out = (0, import_node_fs36.readFileSync)(tmpList, "utf8");
64165
+ const out = (0, import_node_fs38.readFileSync)(tmpList, "utf8");
63365
64166
  (0, import_node_child_process2.execFileSync)("rm", ["-f", tmpList]);
63366
64167
  return out.trim().split("\n").filter(Boolean);
63367
64168
  }
63368
64169
  function runScan2(fileListPath) {
63369
- const files = (0, import_node_fs36.readFileSync)(fileListPath, "utf8").trim().split("\n").filter(Boolean);
64170
+ const files = (0, import_node_fs38.readFileSync)(fileListPath, "utf8").trim().split("\n").filter(Boolean);
63370
64171
  const CHUNK = 600;
63371
64172
  const ruleFires = /* @__PURE__ */ new Map();
63372
64173
  const uniqueFilesPerRule = /* @__PURE__ */ new Map();
63373
64174
  let fileCount = 0;
63374
- const tmpOut = (0, import_node_path50.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
64175
+ const tmpOut = (0, import_node_path53.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
63375
64176
  for (let i = 0; i < files.length; i += CHUNK) {
63376
64177
  const chunk = files.slice(i, i + CHUNK);
63377
64178
  try {
63378
64179
  (0, import_node_child_process2.execFileSync)(
63379
64180
  "node",
63380
- [(0, import_node_path50.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
64181
+ [(0, import_node_path53.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
63381
64182
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
63382
64183
  );
63383
64184
  } catch {
63384
64185
  }
63385
- if (!(0, import_node_fs36.existsSync)(tmpOut)) continue;
63386
- const report = JSON.parse((0, import_node_fs36.readFileSync)(tmpOut, "utf8"));
64186
+ if (!(0, import_node_fs38.existsSync)(tmpOut)) continue;
64187
+ const report = JSON.parse((0, import_node_fs38.readFileSync)(tmpOut, "utf8"));
63387
64188
  fileCount += report.fileCount;
63388
64189
  for (const issue of report.issues) {
63389
64190
  ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
@@ -63411,16 +64212,16 @@ function classify(posFiles, negFiles) {
63411
64212
  async function calibrate(cwd, options = {}) {
63412
64213
  const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
63413
64214
  const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
63414
- if (!(0, import_node_fs36.existsSync)(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
63415
- if (!(0, import_node_fs36.existsSync)(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
64215
+ if (!(0, import_node_fs38.existsSync)(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
64216
+ if (!(0, import_node_fs38.existsSync)(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
63416
64217
  const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
63417
64218
  const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
63418
64219
  const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
63419
64220
  const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
63420
- const posListPath = (0, import_node_path50.join)("/tmp", `cal-pos-${Date.now()}.txt`);
63421
- const negListPath = (0, import_node_path50.join)("/tmp", `cal-neg-${Date.now()}.txt`);
63422
- (0, import_node_fs36.writeFileSync)(posListPath, posSample.join("\n"));
63423
- (0, import_node_fs36.writeFileSync)(negListPath, negSample.join("\n"));
64221
+ const posListPath = (0, import_node_path53.join)("/tmp", `cal-pos-${Date.now()}.txt`);
64222
+ const negListPath = (0, import_node_path53.join)("/tmp", `cal-neg-${Date.now()}.txt`);
64223
+ (0, import_node_fs38.writeFileSync)(posListPath, posSample.join("\n"));
64224
+ (0, import_node_fs38.writeFileSync)(negListPath, negSample.join("\n"));
63424
64225
  const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
63425
64226
  const builtinRules2 = builtins.builtinRules ?? [];
63426
64227
  const metaById = /* @__PURE__ */ new Map();
@@ -63534,9 +64335,9 @@ function registerCalibrate(program) {
63534
64335
  positiveLimit: cmdOptions.positiveLimit,
63535
64336
  negativeLimit: cmdOptions.negativeLimit
63536
64337
  });
63537
- const outputPath = cmdOptions.output ? (0, import_node_path51.resolve)(cwd, cmdOptions.output) : (0, import_node_path51.resolve)(cwd, "corpus", "calibration-empirical.md");
63538
- (0, import_node_fs37.mkdirSync)((0, import_node_path51.dirname)(outputPath), { recursive: true });
63539
- (0, import_node_fs37.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
64338
+ const outputPath = cmdOptions.output ? (0, import_node_path54.resolve)(cwd, cmdOptions.output) : (0, import_node_path54.resolve)(cwd, "corpus", "calibration-empirical.md");
64339
+ (0, import_node_fs39.mkdirSync)((0, import_node_path54.dirname)(outputPath), { recursive: true });
64340
+ (0, import_node_fs39.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
63540
64341
  logger.info(
63541
64342
  "Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
63542
64343
  );
@@ -63673,13 +64474,13 @@ function registerTrend(program) {
63673
64474
  }
63674
64475
 
63675
64476
  // src/cli/commands/drift.ts
63676
- var import_node_path53 = require("path");
64477
+ var import_node_path56 = require("path");
63677
64478
  init_logger();
63678
64479
  init_scan2();
63679
64480
 
63680
64481
  // src/cli/drift.ts
63681
- var import_node_fs38 = require("fs");
63682
- var import_node_path52 = require("path");
64482
+ var import_node_fs40 = require("fs");
64483
+ var import_node_path55 = require("path");
63683
64484
  init_discover();
63684
64485
  init_patterns();
63685
64486
  init_telemetry();
@@ -63693,7 +64494,7 @@ async function runDrift(cwd, config, options = {}) {
63693
64494
  for (const absPath of limited) {
63694
64495
  let source;
63695
64496
  try {
63696
- source = (0, import_node_fs38.readFileSync)(absPath, "utf-8");
64497
+ source = (0, import_node_fs40.readFileSync)(absPath, "utf-8");
63697
64498
  } catch {
63698
64499
  continue;
63699
64500
  }
@@ -63704,7 +64505,7 @@ async function runDrift(cwd, config, options = {}) {
63704
64505
  byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
63705
64506
  byFile.push({
63706
64507
  file: absPath,
63707
- relPath: (0, import_node_path52.relative)(cwd, absPath),
64508
+ relPath: (0, import_node_path55.relative)(cwd, absPath),
63708
64509
  category: v.category,
63709
64510
  import: v.import,
63710
64511
  declared: v.declared,
@@ -63980,7 +64781,7 @@ function registerDrift(program) {
63980
64781
  const options = command.optsWithGlobals();
63981
64782
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
63982
64783
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
63983
- const cwd = (0, import_node_path53.resolve)(options.workspace ?? process.cwd());
64784
+ const cwd = (0, import_node_path56.resolve)(options.workspace ?? process.cwd());
63984
64785
  if (cmdOptions.temporalSince !== void 0) {
63985
64786
  const temporalSince = cmdOptions.temporalSince.trim();
63986
64787
  if (temporalSince.length === 0) {
@@ -64006,13 +64807,13 @@ function registerDrift(program) {
64006
64807
  }
64007
64808
 
64008
64809
  // src/cli/commands/pr.ts
64009
- var import_node_path55 = require("path");
64810
+ var import_node_path58 = require("path");
64010
64811
  init_logger();
64011
64812
  init_scan2();
64012
64813
 
64013
64814
  // src/cli/pr.ts
64014
- var import_node_fs39 = require("fs");
64015
- var import_node_path54 = require("path");
64815
+ var import_node_fs41 = require("fs");
64816
+ var import_node_path57 = require("path");
64016
64817
  var import_node_child_process3 = require("child_process");
64017
64818
  var import_node_util2 = require("util");
64018
64819
  var import_minimatch3 = require("minimatch");
@@ -64058,10 +64859,10 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
64058
64859
  if (gitFiles.length === 0) return [];
64059
64860
  const sourceFiles = [];
64060
64861
  for (const relOrAbs of gitFiles) {
64061
- const abs = (0, import_node_path54.resolve)(cwd, relOrAbs);
64062
- const ext = (0, import_node_path54.extname)(abs).toLowerCase();
64862
+ const abs = (0, import_node_path57.resolve)(cwd, relOrAbs);
64863
+ const ext = (0, import_node_path57.extname)(abs).toLowerCase();
64063
64864
  if (!PR_EXTENSIONS.has(ext)) continue;
64064
- const rel = (0, import_node_path54.relative)(cwd, abs).split("\\").join("/");
64865
+ const rel = (0, import_node_path57.relative)(cwd, abs).split("\\").join("/");
64065
64866
  if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
64066
64867
  continue;
64067
64868
  }
@@ -64089,7 +64890,7 @@ async function runPrScan(cwd, config, options = {}) {
64089
64890
  for (const absPath of candidates) {
64090
64891
  let source;
64091
64892
  try {
64092
- source = (0, import_node_fs39.readFileSync)(absPath, "utf-8");
64893
+ source = (0, import_node_fs41.readFileSync)(absPath, "utf-8");
64093
64894
  } catch {
64094
64895
  continue;
64095
64896
  }
@@ -64132,7 +64933,7 @@ async function runPrScan(cwd, config, options = {}) {
64132
64933
  totalScore += fileScore;
64133
64934
  files.push({
64134
64935
  file: absPath,
64135
- relPath: (0, import_node_path54.relative)(cwd, absPath).split("\\").join("/"),
64936
+ relPath: (0, import_node_path57.relative)(cwd, absPath).split("\\").join("/"),
64136
64937
  score: fileScore,
64137
64938
  issueCount: issues.length,
64138
64939
  constitutionViolationCount: constitutionViolations.length,
@@ -64264,20 +65065,135 @@ function formatPrMarkdown(result) {
64264
65065
  function prExitCode(result) {
64265
65066
  return result.totalScore > result.threshold ? 1 : 0;
64266
65067
  }
65068
+ function parseUnifiedDiffPaths(diff) {
65069
+ const paths = [];
65070
+ const seen = /* @__PURE__ */ new Set();
65071
+ for (const line of diff.split(/\r?\n/)) {
65072
+ const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
65073
+ if (!m) continue;
65074
+ const path = m[1];
65075
+ if (path === "/dev/null" || path === "dev/null") continue;
65076
+ if (seen.has(path)) continue;
65077
+ seen.add(path);
65078
+ paths.push(path);
65079
+ }
65080
+ return paths;
65081
+ }
65082
+ async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
65083
+ const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
65084
+ const maxFiles = options.maxFiles ?? 500;
65085
+ const { existsSync: existsSync32, readFileSync: readFileSync42 } = await import("fs");
65086
+ const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
65087
+ if (!existsSync32(diffPath)) {
65088
+ throw new Error(`Diff file not found: ${diffPath}`);
65089
+ }
65090
+ const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
65091
+ const diff = readFileSync42(diffAbs, "utf-8");
65092
+ const changedPaths = parseUnifiedDiffPaths(diff);
65093
+ if (changedPaths.length === 0) {
65094
+ return emptyPrResult(threshold);
65095
+ }
65096
+ return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
65097
+ }
65098
+ async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
65099
+ const { existsSync: existsSync32, statSync: statSync8 } = await import("fs");
65100
+ const { resolve: resolvePath } = await import("path");
65101
+ const { readFile: readFile4 } = await import("fs/promises");
65102
+ const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
65103
+ const limited = paths.slice(0, maxFiles);
65104
+ const files = [];
65105
+ let totalScore = 0;
65106
+ const byCategory = {};
65107
+ const bySeverity = { low: 0, medium: 0, high: 0 };
65108
+ let constitutionViolations = 0;
65109
+ for (const rel of limited) {
65110
+ const abs = resolvePath(cwd, rel);
65111
+ if (!existsSync32(abs) || !statSync8(abs).isFile()) continue;
65112
+ let source;
65113
+ try {
65114
+ source = await readFile4(abs, "utf-8");
65115
+ } catch {
65116
+ continue;
65117
+ }
65118
+ let result;
65119
+ try {
65120
+ result = await scanFile2(abs, config, void 0, cwd);
65121
+ } catch {
65122
+ continue;
65123
+ }
65124
+ if (result.parseError) continue;
65125
+ const issues = result.issues;
65126
+ let slopPoints = 0;
65127
+ const issueEntries = [];
65128
+ for (const issue of issues) {
65129
+ slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
65130
+ byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
65131
+ bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
65132
+ issueEntries.push({
65133
+ ruleId: issue.ruleId,
65134
+ severity: issue.severity,
65135
+ line: issue.line,
65136
+ category: issue.category,
65137
+ message: issue.message
65138
+ });
65139
+ }
65140
+ totalScore += slopPoints;
65141
+ files.push({
65142
+ file: abs,
65143
+ relPath: rel,
65144
+ score: slopPoints,
65145
+ issueCount: issueEntries.length,
65146
+ constitutionViolationCount: 0,
65147
+ slopPoints,
65148
+ issues: issueEntries,
65149
+ constitutionViolations: []
65150
+ });
65151
+ }
65152
+ return {
65153
+ base: "diff",
65154
+ head: "diff",
65155
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65156
+ passed: totalScore <= threshold,
65157
+ threshold,
65158
+ totalScore,
65159
+ filesChanged: files.length,
65160
+ byCategory,
65161
+ bySeverity,
65162
+ files
65163
+ };
65164
+ }
65165
+ function emptyPrResult(threshold) {
65166
+ return {
65167
+ base: "diff",
65168
+ head: "diff",
65169
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65170
+ passed: true,
65171
+ threshold,
65172
+ totalScore: 0,
65173
+ filesChanged: 0,
65174
+ byCategory: {},
65175
+ bySeverity: { low: 0, medium: 0, high: 0 },
65176
+ files: []
65177
+ };
65178
+ }
64267
65179
 
64268
65180
  // src/cli/commands/pr.ts
64269
65181
  function registerPr(program) {
64270
65182
  program.command("pr").description(
64271
65183
  "Score the slop cost of the current PR. Compares --base (default: main) to --head (default: HEAD), scans only changed source files, returns a single weighted score. Exits 1 when score > --threshold (default 20)."
64272
- ).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).action(
65184
+ ).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).option("--diff <file>", "unified-diff file (overrides --base/--head; meant for CI/Action use)").action(
64273
65185
  async (cmdOptions, command) => {
64274
65186
  try {
64275
65187
  const options = command.optsWithGlobals();
64276
- const cwd = (0, import_node_path55.resolve)(options.workspace ?? process.cwd());
65188
+ const cwd = (0, import_node_path58.resolve)(options.workspace ?? process.cwd());
64277
65189
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64278
65190
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64279
65191
  const { config } = await runScan({ ...options, workspace: cwd });
64280
- const result = await runPrScan(cwd, config, {
65192
+ const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
65193
+ format,
65194
+ threshold: cmdOptions.threshold,
65195
+ maxFiles: cmdOptions.maxFiles
65196
+ }) : await runPrScan(cwd, config, {
64281
65197
  base: cmdOptions.base,
64282
65198
  head: cmdOptions.head,
64283
65199
  format,
@@ -64295,7 +65211,7 @@ function registerPr(program) {
64295
65211
  }
64296
65212
 
64297
65213
  // src/cli/commands/security.ts
64298
- var import_node_path56 = require("path");
65214
+ var import_node_path59 = require("path");
64299
65215
  init_logger();
64300
65216
  init_scan2();
64301
65217
  init_ai_security_risk();
@@ -64308,7 +65224,7 @@ function registerSecurity(program) {
64308
65224
  const options = command.optsWithGlobals();
64309
65225
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64310
65226
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64311
- const cwd = (0, import_node_path56.resolve)(options.workspace ?? process.cwd());
65227
+ const cwd = (0, import_node_path59.resolve)(options.workspace ?? process.cwd());
64312
65228
  const { report } = await runScan({ ...options, workspace: cwd });
64313
65229
  const securityIssues = report.issues.filter((i) => i.category === "security");
64314
65230
  const { risk, findings } = computeAiSecurityRisk(securityIssues);
@@ -64354,12 +65270,12 @@ function registerSecurity(program) {
64354
65270
  }
64355
65271
 
64356
65272
  // src/cli/commands/test.ts
64357
- var import_node_path58 = require("path");
65273
+ var import_node_path61 = require("path");
64358
65274
  init_logger();
64359
65275
  init_scan2();
64360
65276
 
64361
65277
  // src/cli/test.ts
64362
- var import_node_path57 = require("path");
65278
+ var import_node_path60 = require("path");
64363
65279
  init_scan2();
64364
65280
  init_test_quality();
64365
65281
  init_logger();
@@ -64430,7 +65346,7 @@ function formatTestReport(result, opts = {}) {
64430
65346
  const sortedFiles = [...byFile.keys()].sort();
64431
65347
  for (const file of sortedFiles) {
64432
65348
  const issues = byFile.get(file) ?? [];
64433
- const rel = file.startsWith((0, import_node_path57.resolve)(process.cwd())) ? file : file;
65349
+ const rel = file.startsWith((0, import_node_path60.resolve)(process.cwd())) ? file : file;
64434
65350
  lines.push("");
64435
65351
  lines.push(` ${rel}`);
64436
65352
  for (const issue of issues.slice(0, 20)) {
@@ -64459,7 +65375,7 @@ function registerTest(program) {
64459
65375
  const options = command.optsWithGlobals();
64460
65376
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64461
65377
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64462
- const cwd = (0, import_node_path58.resolve)(options.workspace ?? process.cwd());
65378
+ const cwd = (0, import_node_path61.resolve)(options.workspace ?? process.cwd());
64463
65379
  const { config } = await runScan({ ...options, workspace: cwd });
64464
65380
  const { result } = await runTestScan(cwd, config, { strict: options.strict });
64465
65381
  logger.info(formatTestReport(result, { json: format === "json" }));
@@ -64473,7 +65389,7 @@ function registerTest(program) {
64473
65389
  }
64474
65390
 
64475
65391
  // src/cli/commands/architecture.ts
64476
- var import_node_path59 = require("path");
65392
+ var import_node_path62 = require("path");
64477
65393
  init_logger();
64478
65394
  init_scan2();
64479
65395
  init_architecture_score();
@@ -64486,7 +65402,7 @@ function registerArchitecture(program) {
64486
65402
  const options = command.optsWithGlobals();
64487
65403
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64488
65404
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64489
- const cwd = (0, import_node_path59.resolve)(options.workspace ?? process.cwd());
65405
+ const cwd = (0, import_node_path62.resolve)(options.workspace ?? process.cwd());
64490
65406
  const { config } = await runScan({ ...options, workspace: cwd });
64491
65407
  const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
64492
65408
  const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
@@ -64501,13 +65417,13 @@ function registerArchitecture(program) {
64501
65417
  }
64502
65418
 
64503
65419
  // src/cli/commands/business-logic.ts
64504
- var import_node_path61 = require("path");
65420
+ var import_node_path64 = require("path");
64505
65421
  init_logger();
64506
65422
  init_scan2();
64507
65423
 
64508
65424
  // src/cli/business-logic.ts
64509
- var import_node_fs40 = require("fs");
64510
- var import_node_path60 = require("path");
65425
+ var import_node_fs42 = require("fs");
65426
+ var import_node_path63 = require("path");
64511
65427
  init_discover();
64512
65428
  init_business_logic();
64513
65429
  async function runBusinessLogicScan(cwd, config, options = {}) {
@@ -64519,7 +65435,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
64519
65435
  for (const absPath of limited) {
64520
65436
  let source;
64521
65437
  try {
64522
- source = (0, import_node_fs40.readFileSync)(absPath, "utf-8");
65438
+ source = (0, import_node_fs42.readFileSync)(absPath, "utf-8");
64523
65439
  } catch {
64524
65440
  continue;
64525
65441
  }
@@ -64528,7 +65444,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
64528
65444
  for (const issue of fileIssues) {
64529
65445
  issues.push({
64530
65446
  ...issue,
64531
- filePath: (0, import_node_path60.relative)(cwd, absPath) || absPath
65447
+ filePath: (0, import_node_path63.relative)(cwd, absPath) || absPath
64532
65448
  });
64533
65449
  }
64534
65450
  }
@@ -64631,7 +65547,7 @@ function registerBusinessLogic(program) {
64631
65547
  const options = command.optsWithGlobals();
64632
65548
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64633
65549
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64634
- const cwd = (0, import_node_path61.resolve)(options.workspace ?? process.cwd());
65550
+ const cwd = (0, import_node_path64.resolve)(options.workspace ?? process.cwd());
64635
65551
  const { config } = await runScan({ ...options, workspace: cwd });
64636
65552
  const result = await runBusinessLogicScan(cwd, config, {
64637
65553
  maxFiles: cmdOptions.maxFiles
@@ -64647,7 +65563,7 @@ function registerBusinessLogic(program) {
64647
65563
  }
64648
65564
 
64649
65565
  // src/cli/commands/maintenance-cost.ts
64650
- var import_node_path62 = require("path");
65566
+ var import_node_path65 = require("path");
64651
65567
  init_logger();
64652
65568
  init_scan2();
64653
65569
 
@@ -64773,7 +65689,7 @@ function registerMaintenanceCost(program) {
64773
65689
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64774
65690
  const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
64775
65691
  const strict = options.strict ?? cmdOptions.strict ?? false;
64776
- const cwd = (0, import_node_path62.resolve)(options.workspace ?? process.cwd());
65692
+ const cwd = (0, import_node_path65.resolve)(options.workspace ?? process.cwd());
64777
65693
  const { config } = await runScan({ ...options, workspace: cwd });
64778
65694
  const result = await runMaintenanceCostScan(cwd, config, {
64779
65695
  maxFiles: cmdOptions.maxFiles,
@@ -64790,7 +65706,7 @@ function registerMaintenanceCost(program) {
64790
65706
  }
64791
65707
 
64792
65708
  // src/cli/commands/docs.ts
64793
- var import_node_path63 = require("path");
65709
+ var import_node_path66 = require("path");
64794
65710
  init_logger();
64795
65711
  init_scan2();
64796
65712
 
@@ -64917,7 +65833,7 @@ function registerDocs(program) {
64917
65833
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64918
65834
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64919
65835
  const strict = options.strict ?? cmdOptions.strict ?? false;
64920
- const cwd = (0, import_node_path63.resolve)(options.workspace ?? process.cwd());
65836
+ const cwd = (0, import_node_path66.resolve)(options.workspace ?? process.cwd());
64921
65837
  const { config } = await runScan({ ...options, workspace: cwd });
64922
65838
  const result = await runDocsScan(cwd, config, {
64923
65839
  maxDocFiles: cmdOptions.maxFiles,
@@ -64939,7 +65855,7 @@ function registerDocs(program) {
64939
65855
  }
64940
65856
 
64941
65857
  // src/cli/commands/db.ts
64942
- var import_node_path64 = require("path");
65858
+ var import_node_path67 = require("path");
64943
65859
  init_logger();
64944
65860
  init_scan2();
64945
65861
 
@@ -65060,7 +65976,7 @@ function registerDb(program) {
65060
65976
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
65061
65977
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
65062
65978
  const strict = options.strict ?? cmdOptions.strict ?? false;
65063
- const cwd = (0, import_node_path64.resolve)(options.workspace ?? process.cwd());
65979
+ const cwd = (0, import_node_path67.resolve)(options.workspace ?? process.cwd());
65064
65980
  const { config } = await runScan({ ...options, workspace: cwd });
65065
65981
  const result = await runDbScan(cwd, config, {
65066
65982
  maxFiles: cmdOptions.maxFiles,
@@ -65082,13 +65998,13 @@ function registerDb(program) {
65082
65998
  }
65083
65999
 
65084
66000
  // src/cli/commands/patterns.ts
65085
- var import_node_path66 = require("path");
66001
+ var import_node_path69 = require("path");
65086
66002
  init_logger();
65087
66003
  init_scan2();
65088
66004
 
65089
66005
  // src/engine/patterns.ts
65090
- var import_node_fs41 = require("fs");
65091
- var import_node_path65 = require("path");
66006
+ var import_node_fs43 = require("fs");
66007
+ var import_node_path68 = require("path");
65092
66008
  init_patterns();
65093
66009
  init_discover();
65094
66010
  var PATTERN_CATEGORIES = [
@@ -65123,7 +66039,7 @@ var TOAST_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Toast|Notification|Snackbar|Alert|B
65123
66039
  var CARD_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Card|Tile|Chip|Badge)$/;
65124
66040
  var API_PATH_RE2 = /(?:^|\/)(?:lib\/api|services|api-client|clients)\//;
65125
66041
  function baseName(filePath) {
65126
- return (0, import_node_path65.basename)(filePath).replace(/\.(tsx|ts|jsx|js|vue|svelte|astro)$/i, "");
66042
+ return (0, import_node_path68.basename)(filePath).replace(/\.(tsx|ts|jsx|js|vue|svelte|astro)$/i, "");
65127
66043
  }
65128
66044
  function makeStats(patterns) {
65129
66045
  const seen = /* @__PURE__ */ new Set();
@@ -65182,7 +66098,7 @@ function detectApiFromFiles(files, cwd) {
65182
66098
  const out = [];
65183
66099
  for (const f of files) {
65184
66100
  if (API_PATH_RE2.test(f)) {
65185
- out.push((0, import_node_path65.relative)(cwd, f).split("\\").join("/"));
66101
+ out.push((0, import_node_path68.relative)(cwd, f).split("\\").join("/"));
65186
66102
  }
65187
66103
  }
65188
66104
  return out;
@@ -65192,7 +66108,7 @@ function detectFormsFromFiles(files) {
65192
66108
  for (const f of files) {
65193
66109
  let source;
65194
66110
  try {
65195
- source = (0, import_node_fs41.readFileSync)(f, "utf-8");
66111
+ source = (0, import_node_fs43.readFileSync)(f, "utf-8");
65196
66112
  } catch {
65197
66113
  continue;
65198
66114
  }
@@ -65413,7 +66329,7 @@ function registerPatterns(program) {
65413
66329
  const options = command.optsWithGlobals();
65414
66330
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
65415
66331
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
65416
- const cwd = (0, import_node_path66.resolve)(options.workspace ?? process.cwd());
66332
+ const cwd = (0, import_node_path69.resolve)(options.workspace ?? process.cwd());
65417
66333
  const { config } = await runScan({ ...options, workspace: cwd });
65418
66334
  const result = await runPatternsScan(cwd, config, {
65419
66335
  maxFiles: cmdOptions.maxFiles,
@@ -65430,8 +66346,8 @@ function registerPatterns(program) {
65430
66346
  }
65431
66347
 
65432
66348
  // src/cli/commands/research.ts
65433
- var import_node_fs42 = require("fs");
65434
- var import_node_path67 = require("path");
66349
+ var import_node_fs44 = require("fs");
66350
+ var import_node_path70 = require("path");
65435
66351
  init_logger();
65436
66352
  init_config2();
65437
66353
  function registerResearch(program) {
@@ -65448,24 +66364,24 @@ function registerResearch(program) {
65448
66364
  framework: cmdOptions.framework,
65449
66365
  componentType: cmdOptions.componentType,
65450
66366
  provider,
65451
- outputDir: (0, import_node_path67.resolve)(cmdOptions.outputDir),
66367
+ outputDir: (0, import_node_path70.resolve)(cmdOptions.outputDir),
65452
66368
  temperature: cmdOptions.temperature
65453
66369
  });
65454
66370
  logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
65455
66371
  });
65456
66372
  research.command("analyze").description("analyze generated samples and report coverage").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "analysis output path", ".slopbrick/flywheel/analysis.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").action(async (cmdOptions) => {
65457
66373
  try {
65458
- const metadataPath = (0, import_node_path67.resolve)(cmdOptions.inputDir, "metadata.json");
65459
- if (!(0, import_node_fs42.existsSync)(metadataPath)) {
66374
+ const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
66375
+ if (!(0, import_node_fs44.existsSync)(metadataPath)) {
65460
66376
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
65461
66377
  process.exit(2);
65462
66378
  }
65463
- const samples = JSON.parse((0, import_node_fs42.readFileSync)(metadataPath, "utf8"));
66379
+ const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
65464
66380
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
65465
66381
  const analysis = await analyzeSamples(samples, config);
65466
- const outputPath = (0, import_node_path67.resolve)(cmdOptions.output);
65467
- (0, import_node_fs42.mkdirSync)((0, import_node_path67.dirname)(outputPath), { recursive: true });
65468
- (0, import_node_fs42.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
66382
+ const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
66383
+ (0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
66384
+ (0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
65469
66385
  logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
65470
66386
  logger.info(`Wrote analysis to ${outputPath}`);
65471
66387
  } catch (error) {
@@ -65475,12 +66391,12 @@ function registerResearch(program) {
65475
66391
  });
65476
66392
  research.command("candidates").description("extract patterns from generated samples and emit candidate rules").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "output path", ".slopbrick/flywheel/rule-candidates.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").option("--min-frequency <n>", "minimum cluster frequency", parseCount, 2).option("--include-covered", "include samples already covered by AI-specific rules").action(async (cmdOptions) => {
65477
66393
  try {
65478
- const metadataPath = (0, import_node_path67.resolve)(cmdOptions.inputDir, "metadata.json");
65479
- if (!(0, import_node_fs42.existsSync)(metadataPath)) {
66394
+ const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
66395
+ if (!(0, import_node_fs44.existsSync)(metadataPath)) {
65480
66396
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
65481
66397
  process.exit(2);
65482
66398
  }
65483
- const samples = JSON.parse((0, import_node_fs42.readFileSync)(metadataPath, "utf8"));
66399
+ const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
65484
66400
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
65485
66401
  const analysis = await analyzeSamples(samples, config);
65486
66402
  const extraction = extractAndCluster(analysis.samples, {
@@ -65490,8 +66406,8 @@ function registerResearch(program) {
65490
66406
  const candidates = clustersToCandidates(extraction.clusters, {
65491
66407
  minFrequency: cmdOptions.minFrequency
65492
66408
  });
65493
- const outputPath = (0, import_node_path67.resolve)(cmdOptions.output);
65494
- (0, import_node_fs42.mkdirSync)((0, import_node_path67.dirname)(outputPath), { recursive: true });
66409
+ const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
66410
+ (0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
65495
66411
  const payload = {
65496
66412
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65497
66413
  sampleCount: analysis.summary.total,
@@ -65499,7 +66415,7 @@ function registerResearch(program) {
65499
66415
  fingerprintCount: extraction.total,
65500
66416
  candidates
65501
66417
  };
65502
- (0, import_node_fs42.writeFileSync)(outputPath, JSON.stringify(payload, null, 2), "utf8");
66418
+ (0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(payload, null, 2), "utf8");
65503
66419
  logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
65504
66420
  logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
65505
66421
  } catch (error) {
@@ -65510,8 +66426,8 @@ function registerResearch(program) {
65510
66426
  }
65511
66427
 
65512
66428
  // src/cli/commands/init.ts
65513
- var import_node_fs43 = require("fs");
65514
- var import_node_path69 = require("path");
66429
+ var import_node_fs45 = require("fs");
66430
+ var import_node_path72 = require("path");
65515
66431
  init_logger();
65516
66432
  init_builtins();
65517
66433
  init_scan2();
@@ -65521,145 +66437,9 @@ init_git();
65521
66437
  init_cache();
65522
66438
 
65523
66439
  // src/snippet/targets.ts
65524
- var import_node_path68 = require("path");
65525
-
65526
- // src/snippet/render.ts
65527
- function aiSpecificRules(rules) {
65528
- return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
65529
- }
65530
- function categorizeRules(rules) {
65531
- const byCategory = /* @__PURE__ */ new Map();
65532
- for (const r of rules) {
65533
- const list = byCategory.get(r.category) ?? [];
65534
- list.push(r);
65535
- byCategory.set(r.category, list);
65536
- }
65537
- for (const list of byCategory.values()) {
65538
- list.sort((a, b) => a.id.localeCompare(b.id));
65539
- }
65540
- return byCategory;
65541
- }
65542
- function baseContent(rules, toolLabel, intro) {
65543
- const lines = [];
65544
- lines.push("# " + toolLabel);
65545
- lines.push("");
65546
- lines.push(intro);
65547
- lines.push("");
65548
- const ai = aiSpecificRules(rules);
65549
- const byCategory = categorizeRules(ai);
65550
- lines.push("## Category-level directives");
65551
- lines.push("");
65552
- for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
65553
- lines.push("- **" + category + "**: " + directive);
65554
- }
65555
- lines.push("");
65556
- lines.push("## Per-rule directives");
65557
- lines.push("");
65558
- for (const [category, list] of byCategory) {
65559
- lines.push("### " + category);
65560
- lines.push("");
65561
- for (const rule of list) {
65562
- const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
65563
- lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
65564
- }
65565
- lines.push("");
65566
- }
65567
- return lines.join("\n");
65568
- }
65569
-
65570
- // src/snippet/generators.ts
65571
- function generateCursorSnippet(rules) {
65572
- const lines = [];
65573
- lines.push("# slopbrick cursor rules");
65574
- lines.push("");
65575
- lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
65576
- lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
65577
- lines.push("");
65578
- lines.push("## Category-level directives");
65579
- lines.push("");
65580
- for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
65581
- lines.push("- **" + category + "**: " + directive);
65582
- }
65583
- lines.push("");
65584
- lines.push("## Per-rule directives");
65585
- lines.push("");
65586
- const ai = aiSpecificRules(rules);
65587
- const byCategory = categorizeRules(ai);
65588
- for (const [category, list] of byCategory) {
65589
- lines.push("### " + category);
65590
- lines.push("");
65591
- for (const rule of list) {
65592
- const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
65593
- lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
65594
- }
65595
- lines.push("");
65596
- }
65597
- lines.push("## Verification");
65598
- lines.push("");
65599
- lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
65600
- lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
65601
- lines.push("");
65602
- return lines.join("\n");
65603
- }
65604
- function generateAgentsMdSnippet(rules) {
65605
- return baseContent(
65606
- rules,
65607
- "AGENTS.md",
65608
- "Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
65609
- ) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
65610
- }
65611
- function generateClaudeMdSnippet(rules) {
65612
- return baseContent(
65613
- rules,
65614
- "CLAUDE.md",
65615
- "Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
65616
- ) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n";
65617
- }
65618
- function generateAiderSnippet(rules) {
65619
- return baseContent(
65620
- rules,
65621
- "CONVENTIONS.md",
65622
- "Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
65623
- );
65624
- }
65625
- function generateWindsurfSnippet(rules) {
65626
- return baseContent(
65627
- rules,
65628
- ".windsurfrules",
65629
- "Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
65630
- );
65631
- }
65632
- function generateClineRules(rules) {
65633
- return baseContent(
65634
- rules,
65635
- "AGENTS.md",
65636
- "Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
65637
- );
65638
- }
65639
- function generateCursorrulesLegacySnippet(rules) {
65640
- return baseContent(
65641
- rules,
65642
- ".cursorrules",
65643
- "Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
65644
- );
65645
- }
65646
- function generateGeminiSnippet(rules) {
65647
- return baseContent(
65648
- rules,
65649
- "GEMINI.md",
65650
- "Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
65651
- );
65652
- }
65653
- function generateCopilotSnippet(rules) {
65654
- return baseContent(
65655
- rules,
65656
- "copilot-instructions.md",
65657
- "Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
65658
- );
65659
- }
65660
-
65661
- // src/snippet/targets.ts
65662
- var SNIPPET_TARGETS = [
66440
+ var import_node_path71 = require("path");
66441
+ init_generators();
66442
+ var SNIPPET_TARGETS2 = [
65663
66443
  {
65664
66444
  flag: "cursor",
65665
66445
  cliName: "--cursor",
@@ -65752,7 +66532,7 @@ var SNIPPET_TARGETS = [
65752
66532
  }
65753
66533
  ];
65754
66534
  function resolveTargetPath(target) {
65755
- return target.isFolder ? (0, import_node_path68.join)(target.path, target.filename) : target.path;
66535
+ return target.isFolder ? (0, import_node_path71.join)(target.path, target.filename) : target.path;
65756
66536
  }
65757
66537
  function renderMatrix() {
65758
66538
  const lines = [];
@@ -65760,7 +66540,7 @@ function renderMatrix() {
65760
66540
  lines.push("");
65761
66541
  lines.push("| Flag | File | Agent |");
65762
66542
  lines.push("|------|------|-------|");
65763
- for (const t of SNIPPET_TARGETS) {
66543
+ for (const t of SNIPPET_TARGETS2) {
65764
66544
  lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
65765
66545
  }
65766
66546
  return lines.join("\n");
@@ -65774,13 +66554,13 @@ function registerInit(program) {
65774
66554
  logger.info(renderMatrix());
65775
66555
  process.exit(0);
65776
66556
  }
65777
- const cwd = (0, import_node_path69.resolve)(options.workspace ?? process.cwd());
65778
- const configPath = (0, import_node_path69.join)(cwd, "slopbrick.config.mjs");
66557
+ const cwd = (0, import_node_path72.resolve)(options.workspace ?? process.cwd());
66558
+ const configPath = (0, import_node_path72.join)(cwd, "slopbrick.config.mjs");
65779
66559
  const detected = detectStack(cwd);
65780
66560
  const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
65781
66561
  const proposed = serializeConfig(fallbackConfig);
65782
- if ((0, import_node_fs43.existsSync)(configPath) && !cmdOptions.yes) {
65783
- const current = (0, import_node_fs43.readFileSync)(configPath, "utf8");
66562
+ if ((0, import_node_fs45.existsSync)(configPath) && !cmdOptions.yes) {
66563
+ const current = (0, import_node_fs45.readFileSync)(configPath, "utf8");
65784
66564
  logger.error(`A config file already exists at ${configPath}.`);
65785
66565
  logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
65786
66566
  logger.error("");
@@ -65801,7 +66581,7 @@ function registerInit(program) {
65801
66581
  config = buildInitConfig(detected, answers);
65802
66582
  usedWizard = true;
65803
66583
  }
65804
- (0, import_node_fs43.writeFileSync)(configPath, serializeConfig(config));
66584
+ (0, import_node_fs45.writeFileSync)(configPath, serializeConfig(config));
65805
66585
  appendGitignore(cwd);
65806
66586
  const refresh = await refreshRegistrySnapshot(cwd);
65807
66587
  if (!refresh.ok) {
@@ -65825,27 +66605,27 @@ function registerInit(program) {
65825
66605
  }
65826
66606
  logger.info(refresh.message);
65827
66607
  }
65828
- const targetsToWrite = SNIPPET_TARGETS.filter((t) => {
66608
+ const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
65829
66609
  if (cmdOptions.all) return true;
65830
66610
  const opts = cmdOptions;
65831
66611
  return Boolean(opts[t.flag]);
65832
66612
  });
65833
66613
  for (const target of targetsToWrite) {
65834
- const snippetPath = (0, import_node_path69.join)(cwd, resolveTargetPath(target));
65835
- (0, import_node_fs43.mkdirSync)((0, import_node_path69.dirname)(snippetPath), { recursive: true });
66614
+ const snippetPath = (0, import_node_path72.join)(cwd, resolveTargetPath(target));
66615
+ (0, import_node_fs45.mkdirSync)((0, import_node_path72.dirname)(snippetPath), { recursive: true });
65836
66616
  const generated = target.generator(builtinRules);
65837
- if (!target.isFolder && (0, import_node_fs43.existsSync)(snippetPath)) {
65838
- const existing = (0, import_node_fs43.readFileSync)(snippetPath, "utf8");
66617
+ if (!target.isFolder && (0, import_node_fs45.existsSync)(snippetPath)) {
66618
+ const existing = (0, import_node_fs45.readFileSync)(snippetPath, "utf8");
65839
66619
  if (existing.includes("<!-- slopbrick:begin -->")) {
65840
66620
  const updated = existing.replace(
65841
66621
  /<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
65842
66622
  "<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
65843
66623
  );
65844
- (0, import_node_fs43.writeFileSync)(snippetPath, updated, "utf8");
66624
+ (0, import_node_fs45.writeFileSync)(snippetPath, updated, "utf8");
65845
66625
  if (!options.quiet) logger.info(`Updated ${snippetPath}`);
65846
66626
  continue;
65847
66627
  }
65848
- (0, import_node_fs43.writeFileSync)(
66628
+ (0, import_node_fs45.writeFileSync)(
65849
66629
  snippetPath,
65850
66630
  existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
65851
66631
  "utf8"
@@ -65853,7 +66633,7 @@ function registerInit(program) {
65853
66633
  if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
65854
66634
  continue;
65855
66635
  }
65856
- (0, import_node_fs43.writeFileSync)(snippetPath, generated, "utf8");
66636
+ (0, import_node_fs45.writeFileSync)(snippetPath, generated, "utf8");
65857
66637
  if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
65858
66638
  }
65859
66639
  if (options.baseline) {
@@ -65871,14 +66651,14 @@ function registerInit(program) {
65871
66651
  }
65872
66652
 
65873
66653
  // src/cli/commands/flywheel.ts
65874
- var import_node_fs45 = require("fs");
65875
- var import_node_path71 = require("path");
66654
+ var import_node_fs47 = require("fs");
66655
+ var import_node_path74 = require("path");
65876
66656
  init_logger();
65877
66657
  init_telemetry();
65878
66658
 
65879
66659
  // src/report/flywheel.ts
65880
- var import_node_fs44 = require("fs");
65881
- var import_node_path70 = require("path");
66660
+ var import_node_fs46 = require("fs");
66661
+ var import_node_path73 = require("path");
65882
66662
  function average(values) {
65883
66663
  if (values.length === 0) return 0;
65884
66664
  return values.reduce((a, b) => a + b, 0) / values.length;
@@ -65968,7 +66748,7 @@ function formatFlywheel(summary, options = {}) {
65968
66748
  function registerFlywheel(program) {
65969
66749
  program.command("flywheel").description("summarize aggregated scan telemetry").option("--format <pretty|json>", "output format", "pretty").option("--export <path>", "write summary as JSON to <path>").action(async (cmdOptions, command) => {
65970
66750
  const options = command.optsWithGlobals();
65971
- const cwd = (0, import_node_path71.resolve)(options.workspace ?? process.cwd());
66751
+ const cwd = (0, import_node_path74.resolve)(options.workspace ?? process.cwd());
65972
66752
  const payloads = readTelemetry(cwd);
65973
66753
  if (payloads.length === 0) {
65974
66754
  logger.info("No flywheel telemetry found. Run a scan first.");
@@ -65976,9 +66756,9 @@ function registerFlywheel(program) {
65976
66756
  }
65977
66757
  const summary = summarizeTelemetry(payloads);
65978
66758
  if (cmdOptions.export) {
65979
- const exportPath = (0, import_node_path71.resolve)(cmdOptions.export);
65980
- (0, import_node_fs45.mkdirSync)((0, import_node_path71.dirname)(exportPath), { recursive: true });
65981
- (0, import_node_fs45.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
66759
+ const exportPath = (0, import_node_path74.resolve)(cmdOptions.export);
66760
+ (0, import_node_fs47.mkdirSync)((0, import_node_path74.dirname)(exportPath), { recursive: true });
66761
+ (0, import_node_fs47.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
65982
66762
  logger.info(`Wrote flywheel summary to ${exportPath}`);
65983
66763
  process.exit(0);
65984
66764
  }
@@ -66005,8 +66785,8 @@ init_dist2();
66005
66785
  init_memory_io();
66006
66786
 
66007
66787
  // src/fix/focus-ring.ts
66008
- var import_node_fs46 = require("fs");
66009
- var import_node_fs47 = require("fs");
66788
+ var import_node_fs48 = require("fs");
66789
+ var import_node_fs49 = require("fs");
66010
66790
  var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
66011
66791
  var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
66012
66792
  :focus-visible {
@@ -66015,15 +66795,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
66015
66795
  }
66016
66796
  /* @slopbrick:v1.0.0:fix:focus-ring-end */`;
66017
66797
  function applyFocusRingFix(targetFile) {
66018
- if (!(0, import_node_fs46.existsSync)(targetFile)) {
66798
+ if (!(0, import_node_fs48.existsSync)(targetFile)) {
66019
66799
  return { applied: false, reason: "missing-global-css-target" };
66020
66800
  }
66021
- const content = (0, import_node_fs47.readFileSync)(targetFile, "utf-8");
66801
+ const content = (0, import_node_fs49.readFileSync)(targetFile, "utf-8");
66022
66802
  if (content.includes(ANCHOR_START)) {
66023
66803
  return { applied: false, reason: "already-present" };
66024
66804
  }
66025
66805
  const separator = content.endsWith("\n") ? "" : "\n";
66026
- (0, import_node_fs47.writeFileSync)(targetFile, `${content}${separator}${CSS_BLOCK}
66806
+ (0, import_node_fs49.writeFileSync)(targetFile, `${content}${separator}${CSS_BLOCK}
66027
66807
  `);
66028
66808
  return { applied: true };
66029
66809
  }
@@ -66032,21 +66812,21 @@ function applyFocusRingFix(targetFile) {
66032
66812
  init_layout_token();
66033
66813
 
66034
66814
  // src/fix/use-client.ts
66035
- var import_node_fs48 = require("fs");
66815
+ var import_node_fs50 = require("fs");
66036
66816
  function applyUseClientFix(filePath) {
66037
- const content = (0, import_node_fs48.readFileSync)(filePath, "utf-8");
66817
+ const content = (0, import_node_fs50.readFileSync)(filePath, "utf-8");
66038
66818
  const trimmed = content.trimStart();
66039
66819
  if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
66040
66820
  return { applied: false, reason: "already-present" };
66041
66821
  }
66042
- (0, import_node_fs48.writeFileSync)(filePath, `"use client";
66822
+ (0, import_node_fs50.writeFileSync)(filePath, `"use client";
66043
66823
 
66044
66824
  ${content}`);
66045
66825
  return { applied: true };
66046
66826
  }
66047
66827
 
66048
66828
  // src/fix/visual-codemod.ts
66049
- var import_node_fs49 = require("fs");
66829
+ var import_node_fs51 = require("fs");
66050
66830
 
66051
66831
  // src/fix/visual-codemods/tailwind.ts
66052
66832
  var ARBITRARY_ESCAPE_RE = /\b(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|min-w|max-w|min-h|max-h|text-\w+|leading-\w+|rounded|border)-?\[(-?\d+(?:\.\d+)?)(px|rem|em|%|vh|vw)?\]/g;
@@ -66255,7 +67035,7 @@ var ALL_CODEMODS = [
66255
67035
  { name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
66256
67036
  ];
66257
67037
  function applyVisualCodemods(filePath) {
66258
- const original = (0, import_node_fs49.readFileSync)(filePath, "utf-8");
67038
+ const original = (0, import_node_fs51.readFileSync)(filePath, "utf-8");
66259
67039
  let content = original;
66260
67040
  const reasons = [];
66261
67041
  const seen = /* @__PURE__ */ new Set();
@@ -66271,7 +67051,7 @@ function applyVisualCodemods(filePath) {
66271
67051
  }
66272
67052
  }
66273
67053
  if (content !== original) {
66274
- (0, import_node_fs49.writeFileSync)(filePath, content);
67054
+ (0, import_node_fs51.writeFileSync)(filePath, content);
66275
67055
  }
66276
67056
  return {
66277
67057
  filePath,
@@ -66553,12 +67333,12 @@ var http = __toESM(require("http"), 1);
66553
67333
  var https = __toESM(require("https"), 1);
66554
67334
  var import_node_url2 = require("url");
66555
67335
  var sendBeacon = (url, payload, timeoutMs) => {
66556
- return new Promise((resolve42) => {
67336
+ return new Promise((resolve43) => {
66557
67337
  let parsed;
66558
67338
  try {
66559
67339
  parsed = new import_node_url2.URL(url);
66560
67340
  } catch {
66561
- resolve42();
67341
+ resolve43();
66562
67342
  return;
66563
67343
  }
66564
67344
  const isHttps = parsed.protocol === "https:";
@@ -66579,7 +67359,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
66579
67359
  const settle = () => {
66580
67360
  if (settled) return;
66581
67361
  settled = true;
66582
- resolve42();
67362
+ resolve43();
66583
67363
  };
66584
67364
  let req;
66585
67365
  try {
@@ -66659,7 +67439,7 @@ process.on("uncaughtException", (err) => {
66659
67439
  });
66660
67440
  async function runCli({ start }) {
66661
67441
  try {
66662
- const program = new import_commander3.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)");
67442
+ const program = new import_commander3.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("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").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)");
66663
67443
  program.helpInformation = () => formatGroupedHelp(program);
66664
67444
  setExitOverride(program);
66665
67445
  registerInit(program);
@@ -66668,6 +67448,7 @@ async function runCli({ start }) {
66668
67448
  registerBadge(program);
66669
67449
  registerSuggest(program);
66670
67450
  registerExplain(program);
67451
+ registerComposite(program);
66671
67452
  registerCalibration(program);
66672
67453
  registerFlywheel(program);
66673
67454
  const scanAction = async (paths, _options, command) => {
@@ -66686,7 +67467,7 @@ async function runCli({ start }) {
66686
67467
  logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
66687
67468
  process.exit(2);
66688
67469
  }
66689
- const cwd = (0, import_node_path72.resolve)(options.workspace ?? process.cwd());
67470
+ const cwd = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
66690
67471
  if (options.trend !== void 0) {
66691
67472
  const runs = await readRuns(cwd, fsMemoryIO);
66692
67473
  if (runs.length === 0) {
@@ -66734,7 +67515,7 @@ async function runCli({ start }) {
66734
67515
  });
66735
67516
  }
66736
67517
  if (options.baseline) {
66737
- const cwd2 = (0, import_node_path72.resolve)(options.workspace ?? process.cwd());
67518
+ const cwd2 = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
66738
67519
  const configHash = hashConfig(config);
66739
67520
  const gitHead = await getGitHead(cwd2) ?? "unknown";
66740
67521
  const cache = buildBaselineCache(report, configHash, gitHead, cwd2);