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.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.41.0";
22
+ VERSION = "0.42.0";
23
23
  }
24
24
  });
25
25
 
@@ -46862,11 +46862,11 @@ var init_pool = __esm({
46862
46862
  for (let i = 0; i < this.threadCount; i++) {
46863
46863
  spawnWorker();
46864
46864
  }
46865
- return new Promise((resolve42) => {
46865
+ return new Promise((resolve43) => {
46866
46866
  const check = setInterval(() => {
46867
46867
  if (resolved) {
46868
46868
  clearInterval(check);
46869
- resolve42(results);
46869
+ resolve43(results);
46870
46870
  }
46871
46871
  }, 10);
46872
46872
  });
@@ -46883,23 +46883,16 @@ var init_registry = __esm({
46883
46883
  init_builtins();
46884
46884
  RuleRegistry = class {
46885
46885
  rules = /* @__PURE__ */ new Map();
46886
- register(ruleOrId, factory) {
46887
- if (typeof ruleOrId === "string") {
46888
- if (!factory) {
46889
- throw new Error("Factory is required when registering by id");
46890
- }
46891
- const rule = {
46892
- id: ruleOrId,
46893
- category: factory.category,
46894
- severity: factory.severity,
46895
- aiSpecific: factory.aiSpecific,
46896
- create: factory.create,
46897
- analyze: factory.analyze
46898
- };
46899
- this.rules.set(ruleOrId, rule);
46900
- } else {
46901
- this.rules.set(ruleOrId.id, ruleOrId);
46902
- }
46886
+ /**
46887
+ * Register a rule. The 2-arg overload `register(id, factory)` was
46888
+ * deleted in v0.42.0 (architecture review F6): no callers used it,
46889
+ * every `Rule` carries its own `id`, and the per-id registration
46890
+ * was ergonomically redundant with the 1-arg form (`register(rule)`).
46891
+ * External consumers that needed `register-by-id` semantics can
46892
+ * construct a `Rule` directly and pass it in.
46893
+ */
46894
+ register(rule) {
46895
+ this.rules.set(rule.id, rule);
46903
46896
  }
46904
46897
  loadBuiltins(onlyRuleId) {
46905
46898
  for (const rule of builtinRules) {
@@ -46928,6 +46921,15 @@ var init_registry = __esm({
46928
46921
  all() {
46929
46922
  return Array.from(this.rules.values());
46930
46923
  }
46924
+ /** v0.42.0 (§3b.5): look up a registered rule by id.
46925
+ * Returns undefined if not registered. */
46926
+ get(id) {
46927
+ return this.rules.get(id);
46928
+ }
46929
+ /** v0.42.0 (§3b.5): check whether a rule id is registered. */
46930
+ has(id) {
46931
+ return this.rules.has(id);
46932
+ }
46931
46933
  createContexts(config, filePath, cwd, hotspotIssues = []) {
46932
46934
  const context = {
46933
46935
  config,
@@ -50579,6 +50581,10 @@ var init_signal_strength2 = __esm({
50579
50581
  });
50580
50582
 
50581
50583
  // src/engine/worker.ts
50584
+ var worker_exports = {};
50585
+ __export(worker_exports, {
50586
+ scanFile: () => scanFile
50587
+ });
50582
50588
  import { isMainThread, parentPort, workerData } from "worker_threads";
50583
50589
  import { extname as extname5 } from "path";
50584
50590
  import { join as join14, relative as relative5, sep as sep2 } from "path";
@@ -50659,6 +50665,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
50659
50665
  const issues = applyRuleOverrides(rawIssues, config.rules);
50660
50666
  const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
50661
50667
  const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
50668
+ const fireSet = new Set(triggeredRuleIds);
50669
+ const factsWithFireSet = Object.assign(facts, {
50670
+ compositeFireSet: fireSet
50671
+ });
50672
+ for (const { rule } of rules) {
50673
+ if (!rule.id.startsWith("composite/")) continue;
50674
+ try {
50675
+ const compositeIssues = rule.analyze(
50676
+ rule.create({
50677
+ config,
50678
+ filePath,
50679
+ cwd,
50680
+ framework: config.framework,
50681
+ uiLibraries: config.uiLibraries,
50682
+ hasTailwind: config.hasTailwind,
50683
+ supportsRsc: config.supportsRsc,
50684
+ hotspotIssues: []
50685
+ }),
50686
+ factsWithFireSet
50687
+ );
50688
+ if (compositeIssues.length > 0) {
50689
+ issues.push(...compositeIssues);
50690
+ for (const ci of compositeIssues) fireSet.add(ci.ruleId);
50691
+ }
50692
+ } catch (err) {
50693
+ if (process.env.SLOP_AUDIT_DEBUG === "1") {
50694
+ console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
50695
+ }
50696
+ }
50697
+ }
50662
50698
  const gapValues = collectGapValues(facts);
50663
50699
  const styleSources = collectStyleSources(facts);
50664
50700
  const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
@@ -54529,29 +54565,29 @@ function renderStructureMarkdown(inventory, constitution) {
54529
54565
  return lines.join("\n");
54530
54566
  }
54531
54567
  async function writeStructureMarkdown(workspaceDir, md) {
54532
- await new Promise((resolve42, reject) => {
54568
+ await new Promise((resolve43, reject) => {
54533
54569
  try {
54534
54570
  const path = join18(workspaceDir, STRUCTURE_MD_FILE);
54535
54571
  mkdirSync6(dirname12(path), { recursive: true });
54536
54572
  writeFileSync6(path, md, "utf-8");
54537
- resolve42();
54573
+ resolve43();
54538
54574
  } catch (err) {
54539
54575
  reject(err instanceof Error ? err : new Error(String(err)));
54540
54576
  }
54541
54577
  });
54542
54578
  }
54543
54579
  async function readStructureMarkdown(workspaceDir) {
54544
- return new Promise((resolve42) => {
54580
+ return new Promise((resolve43) => {
54545
54581
  try {
54546
54582
  const path = join18(workspaceDir, STRUCTURE_MD_FILE);
54547
54583
  if (!existsSync16(path)) {
54548
- resolve42(null);
54584
+ resolve43(null);
54549
54585
  return;
54550
54586
  }
54551
54587
  const content = readFileSync20(path, "utf-8");
54552
- resolve42(content);
54588
+ resolve43(content);
54553
54589
  } catch {
54554
- resolve42(null);
54590
+ resolve43(null);
54555
54591
  }
54556
54592
  });
54557
54593
  }
@@ -54599,9 +54635,391 @@ var init_structure_md = __esm({
54599
54635
  }
54600
54636
  });
54601
54637
 
54638
+ // src/snippet/data.ts
54639
+ var CATEGORY_DIRECTIVES, RULE_HINTS;
54640
+ var init_data = __esm({
54641
+ "src/snippet/data.ts"() {
54642
+ "use strict";
54643
+ CATEGORY_DIRECTIVES = {
54644
+ 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.',
54645
+ 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.",
54646
+ 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.',
54647
+ 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".',
54648
+ 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.",
54649
+ typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
54650
+ 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].",
54651
+ component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
54652
+ 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.",
54653
+ 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'."
54654
+ };
54655
+ RULE_HINTS = {
54656
+ // v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
54657
+ // remaining key matches a rule in src/rules/builtins.ts. No orphans.
54658
+ // (v0.16.0 previously moved 35 out-of-scope orphan hints to
54659
+ // docs/research/backlog-rule-hints.md
54660
+ // for future implementers to paste back when the corresponding rules ship.)
54661
+ "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.",
54662
+ "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.",
54663
+ "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.",
54664
+ "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.",
54665
+ "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.",
54666
+ "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.",
54667
+ "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.',
54668
+ "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.",
54669
+ "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
54670
+ "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
54671
+ "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
54672
+ // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
54673
+ "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
54674
+ "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
54675
+ "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
54676
+ "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
54677
+ "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
54678
+ "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
54679
+ "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
54680
+ "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
54681
+ "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.",
54682
+ "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
54683
+ "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
54684
+ "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
54685
+ "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.",
54686
+ "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.",
54687
+ "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
54688
+ "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
54689
+ "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.)",
54690
+ "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.)",
54691
+ "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.)",
54692
+ "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
54693
+ "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
54694
+ "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
54695
+ "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
54696
+ "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
54697
+ "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
54698
+ "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
54699
+ "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.",
54700
+ "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.",
54701
+ "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.",
54702
+ "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.",
54703
+ "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.",
54704
+ "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
54705
+ "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
54706
+ "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.',
54707
+ "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
54708
+ "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
54709
+ "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.",
54710
+ "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.",
54711
+ "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.",
54712
+ "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%.",
54713
+ "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.",
54714
+ "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
54715
+ "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
54716
+ "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
54717
+ "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.",
54718
+ "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`).",
54719
+ "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.",
54720
+ "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.",
54721
+ // v0.13.0 — AI-specific rules (peer-reviewed signals).
54722
+ "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
54723
+ "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).",
54724
+ "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).",
54725
+ "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).",
54726
+ "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).",
54727
+ "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).",
54728
+ "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
54729
+ "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
54730
+ "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
54731
+ // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
54732
+ // reclassified post-v7 calibration in v0.14.5d)
54733
+ "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.",
54734
+ "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.",
54735
+ "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.",
54736
+ "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.",
54737
+ "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.",
54738
+ "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.",
54739
+ // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
54740
+ "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.',
54741
+ // v0.17.0 — docs/* rules (markdown drift detection)
54742
+ "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.",
54743
+ "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.",
54744
+ "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
54745
+ // v0.18.9 — Rust-specific rules (tree-sitter-backed)
54746
+ "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.",
54747
+ "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.",
54748
+ "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.",
54749
+ "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.',
54750
+ // v0.19.0 — TypeScript-specific rules
54751
+ "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.",
54752
+ "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.",
54753
+ "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.",
54754
+ "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.",
54755
+ // v0.19.0 — Go-specific rules
54756
+ "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).',
54757
+ "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.",
54758
+ // v0.19.0 — Duplication detector
54759
+ "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.",
54760
+ // v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
54761
+ "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.)",
54762
+ // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
54763
+ "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.)",
54764
+ // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
54765
+ // v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
54766
+ // v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
54767
+ "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.)",
54768
+ "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.)",
54769
+ // v0.35.0 — content-based detection (CoCoNUTS-inspired)
54770
+ "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.",
54771
+ // v0.35.1 — Raidar-inspired content-based detection
54772
+ "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)`.',
54773
+ // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
54774
+ "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.)",
54775
+ "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.)",
54776
+ "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.)",
54777
+ "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.)",
54778
+ "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.)",
54779
+ // v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
54780
+ "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.)",
54781
+ "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.)",
54782
+ "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.)",
54783
+ "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.)"
54784
+ };
54785
+ }
54786
+ });
54787
+
54788
+ // src/snippet/generators.ts
54789
+ function wrapWithMarkers(level, body) {
54790
+ const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
54791
+ return `${head}${MARKER_BEGIN}
54792
+ ${body}
54793
+ ${MARKER_END}
54794
+ `;
54795
+ }
54796
+ function generateCursorSnippet(rules) {
54797
+ const lines = [];
54798
+ lines.push("# slopbrick cursor rules");
54799
+ lines.push("");
54800
+ lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
54801
+ lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
54802
+ lines.push("");
54803
+ lines.push("## Category-level directives");
54804
+ lines.push("");
54805
+ for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
54806
+ lines.push("- **" + category + "**: " + directive);
54807
+ }
54808
+ lines.push("");
54809
+ lines.push("## Per-rule directives");
54810
+ lines.push("");
54811
+ const ai = aiSpecificRules(rules);
54812
+ const byCategory = categorizeRules(ai);
54813
+ for (const [category, list] of byCategory) {
54814
+ lines.push("### " + category);
54815
+ lines.push("");
54816
+ for (const rule of list) {
54817
+ const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
54818
+ lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
54819
+ }
54820
+ lines.push("");
54821
+ }
54822
+ lines.push("## Verification");
54823
+ lines.push("");
54824
+ lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
54825
+ lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
54826
+ lines.push("");
54827
+ return lines.join("\n");
54828
+ }
54829
+ function generateAgentsMdSnippet(rules) {
54830
+ return wrapWithMarkers(
54831
+ 1,
54832
+ baseContent(
54833
+ rules,
54834
+ "AGENTS.md",
54835
+ "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."
54836
+ ) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
54837
+ );
54838
+ }
54839
+ function generateClaudeMdSnippet(rules) {
54840
+ return wrapWithMarkers(
54841
+ 1,
54842
+ baseContent(
54843
+ rules,
54844
+ "CLAUDE.md",
54845
+ "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."
54846
+ ) + "## 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"
54847
+ );
54848
+ }
54849
+ function generateAiderSnippet(rules) {
54850
+ return baseContent(
54851
+ rules,
54852
+ "CONVENTIONS.md",
54853
+ "Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
54854
+ );
54855
+ }
54856
+ function generateWindsurfSnippet(rules) {
54857
+ return baseContent(
54858
+ rules,
54859
+ ".windsurfrules",
54860
+ "Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
54861
+ );
54862
+ }
54863
+ function generateClineRules(rules) {
54864
+ return baseContent(
54865
+ rules,
54866
+ "AGENTS.md",
54867
+ "Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
54868
+ );
54869
+ }
54870
+ function generateCursorrulesLegacySnippet(rules) {
54871
+ return baseContent(
54872
+ rules,
54873
+ ".cursorrules",
54874
+ "Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
54875
+ );
54876
+ }
54877
+ function generateGeminiSnippet(rules) {
54878
+ return baseContent(
54879
+ rules,
54880
+ "GEMINI.md",
54881
+ "Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
54882
+ );
54883
+ }
54884
+ function generateCopilotSnippet(rules) {
54885
+ return baseContent(
54886
+ rules,
54887
+ "copilot-instructions.md",
54888
+ "Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
54889
+ );
54890
+ }
54891
+ var MARKER_BEGIN, MARKER_END;
54892
+ var init_generators = __esm({
54893
+ "src/snippet/generators.ts"() {
54894
+ "use strict";
54895
+ init_data();
54896
+ init_render2();
54897
+ MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
54898
+ MARKER_END = "<!-- slopbrick:end:v3 -->";
54899
+ }
54900
+ });
54901
+
54902
+ // src/snippet/render.ts
54903
+ function aiSpecificRules(rules) {
54904
+ return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
54905
+ }
54906
+ function categorizeRules(rules) {
54907
+ const byCategory = /* @__PURE__ */ new Map();
54908
+ for (const r of rules) {
54909
+ const list = byCategory.get(r.category) ?? [];
54910
+ list.push(r);
54911
+ byCategory.set(r.category, list);
54912
+ }
54913
+ for (const list of byCategory.values()) {
54914
+ list.sort((a, b) => a.id.localeCompare(b.id));
54915
+ }
54916
+ return byCategory;
54917
+ }
54918
+ function baseContent(rules, toolLabel, intro) {
54919
+ const lines = [];
54920
+ lines.push("# " + toolLabel);
54921
+ lines.push("");
54922
+ lines.push(intro);
54923
+ lines.push("");
54924
+ const ai = aiSpecificRules(rules);
54925
+ const byCategory = categorizeRules(ai);
54926
+ lines.push("## Category-level directives");
54927
+ lines.push("");
54928
+ for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
54929
+ lines.push("- **" + category + "**: " + directive);
54930
+ }
54931
+ lines.push("");
54932
+ lines.push("## Per-rule directives");
54933
+ lines.push("");
54934
+ for (const [category, list] of byCategory) {
54935
+ lines.push("### " + category);
54936
+ lines.push("");
54937
+ for (const rule of list) {
54938
+ const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
54939
+ lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
54940
+ }
54941
+ lines.push("");
54942
+ }
54943
+ return lines.join("\n");
54944
+ }
54945
+ function safeRewrite(existing, freshBlockBody) {
54946
+ const beginIdx = existing.indexOf(MARKER_BEGIN);
54947
+ const endIdx = existing.indexOf(MARKER_END);
54948
+ if (beginIdx === -1 && endIdx === -1) {
54949
+ return { content: existing, rewritten: false, failClosed: true, mismatched: false };
54950
+ }
54951
+ if (beginIdx === -1 || endIdx === -1) {
54952
+ return { content: existing, rewritten: false, failClosed: false, mismatched: true };
54953
+ }
54954
+ if (endIdx <= beginIdx) {
54955
+ return { content: existing, rewritten: false, failClosed: false, mismatched: true };
54956
+ }
54957
+ const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
54958
+ const after = existing.slice(endIdx);
54959
+ const next = `${before}
54960
+ ${freshBlockBody}
54961
+ ${after}`;
54962
+ return { content: next, rewritten: true, failClosed: false, mismatched: false };
54963
+ }
54964
+ var init_render2 = __esm({
54965
+ "src/snippet/render.ts"() {
54966
+ "use strict";
54967
+ init_data();
54968
+ init_generators();
54969
+ }
54970
+ });
54971
+
54972
+ // src/snippet/refresh.ts
54973
+ var refresh_exports = {};
54974
+ __export(refresh_exports, {
54975
+ SNIPPET_TARGETS: () => SNIPPET_TARGETS,
54976
+ refreshSnippets: () => refreshSnippets
54977
+ });
54978
+ import { existsSync as existsSync17, readFileSync as readFileSync21, writeFileSync as writeFileSync7 } from "fs";
54979
+ import { join as join19 } from "path";
54980
+ function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
54981
+ const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
54982
+ for (const filename of targets) {
54983
+ const path = join19(cwd, filename);
54984
+ if (!existsSync17(path)) {
54985
+ result.absent.push(filename);
54986
+ continue;
54987
+ }
54988
+ const existing = readFileSync21(path, "utf-8");
54989
+ const agentName = filename.replace(/\.md$/i, "").toLowerCase();
54990
+ const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
54991
+ const newBlock = generator([...rules]).trim();
54992
+ const outcome = safeRewrite(existing, newBlock);
54993
+ if (outcome.rewritten) {
54994
+ const tmp = `${path}.tmp`;
54995
+ writeFileSync7(tmp, outcome.content, "utf-8");
54996
+ const fs = __require("fs");
54997
+ fs.renameSync(tmp, path);
54998
+ result.rewritten.push(filename);
54999
+ } else if (outcome.failClosed) {
55000
+ result.failClosed.push(filename);
55001
+ } else if (outcome.mismatched) {
55002
+ result.mismatched.push(filename);
55003
+ }
55004
+ }
55005
+ return result;
55006
+ }
55007
+ var SNIPPET_TARGETS;
55008
+ var init_refresh = __esm({
55009
+ "src/snippet/refresh.ts"() {
55010
+ "use strict";
55011
+ init_render2();
55012
+ init_generators();
55013
+ SNIPPET_TARGETS = [
55014
+ "AGENTS.md",
55015
+ "CLAUDE.md"
55016
+ ];
55017
+ }
55018
+ });
55019
+
54602
55020
  // src/cli/report/persistRun.ts
54603
- import { existsSync as existsSync17, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7 } from "fs";
54604
- import { join as join19, relative as relative10 } from "path";
55021
+ import { existsSync as existsSync18, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
55022
+ import { join as join20, relative as relative10 } from "path";
54605
55023
  async function persistRun(input) {
54606
55024
  const {
54607
55025
  cwd,
@@ -54683,10 +55101,10 @@ async function persistRun(input) {
54683
55101
  report.research = state.research;
54684
55102
  }
54685
55103
  if (flywheelOutput.suggestions.length > 0) {
54686
- const suggestionsDir = join19(cwd, ".slopbrick", "flywheel");
54687
- if (!existsSync17(suggestionsDir)) mkdirSync7(suggestionsDir, { recursive: true });
54688
- writeFileSync7(
54689
- join19(suggestionsDir, "rule-suggestions.json"),
55104
+ const suggestionsDir = join20(cwd, ".slopbrick", "flywheel");
55105
+ if (!existsSync18(suggestionsDir)) mkdirSync7(suggestionsDir, { recursive: true });
55106
+ writeFileSync8(
55107
+ join20(suggestionsDir, "rule-suggestions.json"),
54690
55108
  JSON.stringify(flywheelOutput.suggestions, null, 2)
54691
55109
  );
54692
55110
  }
@@ -54731,6 +55149,21 @@ async function persistRun(input) {
54731
55149
  }
54732
55150
  }
54733
55151
  recordTelemetry(cwd, report, results, config, patternInventory);
55152
+ if (input.autoRefreshSnippets) {
55153
+ try {
55154
+ const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
55155
+ const outcome = refreshSnippets2(cwd, registry.getRules());
55156
+ if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
55157
+ logger.info(
55158
+ `Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
55159
+ );
55160
+ }
55161
+ } catch (err) {
55162
+ if (options.quiet !== true && !machineReadableStdout) {
55163
+ logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
55164
+ }
55165
+ }
55166
+ }
54734
55167
  }
54735
55168
  var init_persistRun = __esm({
54736
55169
  "src/cli/report/persistRun.ts"() {
@@ -54951,7 +55384,7 @@ var init_json = __esm({
54951
55384
 
54952
55385
  // src/report/sarif.ts
54953
55386
  import { createHash as createHash11 } from "crypto";
54954
- import { readFileSync as readFileSync21 } from "fs";
55387
+ import { readFileSync as readFileSync22 } from "fs";
54955
55388
  import { basename as basename3, isAbsolute as isAbsolute3, relative as relative12, resolve as resolve9 } from "path";
54956
55389
  function buildArtifactUri(filePath, cwd) {
54957
55390
  if (!filePath) {
@@ -55058,7 +55491,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
55058
55491
  let content = fileContentCache.get(sourcePath);
55059
55492
  if (content === void 0) {
55060
55493
  try {
55061
- content = readFileSync21(sourcePath, "utf-8");
55494
+ content = readFileSync22(sourcePath, "utf-8");
55062
55495
  fileContentCache.set(sourcePath, content);
55063
55496
  } catch {
55064
55497
  fileContentCache.set(sourcePath, "");
@@ -56284,7 +56717,7 @@ var init_advice = __esm({
56284
56717
  });
56285
56718
 
56286
56719
  // src/fix/layout-token.ts
56287
- import { readFileSync as readFileSync22, writeFileSync as writeFileSync8 } from "fs";
56720
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "fs";
56288
56721
  function escapeRegExp(value) {
56289
56722
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
56290
56723
  }
@@ -56321,10 +56754,10 @@ function applyReplaceFixes(content, fixes) {
56321
56754
  return { content, applied, skipped, reasons };
56322
56755
  }
56323
56756
  function applyLayoutTokenFix(filePath, fixes) {
56324
- let content = readFileSync22(filePath, "utf-8");
56757
+ let content = readFileSync23(filePath, "utf-8");
56325
56758
  const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
56326
56759
  if (patched !== content) {
56327
- writeFileSync8(filePath, patched);
56760
+ writeFileSync9(filePath, patched);
56328
56761
  }
56329
56762
  return { applied, skipped, reasons };
56330
56763
  }
@@ -56335,7 +56768,7 @@ var init_layout_token = __esm({
56335
56768
  });
56336
56769
 
56337
56770
  // src/report/unified-diff.ts
56338
- import { existsSync as existsSync18, readFileSync as readFileSync23 } from "fs";
56771
+ import { existsSync as existsSync19, readFileSync as readFileSync24 } from "fs";
56339
56772
  import { relative as relative13 } from "path";
56340
56773
  function collectAllFixes(issue) {
56341
56774
  return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
@@ -56412,8 +56845,8 @@ function formatUnifiedDiff(report, cwd) {
56412
56845
  const parts = [];
56413
56846
  let hasHunk = false;
56414
56847
  for (const [filePath, fixes] of byFile) {
56415
- if (!existsSync18(filePath)) continue;
56416
- const original = readFileSync23(filePath, "utf-8");
56848
+ if (!existsSync19(filePath)) continue;
56849
+ const original = readFileSync24(filePath, "utf-8");
56417
56850
  const patched = applyFixesToString(original, fixes);
56418
56851
  if (patched === original) continue;
56419
56852
  if (!hasHunk) {
@@ -56519,7 +56952,7 @@ var init_heatmap = __esm({
56519
56952
 
56520
56953
  // src/cli/report/renderOutput.ts
56521
56954
  import { resolve as resolve10 } from "path";
56522
- import { writeFileSync as writeFileSync9 } from "fs";
56955
+ import { writeFileSync as writeFileSync10 } from "fs";
56523
56956
  function renderOutput(report, options, cwd) {
56524
56957
  if (options.format && !VALID_FORMATS.has(options.format)) {
56525
56958
  process.stderr.write(
@@ -56553,7 +56986,7 @@ function renderOutput(report, options, cwd) {
56553
56986
  if (options.html) {
56554
56987
  const html = formatHtml(report);
56555
56988
  if (typeof options.html === "string") {
56556
- writeFileSync9(resolve10(options.html), html);
56989
+ writeFileSync10(resolve10(options.html), html);
56557
56990
  if (!options.quiet) {
56558
56991
  logger.info(`Wrote HTML report to ${options.html}`);
56559
56992
  }
@@ -56565,7 +56998,7 @@ function renderOutput(report, options, cwd) {
56565
56998
  if (options.json) {
56566
56999
  const json = formatJson(report);
56567
57000
  if (typeof options.json === "string") {
56568
- writeFileSync9(resolve10(options.json), json);
57001
+ writeFileSync10(resolve10(options.json), json);
56569
57002
  if (!options.quiet) {
56570
57003
  logger.info(`Wrote JSON report to ${options.json}`);
56571
57004
  }
@@ -56796,7 +57229,7 @@ var init_watch = __esm({
56796
57229
  });
56797
57230
 
56798
57231
  // src/cli/scan.ts
56799
- import { existsSync as existsSync19, statSync as statSync7 } from "fs";
57232
+ import { existsSync as existsSync20, statSync as statSync7 } from "fs";
56800
57233
  import { resolve as resolve12, relative as relative15, extname as extname7, sep as sep3 } from "path";
56801
57234
  import { randomUUID } from "crypto";
56802
57235
  async function runScan(options, explicitPaths) {
@@ -56804,7 +57237,7 @@ async function runScan(options, explicitPaths) {
56804
57237
  const startTime = Date.now();
56805
57238
  const scanId = randomUUID();
56806
57239
  const cwd = resolve12(options.workspace ?? process.cwd());
56807
- if (!existsSync19(cwd)) {
57240
+ if (!existsSync20(cwd)) {
56808
57241
  throw new Error(`Workspace not found: ${cwd}`);
56809
57242
  }
56810
57243
  const cwdStat = statSync7(cwd);
@@ -56849,7 +57282,7 @@ async function runScan(options, explicitPaths) {
56849
57282
  const resolved = explicitPaths.map((p) => resolve12(cwd, p));
56850
57283
  const expanded = [];
56851
57284
  for (const p of resolved) {
56852
- if (existsSync19(p) && statSync7(p).isDirectory()) {
57285
+ if (existsSync20(p) && statSync7(p).isDirectory()) {
56853
57286
  const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
56854
57287
  for (const f of found) {
56855
57288
  if (!ALL_SOURCE_EXTENSIONS.has(extname7(f).toLowerCase())) continue;
@@ -57003,7 +57436,7 @@ async function runScan(options, explicitPaths) {
57003
57436
  const scannedPaths = new Set(results.map((result) => result.filePath));
57004
57437
  for (const [filePath, cached] of Object.entries(baseline.scores)) {
57005
57438
  if (scannedPaths.has(filePath)) continue;
57006
- if (!existsSync19(filePath)) continue;
57439
+ if (!existsSync20(filePath)) continue;
57007
57440
  scores.push({
57008
57441
  filePath,
57009
57442
  rawScore: 0,
@@ -57042,7 +57475,12 @@ async function runScan(options, explicitPaths) {
57042
57475
  const { report, noIncreaseFailure } = await finalizeReport({
57043
57476
  cwd,
57044
57477
  config,
57045
- options,
57478
+ options: {
57479
+ ...options,
57480
+ // v0.42.0 (§3a.4): forward the opt-in flag through to
57481
+ // `persistRun`, which calls `refreshSnippets` post-scan.
57482
+ autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
57483
+ },
57046
57484
  results,
57047
57485
  aggregated,
57048
57486
  allIssues,
@@ -57110,13 +57548,57 @@ var init_scan2 = __esm({
57110
57548
  }
57111
57549
  });
57112
57550
 
57551
+ // src/rules/composite-seed.ts
57552
+ var composite_seed_exports = {};
57553
+ __export(composite_seed_exports, {
57554
+ HAND_CURATED_SEED: () => HAND_CURATED_SEED,
57555
+ maybeAppendSeed: () => maybeAppendSeed
57556
+ });
57557
+ function maybeAppendSeed(entries) {
57558
+ if (entries.length === 0) {
57559
+ return [...HAND_CURATED_SEED];
57560
+ }
57561
+ return entries;
57562
+ }
57563
+ var HAND_CURATED_SEED;
57564
+ var init_composite_seed = __esm({
57565
+ "src/rules/composite-seed.ts"() {
57566
+ "use strict";
57567
+ HAND_CURATED_SEED = [
57568
+ {
57569
+ id: "composite/ai-fingerprint-cluster",
57570
+ ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
57571
+ minMatch: 2,
57572
+ severity: "medium",
57573
+ defaultOff: true,
57574
+ 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.",
57575
+ calibration: {
57576
+ recall: 0,
57577
+ FP: 0,
57578
+ precision: 0,
57579
+ F1: 0,
57580
+ nFiles: 0
57581
+ },
57582
+ provenance: {
57583
+ seed: "hand-curated-by-brief",
57584
+ discoveredAt: "2026-07-05T00:00:00.000Z",
57585
+ nFiles: 0,
57586
+ members: 3,
57587
+ npmi: 0,
57588
+ fisherP: 1
57589
+ }
57590
+ }
57591
+ ];
57592
+ }
57593
+ });
57594
+
57113
57595
  // src/index.ts
57114
57596
  init_types();
57115
57597
  init_config2();
57116
57598
  init_dist2();
57117
57599
 
57118
57600
  // src/cli/program.ts
57119
- import { resolve as resolve41 } from "path";
57601
+ import { resolve as resolve42 } from "path";
57120
57602
  import { Command } from "commander";
57121
57603
 
57122
57604
  // src/cli/options.ts
@@ -57164,13 +57646,13 @@ init_discover();
57164
57646
  init_git();
57165
57647
  init_cache();
57166
57648
  init_logger();
57167
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync9, rmSync as rmSync2 } from "fs";
57168
- import { join as join21, dirname as dirname14 } from "path";
57649
+ import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, rmSync as rmSync2 } from "fs";
57650
+ import { join as join23, dirname as dirname14 } from "path";
57169
57651
  import { createInterface } from "readline";
57170
57652
 
57171
57653
  // src/rules/registry-loader.ts
57172
- import { existsSync as existsSync20, mkdirSync as mkdirSync8, readFileSync as readFileSync24, writeFileSync as writeFileSync10 } from "fs";
57173
- import { dirname as dirname13, join as join20 } from "path";
57654
+ import { existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "fs";
57655
+ import { dirname as dirname13, join as join21 } from "path";
57174
57656
 
57175
57657
  // src/data/shadcn-registry.json
57176
57658
  var shadcn_registry_default = {
@@ -57304,11 +57786,11 @@ var shadcn_registry_default = {
57304
57786
  var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
57305
57787
  var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
57306
57788
  function cachePath2(cwd) {
57307
- return join20(cwd, ".slopbrick", "cache", "registry-snapshot.json");
57789
+ return join21(cwd, ".slopbrick", "cache", "registry-snapshot.json");
57308
57790
  }
57309
57791
  function ensureCacheDir(cwd) {
57310
57792
  const dir = dirname13(cachePath2(cwd));
57311
- if (!existsSync20(dir)) {
57793
+ if (!existsSync21(dir)) {
57312
57794
  mkdirSync8(dir, { recursive: true });
57313
57795
  }
57314
57796
  }
@@ -57321,9 +57803,9 @@ function isValidSnapshot(value) {
57321
57803
  }
57322
57804
  function isRegistryFresh(cwd) {
57323
57805
  const cached = cachePath2(cwd);
57324
- if (!existsSync20(cached)) return false;
57806
+ if (!existsSync21(cached)) return false;
57325
57807
  try {
57326
- const parsed = JSON.parse(readFileSync24(cached, "utf8"));
57808
+ const parsed = JSON.parse(readFileSync25(cached, "utf8"));
57327
57809
  if (!isValidSnapshot(parsed)) return false;
57328
57810
  return parsed.version === BUNDLED_REGISTRY_VERSION;
57329
57811
  } catch {
@@ -57334,7 +57816,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
57334
57816
  try {
57335
57817
  const cacheFile = cachePath2(cwd);
57336
57818
  ensureCacheDir(cwd);
57337
- writeFileSync10(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
57819
+ writeFileSync11(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
57338
57820
  } catch {
57339
57821
  }
57340
57822
  return {
@@ -57346,7 +57828,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
57346
57828
  }
57347
57829
  function copyBundledSnapshotToCache(cwd) {
57348
57830
  ensureCacheDir(cwd);
57349
- writeFileSync10(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
57831
+ writeFileSync11(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
57350
57832
  }
57351
57833
 
57352
57834
  // src/cli/init.ts
@@ -57375,17 +57857,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
57375
57857
  var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
57376
57858
  var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
57377
57859
  function promptText(rl, question, detected) {
57378
- return new Promise((resolve42) => {
57860
+ return new Promise((resolve43) => {
57379
57861
  const lines = [
57380
57862
  `? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
57381
57863
  ];
57382
57864
  rl.question(lines.join("\n") + "\n", (answer) => {
57383
57865
  const trimmed = answer.trim();
57384
57866
  if (trimmed === "") {
57385
- resolve42(void 0);
57867
+ resolve43(void 0);
57386
57868
  return;
57387
57869
  }
57388
- resolve42(trimmed);
57870
+ resolve43(trimmed);
57389
57871
  });
57390
57872
  });
57391
57873
  }
@@ -57393,7 +57875,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57393
57875
  const defaultIndex = options.indexOf(defaultValue);
57394
57876
  const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
57395
57877
  const safeDefaultValue = options[safeDefaultIndex];
57396
- return new Promise((resolve42) => {
57878
+ return new Promise((resolve43) => {
57397
57879
  function ask() {
57398
57880
  const lines = [
57399
57881
  `? ${question} (detected: ${safeDefaultValue}):`,
@@ -57403,7 +57885,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57403
57885
  rl.question(lines.join("\n") + "\n", (answer) => {
57404
57886
  const trimmed = answer.trim();
57405
57887
  if (trimmed === "") {
57406
- resolve42(safeDefaultValue);
57888
+ resolve43(safeDefaultValue);
57407
57889
  return;
57408
57890
  }
57409
57891
  const num = parseInt(trimmed, 10);
@@ -57412,7 +57894,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
57412
57894
  ask();
57413
57895
  return;
57414
57896
  }
57415
- resolve42(options[num - 1]);
57897
+ resolve43(options[num - 1]);
57416
57898
  });
57417
57899
  }
57418
57900
  ask();
@@ -57422,7 +57904,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57422
57904
  const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
57423
57905
  const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
57424
57906
  const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
57425
- return new Promise((resolve42) => {
57907
+ return new Promise((resolve43) => {
57426
57908
  function ask() {
57427
57909
  const lines = [
57428
57910
  `? ${question} (detected: ${defaultDisplay}):`,
@@ -57433,7 +57915,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57433
57915
  rl.question(lines.join("\n") + "\n", (answer) => {
57434
57916
  const trimmed = answer.trim();
57435
57917
  if (trimmed === "") {
57436
- resolve42(defaultValue.length > 0 ? defaultValue : []);
57918
+ resolve43(defaultValue.length > 0 ? defaultValue : []);
57437
57919
  return;
57438
57920
  }
57439
57921
  const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
@@ -57443,11 +57925,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
57443
57925
  return;
57444
57926
  }
57445
57927
  if (numbers.includes(1)) {
57446
- resolve42([]);
57928
+ resolve43([]);
57447
57929
  return;
57448
57930
  }
57449
57931
  const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
57450
- resolve42(selected);
57932
+ resolve43(selected);
57451
57933
  });
57452
57934
  }
57453
57935
  ask();
@@ -57535,9 +58017,9 @@ async function runDoctor(cwd) {
57535
58017
  }
57536
58018
  try {
57537
58019
  const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
57538
- const testFile = join21(cwd, ".slopbrick", ".doctor-test.ts");
58020
+ const testFile = join23(cwd, ".slopbrick", ".doctor-test.ts");
57539
58021
  mkdirSync9(dirname14(testFile), { recursive: true });
57540
- writeFileSync11(testFile, "export const x = 1;\n");
58022
+ writeFileSync12(testFile, "export const x = 1;\n");
57541
58023
  await tryParse(testFile);
57542
58024
  rmSync2(testFile, { force: true });
57543
58025
  ok("Parser is working.");
@@ -57671,6 +58153,470 @@ function registerSuggest(program) {
57671
58153
  });
57672
58154
  }
57673
58155
 
58156
+ // src/cli/commands/composite.ts
58157
+ init_logger();
58158
+ init_telemetry();
58159
+ import { resolve as resolve15 } from "path";
58160
+
58161
+ // src/engine/cluster.ts
58162
+ import { createHash as createHash12 } from "crypto";
58163
+ var DEFAULT_CLUSTER_PARAMS = {
58164
+ minSupport: 0.05,
58165
+ // 5% of files
58166
+ minNPMI: 0.3,
58167
+ // Cohen's-Landis-and-Koch "fair" agreement
58168
+ fisherAlpha: 0.01,
58169
+ // Bonferroni-style floor for many-pair tests
58170
+ minClusterSize: 2
58171
+ };
58172
+ function adaptiveMinSupport(fileCount) {
58173
+ if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
58174
+ return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
58175
+ }
58176
+ function computeRuleSupport(fireMatrix) {
58177
+ const support = /* @__PURE__ */ new Map();
58178
+ for (const ruleIds of fireMatrix.values()) {
58179
+ for (const ruleId of ruleIds) {
58180
+ support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
58181
+ }
58182
+ }
58183
+ return support;
58184
+ }
58185
+ function prefilterBySupport(support, totalFiles, minSupport) {
58186
+ const kept = /* @__PURE__ */ new Set();
58187
+ const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
58188
+ for (const [ruleId, count] of support) {
58189
+ if (count >= minCount) kept.add(ruleId);
58190
+ }
58191
+ return kept;
58192
+ }
58193
+ function buildContingency(rA, rB, totalFiles) {
58194
+ let aAndB = 0;
58195
+ let aOnly = 0;
58196
+ let bOnly = 0;
58197
+ for (const file of rA) {
58198
+ if (rB.has(file)) aAndB++;
58199
+ else aOnly++;
58200
+ }
58201
+ for (const file of rB) {
58202
+ if (!rA.has(file)) bOnly++;
58203
+ }
58204
+ return {
58205
+ aAndB,
58206
+ aOnly,
58207
+ bOnly,
58208
+ neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
58209
+ };
58210
+ }
58211
+ function fisherExactTwoSided(c) {
58212
+ const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
58213
+ const K1 = c.aAndB + c.aOnly;
58214
+ const K2 = c.aAndB + c.bOnly;
58215
+ const n = K1;
58216
+ const kObs = c.aAndB;
58217
+ const logPmf = (k) => {
58218
+ if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
58219
+ return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
58220
+ };
58221
+ const pObs = Math.exp(logPmf(kObs));
58222
+ const kMin = Math.max(0, K1 + K2 - N);
58223
+ const kMax = Math.min(K1, K2);
58224
+ let p = 0;
58225
+ for (let k = kMin; k <= kMax; k++) {
58226
+ const pk = Math.exp(logPmf(k));
58227
+ if (pk <= pObs + 1e-12) {
58228
+ p += pk;
58229
+ }
58230
+ }
58231
+ return Math.min(1, p);
58232
+ }
58233
+ function lnChoose(n, k) {
58234
+ if (k < 0 || k > n) return -Infinity;
58235
+ if (k === 0 || k === n) return 0;
58236
+ k = Math.min(k, n - k);
58237
+ let s = 0;
58238
+ for (let i = 1; i <= k; i++) {
58239
+ s += Math.log((n - k + i) / i);
58240
+ }
58241
+ return s;
58242
+ }
58243
+ function npmi(c, total) {
58244
+ if (total === 0 || c.aAndB === 0) return -1;
58245
+ const pXY = c.aAndB / total;
58246
+ const pX = (c.aAndB + c.aOnly) / total;
58247
+ const pY = (c.aAndB + c.bOnly) / total;
58248
+ if (pX === 0 || pY === 0 || pXY === 0) return -1;
58249
+ const pmi = Math.log2(pXY / (pX * pY));
58250
+ const norm = -Math.log2(pXY);
58251
+ return norm === 0 ? 1 : pmi / norm;
58252
+ }
58253
+ function computeEdges(fireMatrix, rules, total, params) {
58254
+ const ruleToFiles = /* @__PURE__ */ new Map();
58255
+ for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
58256
+ for (const [file, fired] of fireMatrix) {
58257
+ for (const ruleId of fired) {
58258
+ if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
58259
+ }
58260
+ }
58261
+ const ruleList = Array.from(rules).sort();
58262
+ const edges = [];
58263
+ for (let i = 0; i < ruleList.length; i++) {
58264
+ for (let j = i + 1; j < ruleList.length; j++) {
58265
+ const a = ruleList[i];
58266
+ const b = ruleList[j];
58267
+ const rA = ruleToFiles.get(a);
58268
+ const rB = ruleToFiles.get(b);
58269
+ const c = buildContingency(rA, rB, total);
58270
+ const n = npmi(c, total);
58271
+ if (n < params.minNPMI) continue;
58272
+ const p = fisherExactTwoSided(c);
58273
+ if (p > params.fisherAlpha) continue;
58274
+ edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
58275
+ }
58276
+ }
58277
+ return edges;
58278
+ }
58279
+ function singleLinkageCluster(edges, minClusterSize) {
58280
+ const parent = /* @__PURE__ */ new Map();
58281
+ const find = (x) => {
58282
+ if (!parent.has(x)) {
58283
+ parent.set(x, x);
58284
+ return x;
58285
+ }
58286
+ if (parent.get(x) === x) return x;
58287
+ const root = find(parent.get(x));
58288
+ parent.set(x, root);
58289
+ return root;
58290
+ };
58291
+ const union = (a, b) => {
58292
+ const ra = find(a);
58293
+ const rb = find(b);
58294
+ if (ra !== rb) parent.set(ra, rb);
58295
+ };
58296
+ for (const e of edges) {
58297
+ union(e.ruleIds[0], e.ruleIds[1]);
58298
+ }
58299
+ const groups = /* @__PURE__ */ new Map();
58300
+ for (const k of parent.keys()) {
58301
+ const root = find(k);
58302
+ if (!groups.has(root)) groups.set(root, []);
58303
+ groups.get(root).push(k);
58304
+ }
58305
+ const clusters = [];
58306
+ for (const members of groups.values()) {
58307
+ if (members.length < minClusterSize) continue;
58308
+ members.sort();
58309
+ clusters.push(members);
58310
+ }
58311
+ clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
58312
+ return clusters;
58313
+ }
58314
+ function filesForComposite(fireMatrix, members, minMatch) {
58315
+ const fired = /* @__PURE__ */ new Set();
58316
+ for (const [file, ids] of fireMatrix) {
58317
+ let n = 0;
58318
+ for (const m of members) if (ids.has(m)) n++;
58319
+ if (n >= minMatch) fired.add(file);
58320
+ }
58321
+ return fired;
58322
+ }
58323
+ var DEFAULT_CALIBRATION = {
58324
+ // 1.5× is the documented floor.
58325
+ recallFpRatio: 1.5,
58326
+ // 0.05 absolute FP — the same `min` value most rules get via the
58327
+ // v0.18.x calibration contract.
58328
+ minFpFloor: 0.05
58329
+ };
58330
+ function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
58331
+ if (cluster2.length < 2) return null;
58332
+ const clusterSet = new Set(cluster2);
58333
+ const clusterEdges = edges.filter(
58334
+ (e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
58335
+ );
58336
+ if (clusterEdges.length === 0) return null;
58337
+ const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
58338
+ let best = null;
58339
+ for (let k = 1; k <= cluster2.length; k++) {
58340
+ const firedFiles = filesForComposite(fireMatrix, cluster2, k);
58341
+ const nFiles = firedFiles.size;
58342
+ if (nFiles === 0) continue;
58343
+ let recall = 0;
58344
+ let fp = 0;
58345
+ let precision = 0;
58346
+ let canCalibrate = false;
58347
+ if (positiveFiles && positiveFiles.size > 0) {
58348
+ let tp = 0;
58349
+ let fpc = 0;
58350
+ for (const file of firedFiles) {
58351
+ if (positiveFiles.has(file)) tp++;
58352
+ else fpc++;
58353
+ }
58354
+ const totalPos = positiveFiles.size;
58355
+ const totalNeg = Math.max(1, totalFiles - totalPos);
58356
+ recall = totalPos > 0 ? tp / totalPos : 0;
58357
+ fp = fpc / totalNeg;
58358
+ precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
58359
+ canCalibrate = true;
58360
+ }
58361
+ const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
58362
+ if (canCalibrate) {
58363
+ if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
58364
+ if (fp < thresholds.minFpFloor && recall < 0.5) continue;
58365
+ }
58366
+ if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
58367
+ best = {
58368
+ minMatch: k,
58369
+ npmi: strongest.weight,
58370
+ fisherP: strongest.fisherP,
58371
+ recall,
58372
+ FP: fp,
58373
+ precision,
58374
+ F1,
58375
+ nFiles
58376
+ };
58377
+ }
58378
+ }
58379
+ return best;
58380
+ }
58381
+ function compositeId(members) {
58382
+ const sorted = [...members].sort();
58383
+ return "composite/" + createHash12("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
58384
+ }
58385
+ function worstSeverity(members, memberSeverities) {
58386
+ let worst = "low";
58387
+ const order = { low: 0, medium: 1, high: 2 };
58388
+ for (const m of members) {
58389
+ const s = memberSeverities?.get(m);
58390
+ if (s && order[s] > order[worst]) worst = s;
58391
+ }
58392
+ return worst;
58393
+ }
58394
+ function describeComposite(members, minMatch) {
58395
+ const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
58396
+ return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
58397
+ }
58398
+ function runClusterer(input) {
58399
+ const total = input.fireMatrix.size;
58400
+ const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
58401
+ const params = {
58402
+ minSupport,
58403
+ minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
58404
+ fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
58405
+ minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
58406
+ };
58407
+ const support = computeRuleSupport(input.fireMatrix);
58408
+ const rulesKept = prefilterBySupport(support, total, params.minSupport);
58409
+ const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
58410
+ const clusters = singleLinkageCluster(edges, params.minClusterSize);
58411
+ const entries = [];
58412
+ const clusterMaxNPMI = /* @__PURE__ */ new Map();
58413
+ for (const cluster2 of clusters) {
58414
+ const cal = calibrateCluster(
58415
+ cluster2,
58416
+ edges,
58417
+ input.fireMatrix,
58418
+ input.positiveFiles,
58419
+ total
58420
+ );
58421
+ if (!cal) continue;
58422
+ const id = compositeId(cluster2);
58423
+ const severity = worstSeverity(cluster2, input.memberSeverities);
58424
+ entries.push({
58425
+ id,
58426
+ ruleIds: [...cluster2].sort(),
58427
+ minMatch: cal.minMatch,
58428
+ severity,
58429
+ defaultOff: true,
58430
+ description: describeComposite(cluster2, cal.minMatch),
58431
+ calibration: {
58432
+ recall: round3(cal.recall),
58433
+ FP: round3(cal.FP),
58434
+ precision: round3(cal.precision),
58435
+ F1: round3(cal.F1),
58436
+ nFiles: cal.nFiles
58437
+ },
58438
+ provenance: {
58439
+ seed: "auto-cluster",
58440
+ discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
58441
+ nFiles: total,
58442
+ members: cluster2.length,
58443
+ npmi: round3(cal.npmi),
58444
+ fisherP: round3(cal.fisherP)
58445
+ }
58446
+ });
58447
+ clusterMaxNPMI.set(id, cal.npmi);
58448
+ }
58449
+ entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
58450
+ return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
58451
+ }
58452
+ function round3(n) {
58453
+ return Math.round(n * 1e3) / 1e3;
58454
+ }
58455
+
58456
+ // src/rules/composite-loader.ts
58457
+ import { existsSync as existsSync23, readFileSync as readFileSync27, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
58458
+ import { dirname as dirname15, join as join24 } from "path";
58459
+ var COMPOSITES_FILE = "composites.json";
58460
+ function compositesPath(cwd) {
58461
+ return join24(cwd, COMPOSITES_FILE);
58462
+ }
58463
+ function readComposites(cwd) {
58464
+ const path = compositesPath(cwd);
58465
+ if (!existsSync23(path)) return [];
58466
+ try {
58467
+ const raw = JSON.parse(readFileSync27(path, "utf-8"));
58468
+ if (!Array.isArray(raw)) return [];
58469
+ return raw.filter(isCompositeRuleEntry);
58470
+ } catch {
58471
+ return [];
58472
+ }
58473
+ }
58474
+ function isCompositeRuleEntry(value) {
58475
+ if (!value || typeof value !== "object") return false;
58476
+ const v = value;
58477
+ return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
58478
+ }
58479
+ function writeComposites(cwd, entries) {
58480
+ const path = compositesPath(cwd);
58481
+ mkdirSync10(dirname15(path), { recursive: true });
58482
+ const tmp = `${path}.tmp`;
58483
+ writeFileSync13(tmp, JSON.stringify(entries, null, 2), "utf-8");
58484
+ const fs = __require("fs");
58485
+ fs.renameSync(tmp, path);
58486
+ }
58487
+
58488
+ // src/cli/commands/composite.ts
58489
+ function registerComposite(program) {
58490
+ const cmd = program.command("composite").description(
58491
+ "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."
58492
+ );
58493
+ cmd.command("discover").description(
58494
+ "run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
58495
+ ).action(async (_cmdOptions, command) => {
58496
+ const options = command.optsWithGlobals();
58497
+ const cwd = resolve15(options.workspace ?? process.cwd());
58498
+ const payloads = readTelemetry(cwd);
58499
+ if (payloads.length === 0) {
58500
+ logger.error(
58501
+ "`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
58502
+ );
58503
+ process.exit(1);
58504
+ }
58505
+ const fireMatrix = /* @__PURE__ */ new Map();
58506
+ for (const payload of payloads) {
58507
+ for (const f of payload.files) {
58508
+ const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
58509
+ for (const id of f.ruleIds) set.add(id);
58510
+ if (set.size > 0) fireMatrix.set(f.hash, set);
58511
+ }
58512
+ }
58513
+ const out = runClusterer({
58514
+ fireMatrix,
58515
+ // No labeled positive set; let the clusterer surface for audit.
58516
+ positiveFiles: void 0,
58517
+ now: (/* @__PURE__ */ new Date()).toISOString()
58518
+ });
58519
+ const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
58520
+ writeComposites(cwd, finalEntries);
58521
+ if (out.entries.length === 0) {
58522
+ logger.info("Composite discovery: no clusters passed the empirical gate.");
58523
+ logger.info(
58524
+ " (Try lowering --min-support, or run more scans to densify the ledger.)"
58525
+ );
58526
+ return;
58527
+ }
58528
+ logger.info(
58529
+ `Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
58530
+ );
58531
+ const lines = [];
58532
+ for (const e of out.entries) {
58533
+ lines.push("");
58534
+ lines.push(
58535
+ ` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
58536
+ );
58537
+ lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
58538
+ lines.push(
58539
+ ` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
58540
+ );
58541
+ }
58542
+ logger.info(lines.join("\n"));
58543
+ logger.info(
58544
+ "\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
58545
+ );
58546
+ });
58547
+ cmd.command("enable <id>").description(
58548
+ "opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
58549
+ ).action(async (id, _cmdOptions, command) => {
58550
+ const options = command.optsWithGlobals();
58551
+ const cwd = resolve15(options.workspace ?? process.cwd());
58552
+ const composites = readComposites(cwd);
58553
+ const target = composites.find((c) => c.id === id);
58554
+ if (!target) {
58555
+ logger.error(
58556
+ `Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
58557
+ );
58558
+ process.exit(1);
58559
+ }
58560
+ const cfgPath = resolve15(cwd, "slopbrick.config.mjs");
58561
+ const fs = __require("fs");
58562
+ if (!fs.existsSync(cfgPath)) {
58563
+ logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
58564
+ process.exit(1);
58565
+ }
58566
+ const src = fs.readFileSync(cfgPath, "utf-8");
58567
+ const compositeSnippet = JSON.stringify(
58568
+ {
58569
+ id: target.id,
58570
+ category: "ai",
58571
+ severity: target.severity,
58572
+ aiSpecific: true,
58573
+ description: target.description,
58574
+ defaultOff: true,
58575
+ ruleIds: target.ruleIds,
58576
+ minMatch: target.minMatch,
58577
+ create: "() => ({})",
58578
+ analyze: "() => []"
58579
+ },
58580
+ null,
58581
+ 2
58582
+ );
58583
+ if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
58584
+ logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
58585
+ return;
58586
+ }
58587
+ const updated = src.replace(
58588
+ /(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
58589
+ (_match, head, tail) => `${head} compositeRules: [
58590
+ ${compositeSnippet}
58591
+ ],
58592
+ ${tail}`
58593
+ );
58594
+ fs.writeFileSync(cfgPath, updated, "utf-8");
58595
+ logger.info(`Composite "${id}" enabled.`);
58596
+ logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
58597
+ });
58598
+ cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
58599
+ const options = command.optsWithGlobals();
58600
+ const cwd = resolve15(options.workspace ?? process.cwd());
58601
+ const composites = readComposites(cwd);
58602
+ if (composites.length === 0) {
58603
+ logger.info("No composites registered. Run `slopbrick composite discover` first.");
58604
+ return;
58605
+ }
58606
+ const lines = ["Registered composites:"];
58607
+ for (const e of composites) {
58608
+ lines.push(
58609
+ `
58610
+ ${e.id}
58611
+ ${describeComposite(e.ruleIds, e.minMatch)}
58612
+ members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
58613
+ F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
58614
+ );
58615
+ }
58616
+ logger.info(lines.join("\n"));
58617
+ });
58618
+ }
58619
+
57674
58620
  // src/cli/explain.ts
57675
58621
  var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
57676
58622
  function ruleIdToFilename2(ruleId) {
@@ -57720,152 +58666,7 @@ function formatExplain(result) {
57720
58666
  // src/cli/commands/explain.ts
57721
58667
  init_logger();
57722
58668
  init_builtins();
57723
-
57724
- // src/snippet/data.ts
57725
- var CATEGORY_DIRECTIVES = {
57726
- 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.',
57727
- 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.",
57728
- 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.',
57729
- 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".',
57730
- 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.",
57731
- typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
57732
- 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].",
57733
- component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
57734
- 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.",
57735
- 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'."
57736
- };
57737
- var RULE_HINTS = {
57738
- // v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
57739
- // remaining key matches a rule in src/rules/builtins.ts. No orphans.
57740
- // (v0.16.0 previously moved 35 out-of-scope orphan hints to
57741
- // docs/research/backlog-rule-hints.md
57742
- // for future implementers to paste back when the corresponding rules ship.)
57743
- "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.",
57744
- "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.",
57745
- "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.",
57746
- "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.",
57747
- "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.",
57748
- "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.",
57749
- "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.',
57750
- "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.",
57751
- "visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
57752
- "visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
57753
- "visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
57754
- // v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
57755
- "typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
57756
- "logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
57757
- "logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
57758
- "wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
57759
- "security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
57760
- "security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
57761
- "security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
57762
- "component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
57763
- "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.",
57764
- "component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
57765
- "context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
57766
- "layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
57767
- "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.",
57768
- "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.",
57769
- "layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
57770
- "logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
57771
- "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.)",
57772
- "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.)",
57773
- "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.)",
57774
- "logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
57775
- "logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
57776
- "logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
57777
- "logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
57778
- "logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
57779
- "logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
57780
- "logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
57781
- "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.",
57782
- "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.",
57783
- "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.",
57784
- "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.",
57785
- "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.",
57786
- "perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
57787
- "perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
57788
- "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.',
57789
- "visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
57790
- "visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
57791
- "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.",
57792
- "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.",
57793
- "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.",
57794
- "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%.",
57795
- "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.",
57796
- "wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
57797
- "wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
57798
- "test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
57799
- "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.",
57800
- "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`).",
57801
- "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.",
57802
- "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.",
57803
- // v0.13.0 — AI-specific rules (peer-reviewed signals).
57804
- "ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
57805
- "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).",
57806
- "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).",
57807
- "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).",
57808
- "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).",
57809
- "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).",
57810
- "ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
57811
- "ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
57812
- "ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
57813
- // v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
57814
- // reclassified post-v7 calibration in v0.14.5d)
57815
- "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.",
57816
- "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.",
57817
- "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.",
57818
- "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.",
57819
- "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.",
57820
- "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.",
57821
- // v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
57822
- "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.',
57823
- // v0.17.0 — docs/* rules (markdown drift detection)
57824
- "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.",
57825
- "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.",
57826
- "docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
57827
- // v0.18.9 — Rust-specific rules (tree-sitter-backed)
57828
- "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.",
57829
- "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.",
57830
- "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.",
57831
- "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.',
57832
- // v0.19.0 — TypeScript-specific rules
57833
- "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.",
57834
- "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.",
57835
- "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.",
57836
- "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.",
57837
- // v0.19.0 — Go-specific rules
57838
- "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).',
57839
- "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.",
57840
- // v0.19.0 — Duplication detector
57841
- "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.",
57842
- // v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
57843
- "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.)",
57844
- // v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
57845
- "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.)",
57846
- // v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
57847
- // v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
57848
- // v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
57849
- "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.)",
57850
- "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.)",
57851
- // v0.35.0 — content-based detection (CoCoNUTS-inspired)
57852
- "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.",
57853
- // v0.35.1 — Raidar-inspired content-based detection
57854
- "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)`.',
57855
- // v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
57856
- "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.)",
57857
- "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.)",
57858
- "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.)",
57859
- "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.)",
57860
- "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.)",
57861
- // v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
57862
- "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.)",
57863
- "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.)",
57864
- "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.)",
57865
- "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.)"
57866
- };
57867
-
57868
- // src/cli/commands/explain.ts
58669
+ init_data();
57869
58670
  function registerExplain(program) {
57870
58671
  program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
57871
58672
  const result = explainRule(ruleId, builtinRules, RULE_HINTS);
@@ -57875,7 +58676,7 @@ function registerExplain(program) {
57875
58676
  }
57876
58677
 
57877
58678
  // src/cli/commands/calibration.ts
57878
- import { dirname as dirname15 } from "path";
58679
+ import { dirname as dirname16 } from "path";
57879
58680
  import { fileURLToPath as fileURLToPath2 } from "url";
57880
58681
 
57881
58682
  // src/rules/signal-strength.json
@@ -61468,7 +62269,7 @@ var signal_strength_default2 = {
61468
62269
  };
61469
62270
 
61470
62271
  // src/cli/commands/calibration.ts
61471
- var __dirname = dirname15(fileURLToPath2(import.meta.url));
62272
+ var __dirname = dirname16(fileURLToPath2(import.meta.url));
61472
62273
  function loadData() {
61473
62274
  return signal_strength_default2;
61474
62275
  }
@@ -61591,17 +62392,17 @@ function registerCalibration(program) {
61591
62392
  // src/cli/commands/install.ts
61592
62393
  init_logger();
61593
62394
  init_git();
61594
- import { resolve as resolve15 } from "path";
62395
+ import { resolve as resolve16 } from "path";
61595
62396
 
61596
62397
  // src/cli/installer.ts
61597
62398
  import {
61598
62399
  chmodSync,
61599
- existsSync as existsSync22,
61600
- mkdirSync as mkdirSync10,
61601
- readFileSync as readFileSync26,
61602
- writeFileSync as writeFileSync12
62400
+ existsSync as existsSync24,
62401
+ mkdirSync as mkdirSync11,
62402
+ readFileSync as readFileSync28,
62403
+ writeFileSync as writeFileSync14
61603
62404
  } from "fs";
61604
- import { dirname as dirname16, join as join24 } from "path";
62405
+ import { dirname as dirname17, join as join26 } from "path";
61605
62406
  var BEGIN_SENTINEL = "# slopbrick-hook-begin";
61606
62407
  var END_SENTINEL = "# slopbrick-hook-end";
61607
62408
  var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
@@ -61609,14 +62410,14 @@ npx slopbrick --staged
61609
62410
  ${END_SENTINEL}
61610
62411
  `;
61611
62412
  function hookPath(gitRoot) {
61612
- const huskyDir = join24(gitRoot, ".husky");
61613
- if (existsSync22(huskyDir)) {
61614
- return join24(huskyDir, "pre-commit");
62413
+ const huskyDir = join26(gitRoot, ".husky");
62414
+ if (existsSync24(huskyDir)) {
62415
+ return join26(huskyDir, "pre-commit");
61615
62416
  }
61616
- return join24(gitRoot, ".git", "hooks", "pre-commit");
62417
+ return join26(gitRoot, ".git", "hooks", "pre-commit");
61617
62418
  }
61618
62419
  function readHookContent(path) {
61619
- return readFileSync26(path, "utf8");
62420
+ return readFileSync28(path, "utf8");
61620
62421
  }
61621
62422
  function sentinelsPresent(content) {
61622
62423
  const lines = content.split(/\r?\n/);
@@ -61638,7 +62439,7 @@ function replaceSentinelBlock(content) {
61638
62439
  }
61639
62440
  function installHook(gitRoot) {
61640
62441
  const path = hookPath(gitRoot);
61641
- if (existsSync22(path)) {
62442
+ if (existsSync24(path)) {
61642
62443
  const content = readHookContent(path);
61643
62444
  const { begin, end } = sentinelsPresent(content);
61644
62445
  if (begin && end) {
@@ -61650,7 +62451,7 @@ function installHook(gitRoot) {
61650
62451
  exitCode: 0
61651
62452
  };
61652
62453
  }
61653
- writeFileSync12(path, replaced.endsWith("\n") ? replaced : `${replaced}
62454
+ writeFileSync14(path, replaced.endsWith("\n") ? replaced : `${replaced}
61654
62455
  `);
61655
62456
  chmodSync(path, 493);
61656
62457
  return {
@@ -61670,7 +62471,7 @@ function installHook(gitRoot) {
61670
62471
  }
61671
62472
  const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
61672
62473
  ` : content;
61673
- writeFileSync12(path, `${normalized}${SENTINEL_BLOCK}`);
62474
+ writeFileSync14(path, `${normalized}${SENTINEL_BLOCK}`);
61674
62475
  chmodSync(path, 493);
61675
62476
  return {
61676
62477
  ok: true,
@@ -61678,8 +62479,8 @@ function installHook(gitRoot) {
61678
62479
  exitCode: 0
61679
62480
  };
61680
62481
  }
61681
- mkdirSync10(dirname16(path), { recursive: true });
61682
- writeFileSync12(path, SENTINEL_BLOCK, { mode: 493 });
62482
+ mkdirSync11(dirname17(path), { recursive: true });
62483
+ writeFileSync14(path, SENTINEL_BLOCK, { mode: 493 });
61683
62484
  chmodSync(path, 493);
61684
62485
  return {
61685
62486
  ok: true,
@@ -61689,7 +62490,7 @@ function installHook(gitRoot) {
61689
62490
  }
61690
62491
  function uninstallHook(gitRoot) {
61691
62492
  const path = hookPath(gitRoot);
61692
- if (!existsSync22(path)) {
62493
+ if (!existsSync24(path)) {
61693
62494
  return {
61694
62495
  ok: true,
61695
62496
  message: "Hook not installed",
@@ -61725,7 +62526,7 @@ function uninstallHook(gitRoot) {
61725
62526
  }
61726
62527
  const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
61727
62528
  const result = remaining.join("\n");
61728
- writeFileSync12(path, result.endsWith("\n") ? result : `${result}
62529
+ writeFileSync14(path, result.endsWith("\n") ? result : `${result}
61729
62530
  `);
61730
62531
  return {
61731
62532
  ok: true,
@@ -61738,7 +62539,7 @@ function uninstallHook(gitRoot) {
61738
62539
  function registerInstall(program) {
61739
62540
  program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
61740
62541
  const options = command.optsWithGlobals();
61741
- const cwd = resolve15(options.workspace ?? process.cwd());
62542
+ const cwd = resolve16(options.workspace ?? process.cwd());
61742
62543
  const root = getGitRoot(cwd);
61743
62544
  if (!root) {
61744
62545
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -61755,11 +62556,11 @@ function registerInstall(program) {
61755
62556
  // src/cli/commands/uninstall.ts
61756
62557
  init_logger();
61757
62558
  init_git();
61758
- import { resolve as resolve16 } from "path";
62559
+ import { resolve as resolve17 } from "path";
61759
62560
  function registerUninstall(program) {
61760
62561
  program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
61761
62562
  const options = command.optsWithGlobals();
61762
- const cwd = resolve16(options.workspace ?? process.cwd());
62563
+ const cwd = resolve17(options.workspace ?? process.cwd());
61763
62564
  const root = getGitRoot(cwd);
61764
62565
  if (!root) {
61765
62566
  logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
@@ -61787,8 +62588,8 @@ init_patterns();
61787
62588
  init_architecture_score();
61788
62589
  init_business_logic();
61789
62590
  init_structure_md();
61790
- import { readFileSync as readFileSync27 } from "fs";
61791
- import { resolve as resolve17 } from "path";
62591
+ import { readFileSync as readFileSync29 } from "fs";
62592
+ import { resolve as resolve18 } from "path";
61792
62593
  var TOOL_DEFINITIONS = [
61793
62594
  {
61794
62595
  name: "slop_scan_file",
@@ -62029,10 +62830,10 @@ async function runSuggest(args, ctx, options = {}) {
62029
62830
  function runCheckConstitution(args, ctx) {
62030
62831
  const path = args.path;
62031
62832
  if (!path) return toolError("Missing required argument: path");
62032
- const absPath = resolve17(ctx.cwd, path);
62833
+ const absPath = resolve18(ctx.cwd, path);
62033
62834
  let source;
62034
62835
  try {
62035
- source = readFileSync27(absPath, "utf-8");
62836
+ source = readFileSync29(absPath, "utf-8");
62036
62837
  } catch (err) {
62037
62838
  return toolError(
62038
62839
  `Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -62215,7 +63016,7 @@ function errorResponse(id, code, message, data2) {
62215
63016
  return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
62216
63017
  }
62217
63018
  async function runMcpServer(input, output, cwd) {
62218
- return new Promise((resolve42) => {
63019
+ return new Promise((resolve43) => {
62219
63020
  let buffer = "";
62220
63021
  input.setEncoding("utf-8");
62221
63022
  input.on("data", (chunk) => {
@@ -62253,8 +63054,8 @@ async function runMcpServer(input, output, cwd) {
62253
63054
  nlIdx = buffer.indexOf("\n");
62254
63055
  }
62255
63056
  });
62256
- input.on("end", () => resolve42());
62257
- input.on("close", () => resolve42());
63057
+ input.on("end", () => resolve43());
63058
+ input.on("close", () => resolve43());
62258
63059
  });
62259
63060
  }
62260
63061
 
@@ -62278,7 +63079,7 @@ function registerDoctor(program) {
62278
63079
 
62279
63080
  // src/cli/commands/watch.ts
62280
63081
  init_watch();
62281
- import { resolve as resolve18 } from "path";
63082
+ import { resolve as resolve19 } from "path";
62282
63083
  function registerWatch(program, scanAction) {
62283
63084
  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) => {
62284
63085
  const rawGlobals = command.optsWithGlobals();
@@ -62286,7 +63087,7 @@ function registerWatch(program, scanAction) {
62286
63087
  ...rawGlobals,
62287
63088
  noIncrease: rawGlobals.increase === false
62288
63089
  };
62289
- const cwd = resolve18(options.workspace ?? process.cwd());
63090
+ const cwd = resolve19(options.workspace ?? process.cwd());
62290
63091
  await scanAction([], options, command);
62291
63092
  await watchProject(options, cwd, []);
62292
63093
  });
@@ -62294,11 +63095,11 @@ function registerWatch(program, scanAction) {
62294
63095
 
62295
63096
  // src/cli/commands/lock.ts
62296
63097
  init_logger();
62297
- import { resolve as resolve19 } from "path";
63098
+ import { resolve as resolve20 } from "path";
62298
63099
  function registerLock(program) {
62299
63100
  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(
62300
63101
  (cmdOptions) => {
62301
- const cwd = resolve19(cmdOptions.workspace ?? process.cwd());
63102
+ const cwd = resolve20(cmdOptions.workspace ?? process.cwd());
62302
63103
  if (cmdOptions.uninstall) {
62303
63104
  const result2 = uninstallHook(cwd);
62304
63105
  logger.info(result2.message);
@@ -62321,7 +63122,7 @@ function registerLock(program) {
62321
63122
  // src/cli/commands/ci.ts
62322
63123
  init_logger();
62323
63124
  init_dist();
62324
- import { resolve as resolve20 } from "path";
63125
+ import { resolve as resolve21 } from "path";
62325
63126
  function registerCi(program, scanAction) {
62326
63127
  program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if slopIndex exceeds this number", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
62327
63128
  async (cmdOptions, command) => {
@@ -62334,7 +63135,7 @@ function registerCi(program, scanAction) {
62334
63135
  // scan only changed files
62335
63136
  format: cmdOptions.format ?? "json"
62336
63137
  };
62337
- const cwd = resolve20(options.workspace ?? process.cwd());
63138
+ const cwd = resolve21(options.workspace ?? process.cwd());
62338
63139
  await scanAction([], options, command);
62339
63140
  const health = loadHealth(cwd);
62340
63141
  if (!health) {
@@ -62363,11 +63164,11 @@ function registerCi(program, scanAction) {
62363
63164
 
62364
63165
  // src/cli/commands/memory.ts
62365
63166
  init_logger();
62366
- import { resolve as resolve21 } from "path";
63167
+ import { resolve as resolve22 } from "path";
62367
63168
  function registerMemory(program) {
62368
63169
  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(
62369
63170
  async (cmdOptions) => {
62370
- const cwd = resolve21(cmdOptions.workspace ?? process.cwd());
63171
+ const cwd = resolve22(cmdOptions.workspace ?? process.cwd());
62371
63172
  const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
62372
63173
  const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
62373
63174
  if (cmdOptions.regenerate) {
@@ -62398,48 +63199,48 @@ function registerMemory(program) {
62398
63199
 
62399
63200
  // src/cli/commands/migrate.ts
62400
63201
  init_logger();
62401
- import { resolve as resolve22 } from "path";
63202
+ import { resolve as resolve23 } from "path";
62402
63203
 
62403
63204
  // src/cli/migrate.ts
62404
63205
  init_logger();
62405
- import { existsSync as existsSync23, readFileSync as readFileSync28, renameSync as renameSync4, writeFileSync as writeFileSync13 } from "fs";
62406
- import { join as join25 } from "path";
63206
+ import { existsSync as existsSync25, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync15 } from "fs";
63207
+ import { join as join27 } from "path";
62407
63208
  function planMigration(workspaceDir) {
62408
63209
  const moves = [];
62409
63210
  const rewrites = [];
62410
63211
  const gitignoreEdits = [];
62411
- const oldDir = join25(workspaceDir, ".slop-audit");
62412
- const newDir = join25(workspaceDir, ".slopbrick");
62413
- if (existsSync23(oldDir)) {
63212
+ const oldDir = join27(workspaceDir, ".slop-audit");
63213
+ const newDir = join27(workspaceDir, ".slopbrick");
63214
+ if (existsSync25(oldDir)) {
62414
63215
  moves.push({ from: oldDir, to: newDir, kind: "dir" });
62415
63216
  rewrites.push({
62416
- path: join25(newDir, "inventory.json"),
63217
+ path: join27(newDir, "inventory.json"),
62417
63218
  field: "version",
62418
63219
  from: '"1"',
62419
63220
  to: '"2"'
62420
63221
  });
62421
63222
  rewrites.push({
62422
- path: join25(newDir, "constitution.json"),
63223
+ path: join27(newDir, "constitution.json"),
62423
63224
  field: "version",
62424
63225
  from: '"1"',
62425
63226
  to: '"2"'
62426
63227
  });
62427
63228
  }
62428
- const oldCache = join25(workspaceDir, ".slop-audit-cache.json");
62429
- const newCache = join25(workspaceDir, ".slopbrick-cache.json");
62430
- if (existsSync23(oldCache)) {
63229
+ const oldCache = join27(workspaceDir, ".slop-audit-cache.json");
63230
+ const newCache = join27(workspaceDir, ".slopbrick-cache.json");
63231
+ if (existsSync25(oldCache)) {
62431
63232
  moves.push({ from: oldCache, to: newCache, kind: "file" });
62432
63233
  }
62433
63234
  for (const ext of ["mjs", "cjs", "js"]) {
62434
- const oldCfg = join25(workspaceDir, `slop-audit.config.${ext}`);
62435
- const newCfg = join25(workspaceDir, `slopbrick.config.${ext}`);
62436
- if (existsSync23(oldCfg)) {
63235
+ const oldCfg = join27(workspaceDir, `slop-audit.config.${ext}`);
63236
+ const newCfg = join27(workspaceDir, `slopbrick.config.${ext}`);
63237
+ if (existsSync25(oldCfg)) {
62437
63238
  moves.push({ from: oldCfg, to: newCfg, kind: "config" });
62438
63239
  }
62439
63240
  }
62440
- const gi = join25(workspaceDir, ".gitignore");
62441
- if (existsSync23(gi)) {
62442
- const content = readFileSync28(gi, "utf-8");
63241
+ const gi = join27(workspaceDir, ".gitignore");
63242
+ if (existsSync25(gi)) {
63243
+ const content = readFileSync30(gi, "utf-8");
62443
63244
  if (content.includes(".slop-audit/")) {
62444
63245
  gitignoreEdits.push({
62445
63246
  path: gi,
@@ -62458,7 +63259,7 @@ function planMigration(workspaceDir) {
62458
63259
  return { moves, rewrites, gitignoreEdits };
62459
63260
  }
62460
63261
  function isAlreadyMigrated(workspaceDir) {
62461
- return existsSync23(join25(workspaceDir, ".slopbrick")) && !existsSync23(join25(workspaceDir, ".slop-audit"));
63262
+ return existsSync25(join27(workspaceDir, ".slopbrick")) && !existsSync25(join27(workspaceDir, ".slop-audit"));
62462
63263
  }
62463
63264
  function applyMigration(plan, options = {}) {
62464
63265
  if (options.dryRun) return;
@@ -62466,20 +63267,20 @@ function applyMigration(plan, options = {}) {
62466
63267
  renameSync4(m.from, m.to);
62467
63268
  }
62468
63269
  for (const r of plan.rewrites) {
62469
- if (!existsSync23(r.path)) continue;
62470
- const content = readFileSync28(r.path, "utf-8");
63270
+ if (!existsSync25(r.path)) continue;
63271
+ const content = readFileSync30(r.path, "utf-8");
62471
63272
  const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
62472
- writeFileSync13(r.path, next);
63273
+ writeFileSync15(r.path, next);
62473
63274
  }
62474
63275
  for (const g of plan.gitignoreEdits) {
62475
- const content = readFileSync28(g.path, "utf-8");
63276
+ const content = readFileSync30(g.path, "utf-8");
62476
63277
  const next = content.replaceAll(g.from, g.to);
62477
- writeFileSync13(g.path, next);
63278
+ writeFileSync15(g.path, next);
62478
63279
  }
62479
63280
  }
62480
63281
  function runMigrate(options) {
62481
63282
  const { workspace, dryRun = false, force = false } = options;
62482
- if (!existsSync23(workspace)) {
63283
+ if (!existsSync25(workspace)) {
62483
63284
  return {
62484
63285
  ok: false,
62485
63286
  alreadyMigrated: false,
@@ -62489,9 +63290,9 @@ function runMigrate(options) {
62489
63290
  };
62490
63291
  }
62491
63292
  const alreadyMigrated = isAlreadyMigrated(workspace);
62492
- const newDir = join25(workspace, ".slopbrick");
62493
- const oldDir = join25(workspace, ".slop-audit");
62494
- if (existsSync23(newDir) && existsSync23(oldDir) && !force) {
63293
+ const newDir = join27(workspace, ".slopbrick");
63294
+ const oldDir = join27(workspace, ".slop-audit");
63295
+ if (existsSync25(newDir) && existsSync25(oldDir) && !force) {
62495
63296
  return {
62496
63297
  ok: false,
62497
63298
  alreadyMigrated: false,
@@ -62567,7 +63368,7 @@ function registerMigrate(program) {
62567
63368
  (cmdOptions, command) => {
62568
63369
  const globals = command.optsWithGlobals();
62569
63370
  const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
62570
- const cwd = resolve22(cmdOptions.workspace ?? process.cwd());
63371
+ const cwd = resolve23(cmdOptions.workspace ?? process.cwd());
62571
63372
  const result = runMigrate({
62572
63373
  workspace: cwd,
62573
63374
  dryRun: cmdOptions.dryRun,
@@ -62673,12 +63474,12 @@ function registerRules(program) {
62673
63474
  // src/cli/commands/validate-config.ts
62674
63475
  init_logger();
62675
63476
  init_validation();
62676
- import { existsSync as existsSync24 } from "fs";
62677
- import { extname as extname8, resolve as resolve23 } from "path";
63477
+ import { existsSync as existsSync26 } from "fs";
63478
+ import { extname as extname8, resolve as resolve24 } from "path";
62678
63479
  function registerValidateConfig(program) {
62679
63480
  program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
62680
- const path = configPath ? resolve23(configPath) : resolve23(process.cwd(), "slopbrick.config.mjs");
62681
- if (!existsSync24(path)) {
63481
+ const path = configPath ? resolve24(configPath) : resolve24(process.cwd(), "slopbrick.config.mjs");
63482
+ if (!existsSync26(path)) {
62682
63483
  logger.error(`Error: config file not found: ${path}`);
62683
63484
  process.exit(2);
62684
63485
  }
@@ -62904,8 +63705,8 @@ ${formatMarkdown(result.report)}`);
62904
63705
 
62905
63706
  // src/cli/commands/calibrate.ts
62906
63707
  init_logger();
62907
- import { mkdirSync as mkdirSync13, writeFileSync as writeFileSync16 } from "fs";
62908
- import { dirname as dirname18, resolve as resolve25 } from "path";
63708
+ import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync18 } from "fs";
63709
+ import { dirname as dirname19, resolve as resolve26 } from "path";
62909
63710
 
62910
63711
  // src/research/providers/openai.ts
62911
63712
  var OpenAiProvider = class {
@@ -62960,8 +63761,8 @@ function createProvider(config) {
62960
63761
  }
62961
63762
 
62962
63763
  // src/research/generator.ts
62963
- import { mkdirSync as mkdirSync11, writeFileSync as writeFileSync14 } from "fs";
62964
- import { join as join26 } from "path";
63764
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync16 } from "fs";
63765
+ import { join as join28 } from "path";
62965
63766
 
62966
63767
  // src/research/prompts.ts
62967
63768
  var DEFAULT_PROMPT_TEMPLATES = [
@@ -63024,14 +63825,14 @@ async function generateSamples(options) {
63024
63825
  }
63025
63826
  const samples = [];
63026
63827
  const ext = extForFramework(framework);
63027
- const dir = join26(outputDir, framework, componentType);
63028
- mkdirSync11(dir, { recursive: true });
63828
+ const dir = join28(outputDir, framework, componentType);
63829
+ mkdirSync12(dir, { recursive: true });
63029
63830
  for (let i = 1; i <= count; i += 1) {
63030
63831
  const raw = await provider.generateSample(renderPrompt(template), { temperature });
63031
63832
  const code = extractCodeFromMarkdown(raw);
63032
63833
  const fileName = `sample-${i}${ext}`;
63033
- const filePath = join26(dir, fileName);
63034
- writeFileSync14(filePath, code, "utf8");
63834
+ const filePath = join28(dir, fileName);
63835
+ writeFileSync16(filePath, code, "utf8");
63035
63836
  const sample = {
63036
63837
  filePath,
63037
63838
  framework,
@@ -63042,8 +63843,8 @@ async function generateSamples(options) {
63042
63843
  };
63043
63844
  samples.push(sample);
63044
63845
  }
63045
- const metadataPath = join26(dir, "metadata.json");
63046
- writeFileSync14(metadataPath, JSON.stringify(samples, null, 2), "utf8");
63846
+ const metadataPath = join28(dir, "metadata.json");
63847
+ writeFileSync16(metadataPath, JSON.stringify(samples, null, 2), "utf8");
63047
63848
  return samples;
63048
63849
  }
63049
63850
 
@@ -63249,46 +64050,46 @@ function slugify(value) {
63249
64050
 
63250
64051
  // src/research/calibrator.ts
63251
64052
  import { execFileSync as execFileSync2 } from "child_process";
63252
- import { existsSync as existsSync25, readFileSync as readFileSync29, writeFileSync as writeFileSync15, mkdirSync as mkdirSync12 } from "fs";
63253
- import { join as join28, resolve as resolve24 } from "path";
64053
+ import { existsSync as existsSync27, readFileSync as readFileSync31, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
64054
+ import { join as join30, resolve as resolve25 } from "path";
63254
64055
 
63255
64056
  // src/corpus-paths.ts
63256
- import { join as join27 } from "path";
64057
+ import { join as join29 } from "path";
63257
64058
  var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
63258
- var POSITIVE_DIR = join27(CORPUS_ROOT, "positive");
63259
- var NEGATIVE_DIR = join27(CORPUS_ROOT, "negative");
63260
- var FILELISTS_DIR = join27(CORPUS_ROOT, "filelists");
64059
+ var POSITIVE_DIR = join29(CORPUS_ROOT, "positive");
64060
+ var NEGATIVE_DIR = join29(CORPUS_ROOT, "negative");
64061
+ var FILELISTS_DIR = join29(CORPUS_ROOT, "filelists");
63261
64062
 
63262
64063
  // src/research/calibrator.ts
63263
64064
  var DEFAULT_POSITIVE = POSITIVE_DIR;
63264
64065
  var DEFAULT_NEGATIVE = NEGATIVE_DIR;
63265
64066
  function buildFileList(dir, extensions) {
63266
- const tmpList = join28("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
64067
+ const tmpList = join30("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
63267
64068
  const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
63268
64069
  execFileSync2("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
63269
- const out = readFileSync29(tmpList, "utf8");
64070
+ const out = readFileSync31(tmpList, "utf8");
63270
64071
  execFileSync2("rm", ["-f", tmpList]);
63271
64072
  return out.trim().split("\n").filter(Boolean);
63272
64073
  }
63273
64074
  function runScan2(fileListPath) {
63274
- const files = readFileSync29(fileListPath, "utf8").trim().split("\n").filter(Boolean);
64075
+ const files = readFileSync31(fileListPath, "utf8").trim().split("\n").filter(Boolean);
63275
64076
  const CHUNK = 600;
63276
64077
  const ruleFires = /* @__PURE__ */ new Map();
63277
64078
  const uniqueFilesPerRule = /* @__PURE__ */ new Map();
63278
64079
  let fileCount = 0;
63279
- const tmpOut = join28("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
64080
+ const tmpOut = join30("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
63280
64081
  for (let i = 0; i < files.length; i += CHUNK) {
63281
64082
  const chunk = files.slice(i, i + CHUNK);
63282
64083
  try {
63283
64084
  execFileSync2(
63284
64085
  "node",
63285
- [join28(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
64086
+ [join30(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
63286
64087
  { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
63287
64088
  );
63288
64089
  } catch {
63289
64090
  }
63290
- if (!existsSync25(tmpOut)) continue;
63291
- const report = JSON.parse(readFileSync29(tmpOut, "utf8"));
64091
+ if (!existsSync27(tmpOut)) continue;
64092
+ const report = JSON.parse(readFileSync31(tmpOut, "utf8"));
63292
64093
  fileCount += report.fileCount;
63293
64094
  for (const issue of report.issues) {
63294
64095
  ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
@@ -63316,16 +64117,16 @@ function classify(posFiles, negFiles) {
63316
64117
  async function calibrate(cwd, options = {}) {
63317
64118
  const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
63318
64119
  const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
63319
- if (!existsSync25(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
63320
- if (!existsSync25(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
64120
+ if (!existsSync27(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
64121
+ if (!existsSync27(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
63321
64122
  const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
63322
64123
  const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
63323
64124
  const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
63324
64125
  const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
63325
- const posListPath = join28("/tmp", `cal-pos-${Date.now()}.txt`);
63326
- const negListPath = join28("/tmp", `cal-neg-${Date.now()}.txt`);
63327
- writeFileSync15(posListPath, posSample.join("\n"));
63328
- writeFileSync15(negListPath, negSample.join("\n"));
64126
+ const posListPath = join30("/tmp", `cal-pos-${Date.now()}.txt`);
64127
+ const negListPath = join30("/tmp", `cal-neg-${Date.now()}.txt`);
64128
+ writeFileSync17(posListPath, posSample.join("\n"));
64129
+ writeFileSync17(negListPath, negSample.join("\n"));
63329
64130
  const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
63330
64131
  const builtinRules2 = builtins.builtinRules ?? [];
63331
64132
  const metaById = /* @__PURE__ */ new Map();
@@ -63439,9 +64240,9 @@ function registerCalibrate(program) {
63439
64240
  positiveLimit: cmdOptions.positiveLimit,
63440
64241
  negativeLimit: cmdOptions.negativeLimit
63441
64242
  });
63442
- const outputPath = cmdOptions.output ? resolve25(cwd, cmdOptions.output) : resolve25(cwd, "corpus", "calibration-empirical.md");
63443
- mkdirSync13(dirname18(outputPath), { recursive: true });
63444
- writeFileSync16(outputPath, reportToMarkdown(report), "utf8");
64243
+ const outputPath = cmdOptions.output ? resolve26(cwd, cmdOptions.output) : resolve26(cwd, "corpus", "calibration-empirical.md");
64244
+ mkdirSync14(dirname19(outputPath), { recursive: true });
64245
+ writeFileSync18(outputPath, reportToMarkdown(report), "utf8");
63445
64246
  logger.info(
63446
64247
  "Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
63447
64248
  );
@@ -63580,13 +64381,13 @@ function registerTrend(program) {
63580
64381
  // src/cli/commands/drift.ts
63581
64382
  init_logger();
63582
64383
  init_scan2();
63583
- import { resolve as resolve26 } from "path";
64384
+ import { resolve as resolve27 } from "path";
63584
64385
 
63585
64386
  // src/cli/drift.ts
63586
64387
  init_discover();
63587
64388
  init_patterns();
63588
64389
  init_telemetry();
63589
- import { readFileSync as readFileSync30 } from "fs";
64390
+ import { readFileSync as readFileSync32 } from "fs";
63590
64391
  import { basename as basename4, relative as relative16 } from "path";
63591
64392
  async function runDrift(cwd, config, options = {}) {
63592
64393
  const maxFiles = options.maxFiles ?? 1e3;
@@ -63598,7 +64399,7 @@ async function runDrift(cwd, config, options = {}) {
63598
64399
  for (const absPath of limited) {
63599
64400
  let source;
63600
64401
  try {
63601
- source = readFileSync30(absPath, "utf-8");
64402
+ source = readFileSync32(absPath, "utf-8");
63602
64403
  } catch {
63603
64404
  continue;
63604
64405
  }
@@ -63885,7 +64686,7 @@ function registerDrift(program) {
63885
64686
  const options = command.optsWithGlobals();
63886
64687
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
63887
64688
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
63888
- const cwd = resolve26(options.workspace ?? process.cwd());
64689
+ const cwd = resolve27(options.workspace ?? process.cwd());
63889
64690
  if (cmdOptions.temporalSince !== void 0) {
63890
64691
  const temporalSince = cmdOptions.temporalSince.trim();
63891
64692
  if (temporalSince.length === 0) {
@@ -63913,7 +64714,7 @@ function registerDrift(program) {
63913
64714
  // src/cli/commands/pr.ts
63914
64715
  init_logger();
63915
64716
  init_scan2();
63916
- import { resolve as resolve28 } from "path";
64717
+ import { resolve as resolve29 } from "path";
63917
64718
 
63918
64719
  // src/cli/pr.ts
63919
64720
  init_git();
@@ -63921,8 +64722,8 @@ init_worker();
63921
64722
  init_metrics();
63922
64723
  init_patterns();
63923
64724
  init_discover();
63924
- import { readFileSync as readFileSync31 } from "fs";
63925
- import { extname as extname9, relative as relative17, resolve as resolve27 } from "path";
64725
+ import { readFileSync as readFileSync33 } from "fs";
64726
+ import { extname as extname9, relative as relative17, resolve as resolve28 } from "path";
63926
64727
  import { execFile as execFileCb } from "child_process";
63927
64728
  import { promisify as promisify2 } from "util";
63928
64729
  import { minimatch as minimatch3 } from "minimatch";
@@ -63963,7 +64764,7 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
63963
64764
  if (gitFiles.length === 0) return [];
63964
64765
  const sourceFiles = [];
63965
64766
  for (const relOrAbs of gitFiles) {
63966
- const abs = resolve27(cwd, relOrAbs);
64767
+ const abs = resolve28(cwd, relOrAbs);
63967
64768
  const ext = extname9(abs).toLowerCase();
63968
64769
  if (!PR_EXTENSIONS.has(ext)) continue;
63969
64770
  const rel = relative17(cwd, abs).split("\\").join("/");
@@ -63994,7 +64795,7 @@ async function runPrScan(cwd, config, options = {}) {
63994
64795
  for (const absPath of candidates) {
63995
64796
  let source;
63996
64797
  try {
63997
- source = readFileSync31(absPath, "utf-8");
64798
+ source = readFileSync33(absPath, "utf-8");
63998
64799
  } catch {
63999
64800
  continue;
64000
64801
  }
@@ -64169,20 +64970,135 @@ function formatPrMarkdown(result) {
64169
64970
  function prExitCode(result) {
64170
64971
  return result.totalScore > result.threshold ? 1 : 0;
64171
64972
  }
64973
+ function parseUnifiedDiffPaths(diff) {
64974
+ const paths = [];
64975
+ const seen = /* @__PURE__ */ new Set();
64976
+ for (const line of diff.split(/\r?\n/)) {
64977
+ const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
64978
+ if (!m) continue;
64979
+ const path = m[1];
64980
+ if (path === "/dev/null" || path === "dev/null") continue;
64981
+ if (seen.has(path)) continue;
64982
+ seen.add(path);
64983
+ paths.push(path);
64984
+ }
64985
+ return paths;
64986
+ }
64987
+ async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
64988
+ const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
64989
+ const maxFiles = options.maxFiles ?? 500;
64990
+ const { existsSync: existsSync32, readFileSync: readFileSync42 } = await import("fs");
64991
+ const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
64992
+ if (!existsSync32(diffPath)) {
64993
+ throw new Error(`Diff file not found: ${diffPath}`);
64994
+ }
64995
+ const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
64996
+ const diff = readFileSync42(diffAbs, "utf-8");
64997
+ const changedPaths = parseUnifiedDiffPaths(diff);
64998
+ if (changedPaths.length === 0) {
64999
+ return emptyPrResult(threshold);
65000
+ }
65001
+ return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
65002
+ }
65003
+ async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
65004
+ const { existsSync: existsSync32, statSync: statSync8 } = await import("fs");
65005
+ const { resolve: resolvePath } = await import("path");
65006
+ const { readFile: readFile4 } = await import("fs/promises");
65007
+ const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
65008
+ const limited = paths.slice(0, maxFiles);
65009
+ const files = [];
65010
+ let totalScore = 0;
65011
+ const byCategory = {};
65012
+ const bySeverity = { low: 0, medium: 0, high: 0 };
65013
+ let constitutionViolations = 0;
65014
+ for (const rel of limited) {
65015
+ const abs = resolvePath(cwd, rel);
65016
+ if (!existsSync32(abs) || !statSync8(abs).isFile()) continue;
65017
+ let source;
65018
+ try {
65019
+ source = await readFile4(abs, "utf-8");
65020
+ } catch {
65021
+ continue;
65022
+ }
65023
+ let result;
65024
+ try {
65025
+ result = await scanFile2(abs, config, void 0, cwd);
65026
+ } catch {
65027
+ continue;
65028
+ }
65029
+ if (result.parseError) continue;
65030
+ const issues = result.issues;
65031
+ let slopPoints = 0;
65032
+ const issueEntries = [];
65033
+ for (const issue of issues) {
65034
+ slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
65035
+ byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
65036
+ bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
65037
+ issueEntries.push({
65038
+ ruleId: issue.ruleId,
65039
+ severity: issue.severity,
65040
+ line: issue.line,
65041
+ category: issue.category,
65042
+ message: issue.message
65043
+ });
65044
+ }
65045
+ totalScore += slopPoints;
65046
+ files.push({
65047
+ file: abs,
65048
+ relPath: rel,
65049
+ score: slopPoints,
65050
+ issueCount: issueEntries.length,
65051
+ constitutionViolationCount: 0,
65052
+ slopPoints,
65053
+ issues: issueEntries,
65054
+ constitutionViolations: []
65055
+ });
65056
+ }
65057
+ return {
65058
+ base: "diff",
65059
+ head: "diff",
65060
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65061
+ passed: totalScore <= threshold,
65062
+ threshold,
65063
+ totalScore,
65064
+ filesChanged: files.length,
65065
+ byCategory,
65066
+ bySeverity,
65067
+ files
65068
+ };
65069
+ }
65070
+ function emptyPrResult(threshold) {
65071
+ return {
65072
+ base: "diff",
65073
+ head: "diff",
65074
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65075
+ passed: true,
65076
+ threshold,
65077
+ totalScore: 0,
65078
+ filesChanged: 0,
65079
+ byCategory: {},
65080
+ bySeverity: { low: 0, medium: 0, high: 0 },
65081
+ files: []
65082
+ };
65083
+ }
64172
65084
 
64173
65085
  // src/cli/commands/pr.ts
64174
65086
  function registerPr(program) {
64175
65087
  program.command("pr").description(
64176
65088
  "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)."
64177
- ).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(
65089
+ ).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(
64178
65090
  async (cmdOptions, command) => {
64179
65091
  try {
64180
65092
  const options = command.optsWithGlobals();
64181
- const cwd = resolve28(options.workspace ?? process.cwd());
65093
+ const cwd = resolve29(options.workspace ?? process.cwd());
64182
65094
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64183
65095
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64184
65096
  const { config } = await runScan({ ...options, workspace: cwd });
64185
- const result = await runPrScan(cwd, config, {
65097
+ const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
65098
+ format,
65099
+ threshold: cmdOptions.threshold,
65100
+ maxFiles: cmdOptions.maxFiles
65101
+ }) : await runPrScan(cwd, config, {
64186
65102
  base: cmdOptions.base,
64187
65103
  head: cmdOptions.head,
64188
65104
  format,
@@ -64203,7 +65119,7 @@ function registerPr(program) {
64203
65119
  init_logger();
64204
65120
  init_scan2();
64205
65121
  init_ai_security_risk();
64206
- import { resolve as resolve29 } from "path";
65122
+ import { resolve as resolve30 } from "path";
64207
65123
  function registerSecurity(program) {
64208
65124
  program.command("security").description(
64209
65125
  "AI Security Risk \u2014 categorical severity for security findings disproportionately introduced by AI-generated code"
@@ -64213,7 +65129,7 @@ function registerSecurity(program) {
64213
65129
  const options = command.optsWithGlobals();
64214
65130
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64215
65131
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64216
- const cwd = resolve29(options.workspace ?? process.cwd());
65132
+ const cwd = resolve30(options.workspace ?? process.cwd());
64217
65133
  const { report } = await runScan({ ...options, workspace: cwd });
64218
65134
  const securityIssues = report.issues.filter((i) => i.category === "security");
64219
65135
  const { risk, findings } = computeAiSecurityRisk(securityIssues);
@@ -64261,13 +65177,13 @@ function registerSecurity(program) {
64261
65177
  // src/cli/commands/test.ts
64262
65178
  init_logger();
64263
65179
  init_scan2();
64264
- import { resolve as resolve31 } from "path";
65180
+ import { resolve as resolve32 } from "path";
64265
65181
 
64266
65182
  // src/cli/test.ts
64267
65183
  init_scan2();
64268
65184
  init_test_quality();
64269
65185
  init_logger();
64270
- import { resolve as resolve30 } from "path";
65186
+ import { resolve as resolve31 } from "path";
64271
65187
  var TEST_INCLUDE_GLOBS = [
64272
65188
  "**/*.test.{ts,tsx,js,jsx}",
64273
65189
  "**/*.spec.{ts,tsx,js,jsx}",
@@ -64335,7 +65251,7 @@ function formatTestReport(result, opts = {}) {
64335
65251
  const sortedFiles = [...byFile.keys()].sort();
64336
65252
  for (const file of sortedFiles) {
64337
65253
  const issues = byFile.get(file) ?? [];
64338
- const rel = file.startsWith(resolve30(process.cwd())) ? file : file;
65254
+ const rel = file.startsWith(resolve31(process.cwd())) ? file : file;
64339
65255
  lines.push("");
64340
65256
  lines.push(` ${rel}`);
64341
65257
  for (const issue of issues.slice(0, 20)) {
@@ -64364,7 +65280,7 @@ function registerTest(program) {
64364
65280
  const options = command.optsWithGlobals();
64365
65281
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64366
65282
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64367
- const cwd = resolve31(options.workspace ?? process.cwd());
65283
+ const cwd = resolve32(options.workspace ?? process.cwd());
64368
65284
  const { config } = await runScan({ ...options, workspace: cwd });
64369
65285
  const { result } = await runTestScan(cwd, config, { strict: options.strict });
64370
65286
  logger.info(formatTestReport(result, { json: format === "json" }));
@@ -64381,7 +65297,7 @@ function registerTest(program) {
64381
65297
  init_logger();
64382
65298
  init_scan2();
64383
65299
  init_architecture_score();
64384
- import { resolve as resolve32 } from "path";
65300
+ import { resolve as resolve33 } from "path";
64385
65301
  function registerArchitecture(program) {
64386
65302
  program.command("architecture").description(
64387
65303
  "compute the Architecture Consistency Score (0-100) \u2014 one number for cross-file pattern duplication"
@@ -64391,7 +65307,7 @@ function registerArchitecture(program) {
64391
65307
  const options = command.optsWithGlobals();
64392
65308
  const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
64393
65309
  const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
64394
- const cwd = resolve32(options.workspace ?? process.cwd());
65310
+ const cwd = resolve33(options.workspace ?? process.cwd());
64395
65311
  const { config } = await runScan({ ...options, workspace: cwd });
64396
65312
  const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
64397
65313
  const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
@@ -64408,12 +65324,12 @@ function registerArchitecture(program) {
64408
65324
  // src/cli/commands/business-logic.ts
64409
65325
  init_logger();
64410
65326
  init_scan2();
64411
- import { resolve as resolve33 } from "path";
65327
+ import { resolve as resolve34 } from "path";
64412
65328
 
64413
65329
  // src/cli/business-logic.ts
64414
65330
  init_discover();
64415
65331
  init_business_logic();
64416
- import { readFileSync as readFileSync32 } from "fs";
65332
+ import { readFileSync as readFileSync34 } from "fs";
64417
65333
  import { relative as relative18 } from "path";
64418
65334
  async function runBusinessLogicScan(cwd, config, options = {}) {
64419
65335
  const maxFiles = options.maxFiles ?? 500;
@@ -64424,7 +65340,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
64424
65340
  for (const absPath of limited) {
64425
65341
  let source;
64426
65342
  try {
64427
- source = readFileSync32(absPath, "utf-8");
65343
+ source = readFileSync34(absPath, "utf-8");
64428
65344
  } catch {
64429
65345
  continue;
64430
65346
  }
@@ -64536,7 +65452,7 @@ function registerBusinessLogic(program) {
64536
65452
  const options = command.optsWithGlobals();
64537
65453
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64538
65454
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64539
- const cwd = resolve33(options.workspace ?? process.cwd());
65455
+ const cwd = resolve34(options.workspace ?? process.cwd());
64540
65456
  const { config } = await runScan({ ...options, workspace: cwd });
64541
65457
  const result = await runBusinessLogicScan(cwd, config, {
64542
65458
  maxFiles: cmdOptions.maxFiles
@@ -64554,7 +65470,7 @@ function registerBusinessLogic(program) {
64554
65470
  // src/cli/commands/maintenance-cost.ts
64555
65471
  init_logger();
64556
65472
  init_scan2();
64557
- import { resolve as resolve34 } from "path";
65473
+ import { resolve as resolve35 } from "path";
64558
65474
 
64559
65475
  // src/cli/maintenance-cost.ts
64560
65476
  init_scan2();
@@ -64678,7 +65594,7 @@ function registerMaintenanceCost(program) {
64678
65594
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64679
65595
  const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
64680
65596
  const strict = options.strict ?? cmdOptions.strict ?? false;
64681
- const cwd = resolve34(options.workspace ?? process.cwd());
65597
+ const cwd = resolve35(options.workspace ?? process.cwd());
64682
65598
  const { config } = await runScan({ ...options, workspace: cwd });
64683
65599
  const result = await runMaintenanceCostScan(cwd, config, {
64684
65600
  maxFiles: cmdOptions.maxFiles,
@@ -64697,7 +65613,7 @@ function registerMaintenanceCost(program) {
64697
65613
  // src/cli/commands/docs.ts
64698
65614
  init_logger();
64699
65615
  init_scan2();
64700
- import { resolve as resolve35 } from "path";
65616
+ import { resolve as resolve36 } from "path";
64701
65617
 
64702
65618
  // src/cli/docs.ts
64703
65619
  init_scan2();
@@ -64822,7 +65738,7 @@ function registerDocs(program) {
64822
65738
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64823
65739
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64824
65740
  const strict = options.strict ?? cmdOptions.strict ?? false;
64825
- const cwd = resolve35(options.workspace ?? process.cwd());
65741
+ const cwd = resolve36(options.workspace ?? process.cwd());
64826
65742
  const { config } = await runScan({ ...options, workspace: cwd });
64827
65743
  const result = await runDocsScan(cwd, config, {
64828
65744
  maxDocFiles: cmdOptions.maxFiles,
@@ -64846,7 +65762,7 @@ function registerDocs(program) {
64846
65762
  // src/cli/commands/db.ts
64847
65763
  init_logger();
64848
65764
  init_scan2();
64849
- import { resolve as resolve36 } from "path";
65765
+ import { resolve as resolve37 } from "path";
64850
65766
 
64851
65767
  // src/cli/db.ts
64852
65768
  init_scan2();
@@ -64965,7 +65881,7 @@ function registerDb(program) {
64965
65881
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
64966
65882
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
64967
65883
  const strict = options.strict ?? cmdOptions.strict ?? false;
64968
- const cwd = resolve36(options.workspace ?? process.cwd());
65884
+ const cwd = resolve37(options.workspace ?? process.cwd());
64969
65885
  const { config } = await runScan({ ...options, workspace: cwd });
64970
65886
  const result = await runDbScan(cwd, config, {
64971
65887
  maxFiles: cmdOptions.maxFiles,
@@ -64989,12 +65905,12 @@ function registerDb(program) {
64989
65905
  // src/cli/commands/patterns.ts
64990
65906
  init_logger();
64991
65907
  init_scan2();
64992
- import { resolve as resolve37 } from "path";
65908
+ import { resolve as resolve38 } from "path";
64993
65909
 
64994
65910
  // src/engine/patterns.ts
64995
65911
  init_patterns();
64996
65912
  init_discover();
64997
- import { readFileSync as readFileSync33 } from "fs";
65913
+ import { readFileSync as readFileSync35 } from "fs";
64998
65914
  import { basename as basename5, relative as relative19 } from "path";
64999
65915
  var PATTERN_CATEGORIES = [
65000
65916
  "modal",
@@ -65097,7 +66013,7 @@ function detectFormsFromFiles(files) {
65097
66013
  for (const f of files) {
65098
66014
  let source;
65099
66015
  try {
65100
- source = readFileSync33(f, "utf-8");
66016
+ source = readFileSync35(f, "utf-8");
65101
66017
  } catch {
65102
66018
  continue;
65103
66019
  }
@@ -65318,7 +66234,7 @@ function registerPatterns(program) {
65318
66234
  const options = command.optsWithGlobals();
65319
66235
  const rawFormat = options.format ?? cmdOptions.format ?? "text";
65320
66236
  const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
65321
- const cwd = resolve37(options.workspace ?? process.cwd());
66237
+ const cwd = resolve38(options.workspace ?? process.cwd());
65322
66238
  const { config } = await runScan({ ...options, workspace: cwd });
65323
66239
  const result = await runPatternsScan(cwd, config, {
65324
66240
  maxFiles: cmdOptions.maxFiles,
@@ -65337,8 +66253,8 @@ function registerPatterns(program) {
65337
66253
  // src/cli/commands/research.ts
65338
66254
  init_logger();
65339
66255
  init_config2();
65340
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, readFileSync as readFileSync34, writeFileSync as writeFileSync17 } from "fs";
65341
- import { dirname as dirname19, resolve as resolve38 } from "path";
66256
+ import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "fs";
66257
+ import { dirname as dirname20, resolve as resolve39 } from "path";
65342
66258
  function registerResearch(program) {
65343
66259
  const research = program.command("research").description("research commands for the AI UI learning loop");
65344
66260
  research.command("generate").description("generate synthetic UI samples").requiredOption("--count <n>", "number of samples", parseCount).requiredOption("--framework <name>", "target framework").requiredOption("--component-type <type>", "component type").requiredOption("--provider <name>", "AI provider (openai)").option("--api-key <key>", "API key for provider").option("--model <name>", "model name").option("--temperature <n>", "sampling temperature", parseFloat, 0.7).option("--output-dir <path>", "output directory", ".slopbrick/corpus/generated").action(async (cmdOptions) => {
@@ -65353,24 +66269,24 @@ function registerResearch(program) {
65353
66269
  framework: cmdOptions.framework,
65354
66270
  componentType: cmdOptions.componentType,
65355
66271
  provider,
65356
- outputDir: resolve38(cmdOptions.outputDir),
66272
+ outputDir: resolve39(cmdOptions.outputDir),
65357
66273
  temperature: cmdOptions.temperature
65358
66274
  });
65359
66275
  logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
65360
66276
  });
65361
66277
  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) => {
65362
66278
  try {
65363
- const metadataPath = resolve38(cmdOptions.inputDir, "metadata.json");
65364
- if (!existsSync26(metadataPath)) {
66279
+ const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
66280
+ if (!existsSync28(metadataPath)) {
65365
66281
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
65366
66282
  process.exit(2);
65367
66283
  }
65368
- const samples = JSON.parse(readFileSync34(metadataPath, "utf8"));
66284
+ const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
65369
66285
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
65370
66286
  const analysis = await analyzeSamples(samples, config);
65371
- const outputPath = resolve38(cmdOptions.output);
65372
- mkdirSync14(dirname19(outputPath), { recursive: true });
65373
- writeFileSync17(outputPath, JSON.stringify(analysis, null, 2), "utf8");
66287
+ const outputPath = resolve39(cmdOptions.output);
66288
+ mkdirSync15(dirname20(outputPath), { recursive: true });
66289
+ writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
65374
66290
  logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
65375
66291
  logger.info(`Wrote analysis to ${outputPath}`);
65376
66292
  } catch (error) {
@@ -65380,12 +66296,12 @@ function registerResearch(program) {
65380
66296
  });
65381
66297
  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) => {
65382
66298
  try {
65383
- const metadataPath = resolve38(cmdOptions.inputDir, "metadata.json");
65384
- if (!existsSync26(metadataPath)) {
66299
+ const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
66300
+ if (!existsSync28(metadataPath)) {
65385
66301
  logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
65386
66302
  process.exit(2);
65387
66303
  }
65388
- const samples = JSON.parse(readFileSync34(metadataPath, "utf8"));
66304
+ const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
65389
66305
  const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
65390
66306
  const analysis = await analyzeSamples(samples, config);
65391
66307
  const extraction = extractAndCluster(analysis.samples, {
@@ -65395,8 +66311,8 @@ function registerResearch(program) {
65395
66311
  const candidates = clustersToCandidates(extraction.clusters, {
65396
66312
  minFrequency: cmdOptions.minFrequency
65397
66313
  });
65398
- const outputPath = resolve38(cmdOptions.output);
65399
- mkdirSync14(dirname19(outputPath), { recursive: true });
66314
+ const outputPath = resolve39(cmdOptions.output);
66315
+ mkdirSync15(dirname20(outputPath), { recursive: true });
65400
66316
  const payload = {
65401
66317
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
65402
66318
  sampleCount: analysis.summary.total,
@@ -65404,7 +66320,7 @@ function registerResearch(program) {
65404
66320
  fingerprintCount: extraction.total,
65405
66321
  candidates
65406
66322
  };
65407
- writeFileSync17(outputPath, JSON.stringify(payload, null, 2), "utf8");
66323
+ writeFileSync19(outputPath, JSON.stringify(payload, null, 2), "utf8");
65408
66324
  logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
65409
66325
  logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
65410
66326
  } catch (error) {
@@ -65417,8 +66333,8 @@ function registerResearch(program) {
65417
66333
  // src/cli/commands/init.ts
65418
66334
  init_logger();
65419
66335
  init_builtins();
65420
- import { existsSync as existsSync27, mkdirSync as mkdirSync15, readFileSync as readFileSync35, writeFileSync as writeFileSync18 } from "fs";
65421
- import { dirname as dirname20, join as join30, resolve as resolve39 } from "path";
66336
+ import { existsSync as existsSync29, mkdirSync as mkdirSync16, readFileSync as readFileSync37, writeFileSync as writeFileSync20 } from "fs";
66337
+ import { dirname as dirname21, join as join32, resolve as resolve40 } from "path";
65422
66338
  init_scan2();
65423
66339
  init_config2();
65424
66340
  init_threshold();
@@ -65426,145 +66342,9 @@ init_git();
65426
66342
  init_cache();
65427
66343
 
65428
66344
  // src/snippet/targets.ts
65429
- import { join as join29 } from "path";
65430
-
65431
- // src/snippet/render.ts
65432
- function aiSpecificRules(rules) {
65433
- return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
65434
- }
65435
- function categorizeRules(rules) {
65436
- const byCategory = /* @__PURE__ */ new Map();
65437
- for (const r of rules) {
65438
- const list = byCategory.get(r.category) ?? [];
65439
- list.push(r);
65440
- byCategory.set(r.category, list);
65441
- }
65442
- for (const list of byCategory.values()) {
65443
- list.sort((a, b) => a.id.localeCompare(b.id));
65444
- }
65445
- return byCategory;
65446
- }
65447
- function baseContent(rules, toolLabel, intro) {
65448
- const lines = [];
65449
- lines.push("# " + toolLabel);
65450
- lines.push("");
65451
- lines.push(intro);
65452
- lines.push("");
65453
- const ai = aiSpecificRules(rules);
65454
- const byCategory = categorizeRules(ai);
65455
- lines.push("## Category-level directives");
65456
- lines.push("");
65457
- for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
65458
- lines.push("- **" + category + "**: " + directive);
65459
- }
65460
- lines.push("");
65461
- lines.push("## Per-rule directives");
65462
- lines.push("");
65463
- for (const [category, list] of byCategory) {
65464
- lines.push("### " + category);
65465
- lines.push("");
65466
- for (const rule of list) {
65467
- const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
65468
- lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
65469
- }
65470
- lines.push("");
65471
- }
65472
- return lines.join("\n");
65473
- }
65474
-
65475
- // src/snippet/generators.ts
65476
- function generateCursorSnippet(rules) {
65477
- const lines = [];
65478
- lines.push("# slopbrick cursor rules");
65479
- lines.push("");
65480
- lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
65481
- lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
65482
- lines.push("");
65483
- lines.push("## Category-level directives");
65484
- lines.push("");
65485
- for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
65486
- lines.push("- **" + category + "**: " + directive);
65487
- }
65488
- lines.push("");
65489
- lines.push("## Per-rule directives");
65490
- lines.push("");
65491
- const ai = aiSpecificRules(rules);
65492
- const byCategory = categorizeRules(ai);
65493
- for (const [category, list] of byCategory) {
65494
- lines.push("### " + category);
65495
- lines.push("");
65496
- for (const rule of list) {
65497
- const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
65498
- lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
65499
- }
65500
- lines.push("");
65501
- }
65502
- lines.push("## Verification");
65503
- lines.push("");
65504
- lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
65505
- lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
65506
- lines.push("");
65507
- return lines.join("\n");
65508
- }
65509
- function generateAgentsMdSnippet(rules) {
65510
- return baseContent(
65511
- rules,
65512
- "AGENTS.md",
65513
- "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."
65514
- ) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
65515
- }
65516
- function generateClaudeMdSnippet(rules) {
65517
- return baseContent(
65518
- rules,
65519
- "CLAUDE.md",
65520
- "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."
65521
- ) + "## 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";
65522
- }
65523
- function generateAiderSnippet(rules) {
65524
- return baseContent(
65525
- rules,
65526
- "CONVENTIONS.md",
65527
- "Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
65528
- );
65529
- }
65530
- function generateWindsurfSnippet(rules) {
65531
- return baseContent(
65532
- rules,
65533
- ".windsurfrules",
65534
- "Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
65535
- );
65536
- }
65537
- function generateClineRules(rules) {
65538
- return baseContent(
65539
- rules,
65540
- "AGENTS.md",
65541
- "Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
65542
- );
65543
- }
65544
- function generateCursorrulesLegacySnippet(rules) {
65545
- return baseContent(
65546
- rules,
65547
- ".cursorrules",
65548
- "Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
65549
- );
65550
- }
65551
- function generateGeminiSnippet(rules) {
65552
- return baseContent(
65553
- rules,
65554
- "GEMINI.md",
65555
- "Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
65556
- );
65557
- }
65558
- function generateCopilotSnippet(rules) {
65559
- return baseContent(
65560
- rules,
65561
- "copilot-instructions.md",
65562
- "Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
65563
- );
65564
- }
65565
-
65566
- // src/snippet/targets.ts
65567
- var SNIPPET_TARGETS = [
66345
+ init_generators();
66346
+ import { join as join31 } from "path";
66347
+ var SNIPPET_TARGETS2 = [
65568
66348
  {
65569
66349
  flag: "cursor",
65570
66350
  cliName: "--cursor",
@@ -65657,7 +66437,7 @@ var SNIPPET_TARGETS = [
65657
66437
  }
65658
66438
  ];
65659
66439
  function resolveTargetPath(target) {
65660
- return target.isFolder ? join29(target.path, target.filename) : target.path;
66440
+ return target.isFolder ? join31(target.path, target.filename) : target.path;
65661
66441
  }
65662
66442
  function renderMatrix() {
65663
66443
  const lines = [];
@@ -65665,7 +66445,7 @@ function renderMatrix() {
65665
66445
  lines.push("");
65666
66446
  lines.push("| Flag | File | Agent |");
65667
66447
  lines.push("|------|------|-------|");
65668
- for (const t of SNIPPET_TARGETS) {
66448
+ for (const t of SNIPPET_TARGETS2) {
65669
66449
  lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
65670
66450
  }
65671
66451
  return lines.join("\n");
@@ -65679,13 +66459,13 @@ function registerInit(program) {
65679
66459
  logger.info(renderMatrix());
65680
66460
  process.exit(0);
65681
66461
  }
65682
- const cwd = resolve39(options.workspace ?? process.cwd());
65683
- const configPath = join30(cwd, "slopbrick.config.mjs");
66462
+ const cwd = resolve40(options.workspace ?? process.cwd());
66463
+ const configPath = join32(cwd, "slopbrick.config.mjs");
65684
66464
  const detected = detectStack(cwd);
65685
66465
  const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
65686
66466
  const proposed = serializeConfig(fallbackConfig);
65687
- if (existsSync27(configPath) && !cmdOptions.yes) {
65688
- const current = readFileSync35(configPath, "utf8");
66467
+ if (existsSync29(configPath) && !cmdOptions.yes) {
66468
+ const current = readFileSync37(configPath, "utf8");
65689
66469
  logger.error(`A config file already exists at ${configPath}.`);
65690
66470
  logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
65691
66471
  logger.error("");
@@ -65706,7 +66486,7 @@ function registerInit(program) {
65706
66486
  config = buildInitConfig(detected, answers);
65707
66487
  usedWizard = true;
65708
66488
  }
65709
- writeFileSync18(configPath, serializeConfig(config));
66489
+ writeFileSync20(configPath, serializeConfig(config));
65710
66490
  appendGitignore(cwd);
65711
66491
  const refresh = await refreshRegistrySnapshot(cwd);
65712
66492
  if (!refresh.ok) {
@@ -65730,27 +66510,27 @@ function registerInit(program) {
65730
66510
  }
65731
66511
  logger.info(refresh.message);
65732
66512
  }
65733
- const targetsToWrite = SNIPPET_TARGETS.filter((t) => {
66513
+ const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
65734
66514
  if (cmdOptions.all) return true;
65735
66515
  const opts = cmdOptions;
65736
66516
  return Boolean(opts[t.flag]);
65737
66517
  });
65738
66518
  for (const target of targetsToWrite) {
65739
- const snippetPath = join30(cwd, resolveTargetPath(target));
65740
- mkdirSync15(dirname20(snippetPath), { recursive: true });
66519
+ const snippetPath = join32(cwd, resolveTargetPath(target));
66520
+ mkdirSync16(dirname21(snippetPath), { recursive: true });
65741
66521
  const generated = target.generator(builtinRules);
65742
- if (!target.isFolder && existsSync27(snippetPath)) {
65743
- const existing = readFileSync35(snippetPath, "utf8");
66522
+ if (!target.isFolder && existsSync29(snippetPath)) {
66523
+ const existing = readFileSync37(snippetPath, "utf8");
65744
66524
  if (existing.includes("<!-- slopbrick:begin -->")) {
65745
66525
  const updated = existing.replace(
65746
66526
  /<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
65747
66527
  "<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
65748
66528
  );
65749
- writeFileSync18(snippetPath, updated, "utf8");
66529
+ writeFileSync20(snippetPath, updated, "utf8");
65750
66530
  if (!options.quiet) logger.info(`Updated ${snippetPath}`);
65751
66531
  continue;
65752
66532
  }
65753
- writeFileSync18(
66533
+ writeFileSync20(
65754
66534
  snippetPath,
65755
66535
  existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
65756
66536
  "utf8"
@@ -65758,7 +66538,7 @@ function registerInit(program) {
65758
66538
  if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
65759
66539
  continue;
65760
66540
  }
65761
- writeFileSync18(snippetPath, generated, "utf8");
66541
+ writeFileSync20(snippetPath, generated, "utf8");
65762
66542
  if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
65763
66543
  }
65764
66544
  if (options.baseline) {
@@ -65778,12 +66558,12 @@ function registerInit(program) {
65778
66558
  // src/cli/commands/flywheel.ts
65779
66559
  init_logger();
65780
66560
  init_telemetry();
65781
- import { mkdirSync as mkdirSync16, writeFileSync as writeFileSync19 } from "fs";
65782
- import { dirname as dirname21, resolve as resolve40 } from "path";
66561
+ import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync21 } from "fs";
66562
+ import { dirname as dirname22, resolve as resolve41 } from "path";
65783
66563
 
65784
66564
  // src/report/flywheel.ts
65785
- import { existsSync as existsSync28, readFileSync as readFileSync36 } from "fs";
65786
- import { join as join31 } from "path";
66565
+ import { existsSync as existsSync30, readFileSync as readFileSync38 } from "fs";
66566
+ import { join as join33 } from "path";
65787
66567
  function average(values) {
65788
66568
  if (values.length === 0) return 0;
65789
66569
  return values.reduce((a, b) => a + b, 0) / values.length;
@@ -65873,7 +66653,7 @@ function formatFlywheel(summary, options = {}) {
65873
66653
  function registerFlywheel(program) {
65874
66654
  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) => {
65875
66655
  const options = command.optsWithGlobals();
65876
- const cwd = resolve40(options.workspace ?? process.cwd());
66656
+ const cwd = resolve41(options.workspace ?? process.cwd());
65877
66657
  const payloads = readTelemetry(cwd);
65878
66658
  if (payloads.length === 0) {
65879
66659
  logger.info("No flywheel telemetry found. Run a scan first.");
@@ -65881,9 +66661,9 @@ function registerFlywheel(program) {
65881
66661
  }
65882
66662
  const summary = summarizeTelemetry(payloads);
65883
66663
  if (cmdOptions.export) {
65884
- const exportPath = resolve40(cmdOptions.export);
65885
- mkdirSync16(dirname21(exportPath), { recursive: true });
65886
- writeFileSync19(exportPath, JSON.stringify(summary, null, 2), "utf-8");
66664
+ const exportPath = resolve41(cmdOptions.export);
66665
+ mkdirSync17(dirname22(exportPath), { recursive: true });
66666
+ writeFileSync21(exportPath, JSON.stringify(summary, null, 2), "utf-8");
65887
66667
  logger.info(`Wrote flywheel summary to ${exportPath}`);
65888
66668
  process.exit(0);
65889
66669
  }
@@ -65910,8 +66690,8 @@ init_dist2();
65910
66690
  init_memory_io();
65911
66691
 
65912
66692
  // src/fix/focus-ring.ts
65913
- import { existsSync as existsSync29 } from "fs";
65914
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync20 } from "fs";
66693
+ import { existsSync as existsSync31 } from "fs";
66694
+ import { readFileSync as readFileSync39, writeFileSync as writeFileSync22 } from "fs";
65915
66695
  var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
65916
66696
  var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
65917
66697
  :focus-visible {
@@ -65920,15 +66700,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
65920
66700
  }
65921
66701
  /* @slopbrick:v1.0.0:fix:focus-ring-end */`;
65922
66702
  function applyFocusRingFix(targetFile) {
65923
- if (!existsSync29(targetFile)) {
66703
+ if (!existsSync31(targetFile)) {
65924
66704
  return { applied: false, reason: "missing-global-css-target" };
65925
66705
  }
65926
- const content = readFileSync37(targetFile, "utf-8");
66706
+ const content = readFileSync39(targetFile, "utf-8");
65927
66707
  if (content.includes(ANCHOR_START)) {
65928
66708
  return { applied: false, reason: "already-present" };
65929
66709
  }
65930
66710
  const separator = content.endsWith("\n") ? "" : "\n";
65931
- writeFileSync20(targetFile, `${content}${separator}${CSS_BLOCK}
66711
+ writeFileSync22(targetFile, `${content}${separator}${CSS_BLOCK}
65932
66712
  `);
65933
66713
  return { applied: true };
65934
66714
  }
@@ -65937,21 +66717,21 @@ function applyFocusRingFix(targetFile) {
65937
66717
  init_layout_token();
65938
66718
 
65939
66719
  // src/fix/use-client.ts
65940
- import { readFileSync as readFileSync38, writeFileSync as writeFileSync21 } from "fs";
66720
+ import { readFileSync as readFileSync40, writeFileSync as writeFileSync23 } from "fs";
65941
66721
  function applyUseClientFix(filePath) {
65942
- const content = readFileSync38(filePath, "utf-8");
66722
+ const content = readFileSync40(filePath, "utf-8");
65943
66723
  const trimmed = content.trimStart();
65944
66724
  if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
65945
66725
  return { applied: false, reason: "already-present" };
65946
66726
  }
65947
- writeFileSync21(filePath, `"use client";
66727
+ writeFileSync23(filePath, `"use client";
65948
66728
 
65949
66729
  ${content}`);
65950
66730
  return { applied: true };
65951
66731
  }
65952
66732
 
65953
66733
  // src/fix/visual-codemod.ts
65954
- import { readFileSync as readFileSync39, writeFileSync as writeFileSync22 } from "fs";
66734
+ import { readFileSync as readFileSync41, writeFileSync as writeFileSync24 } from "fs";
65955
66735
 
65956
66736
  // src/fix/visual-codemods/tailwind.ts
65957
66737
  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;
@@ -66160,7 +66940,7 @@ var ALL_CODEMODS = [
66160
66940
  { name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
66161
66941
  ];
66162
66942
  function applyVisualCodemods(filePath) {
66163
- const original = readFileSync39(filePath, "utf-8");
66943
+ const original = readFileSync41(filePath, "utf-8");
66164
66944
  let content = original;
66165
66945
  const reasons = [];
66166
66946
  const seen = /* @__PURE__ */ new Set();
@@ -66176,7 +66956,7 @@ function applyVisualCodemods(filePath) {
66176
66956
  }
66177
66957
  }
66178
66958
  if (content !== original) {
66179
- writeFileSync22(filePath, content);
66959
+ writeFileSync24(filePath, content);
66180
66960
  }
66181
66961
  return {
66182
66962
  filePath,
@@ -66458,12 +67238,12 @@ import * as http from "http";
66458
67238
  import * as https from "https";
66459
67239
  import { URL as URL2 } from "url";
66460
67240
  var sendBeacon = (url, payload, timeoutMs) => {
66461
- return new Promise((resolve42) => {
67241
+ return new Promise((resolve43) => {
66462
67242
  let parsed;
66463
67243
  try {
66464
67244
  parsed = new URL2(url);
66465
67245
  } catch {
66466
- resolve42();
67246
+ resolve43();
66467
67247
  return;
66468
67248
  }
66469
67249
  const isHttps = parsed.protocol === "https:";
@@ -66484,7 +67264,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
66484
67264
  const settle = () => {
66485
67265
  if (settled) return;
66486
67266
  settled = true;
66487
- resolve42();
67267
+ resolve43();
66488
67268
  };
66489
67269
  let req;
66490
67270
  try {
@@ -66564,7 +67344,7 @@ process.on("uncaughtException", (err) => {
66564
67344
  });
66565
67345
  async function runCli({ start }) {
66566
67346
  try {
66567
- const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
67347
+ const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--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)");
66568
67348
  program.helpInformation = () => formatGroupedHelp(program);
66569
67349
  setExitOverride(program);
66570
67350
  registerInit(program);
@@ -66573,6 +67353,7 @@ async function runCli({ start }) {
66573
67353
  registerBadge(program);
66574
67354
  registerSuggest(program);
66575
67355
  registerExplain(program);
67356
+ registerComposite(program);
66576
67357
  registerCalibration(program);
66577
67358
  registerFlywheel(program);
66578
67359
  const scanAction = async (paths, _options, command) => {
@@ -66591,7 +67372,7 @@ async function runCli({ start }) {
66591
67372
  logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
66592
67373
  process.exit(2);
66593
67374
  }
66594
- const cwd = resolve41(options.workspace ?? process.cwd());
67375
+ const cwd = resolve42(options.workspace ?? process.cwd());
66595
67376
  if (options.trend !== void 0) {
66596
67377
  const runs = await readRuns(cwd, fsMemoryIO);
66597
67378
  if (runs.length === 0) {
@@ -66639,7 +67420,7 @@ async function runCli({ start }) {
66639
67420
  });
66640
67421
  }
66641
67422
  if (options.baseline) {
66642
- const cwd2 = resolve41(options.workspace ?? process.cwd());
67423
+ const cwd2 = resolve42(options.workspace ?? process.cwd());
66643
67424
  const configHash = hashConfig(config);
66644
67425
  const gitHead = await getGitHead(cwd2) ?? "unknown";
66645
67426
  const cache = buildBaselineCache(report, configHash, gitHead, cwd2);