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/engine/worker.cjs +49 -17
- package/dist/engine/worker.js +49 -17
- package/dist/index.cjs +1388 -607
- package/dist/index.d.cts +103 -1
- package/dist/index.d.ts +103 -1
- package/dist/index.js +1330 -549
- package/package.json +1 -1
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.
|
|
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((
|
|
46865
|
+
return new Promise((resolve43) => {
|
|
46866
46866
|
const check = setInterval(() => {
|
|
46867
46867
|
if (resolved) {
|
|
46868
46868
|
clearInterval(check);
|
|
46869
|
-
|
|
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
|
-
|
|
46887
|
-
|
|
46888
|
-
|
|
46889
|
-
|
|
46890
|
-
|
|
46891
|
-
|
|
46892
|
-
|
|
46893
|
-
|
|
46894
|
-
|
|
46895
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
54580
|
+
return new Promise((resolve43) => {
|
|
54545
54581
|
try {
|
|
54546
54582
|
const path = join18(workspaceDir, STRUCTURE_MD_FILE);
|
|
54547
54583
|
if (!existsSync16(path)) {
|
|
54548
|
-
|
|
54584
|
+
resolve43(null);
|
|
54549
54585
|
return;
|
|
54550
54586
|
}
|
|
54551
54587
|
const content = readFileSync20(path, "utf-8");
|
|
54552
|
-
|
|
54588
|
+
resolve43(content);
|
|
54553
54589
|
} catch {
|
|
54554
|
-
|
|
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
|
|
54604
|
-
import { join as
|
|
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 =
|
|
54687
|
-
if (!
|
|
54688
|
-
|
|
54689
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
56757
|
+
let content = readFileSync23(filePath, "utf-8");
|
|
56325
56758
|
const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
|
|
56326
56759
|
if (patched !== content) {
|
|
56327
|
-
|
|
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
|
|
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 (!
|
|
56416
|
-
const original =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
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
|
|
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
|
|
57168
|
-
import { join as
|
|
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
|
|
57173
|
-
import { dirname as dirname13, join as
|
|
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
|
|
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 (!
|
|
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 (!
|
|
57806
|
+
if (!existsSync21(cached)) return false;
|
|
57325
57807
|
try {
|
|
57326
|
-
const parsed = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
57867
|
+
resolve43(void 0);
|
|
57386
57868
|
return;
|
|
57387
57869
|
}
|
|
57388
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
58020
|
+
const testFile = join23(cwd, ".slopbrick", ".doctor-test.ts");
|
|
57539
58021
|
mkdirSync9(dirname14(testFile), { recursive: true });
|
|
57540
|
-
|
|
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
|
|
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 =
|
|
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
|
|
62395
|
+
import { resolve as resolve16 } from "path";
|
|
61595
62396
|
|
|
61596
62397
|
// src/cli/installer.ts
|
|
61597
62398
|
import {
|
|
61598
62399
|
chmodSync,
|
|
61599
|
-
existsSync as
|
|
61600
|
-
mkdirSync as
|
|
61601
|
-
readFileSync as
|
|
61602
|
-
writeFileSync as
|
|
62400
|
+
existsSync as existsSync24,
|
|
62401
|
+
mkdirSync as mkdirSync11,
|
|
62402
|
+
readFileSync as readFileSync28,
|
|
62403
|
+
writeFileSync as writeFileSync14
|
|
61603
62404
|
} from "fs";
|
|
61604
|
-
import { dirname as
|
|
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 =
|
|
61613
|
-
if (
|
|
61614
|
-
return
|
|
62413
|
+
const huskyDir = join26(gitRoot, ".husky");
|
|
62414
|
+
if (existsSync24(huskyDir)) {
|
|
62415
|
+
return join26(huskyDir, "pre-commit");
|
|
61615
62416
|
}
|
|
61616
|
-
return
|
|
62417
|
+
return join26(gitRoot, ".git", "hooks", "pre-commit");
|
|
61617
62418
|
}
|
|
61618
62419
|
function readHookContent(path) {
|
|
61619
|
-
return
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61682
|
-
|
|
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 (!
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
61791
|
-
import { resolve as
|
|
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 =
|
|
62833
|
+
const absPath = resolve18(ctx.cwd, path);
|
|
62033
62834
|
let source;
|
|
62034
62835
|
try {
|
|
62035
|
-
source =
|
|
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((
|
|
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", () =>
|
|
62257
|
-
input.on("close", () =>
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
63202
|
+
import { resolve as resolve23 } from "path";
|
|
62402
63203
|
|
|
62403
63204
|
// src/cli/migrate.ts
|
|
62404
63205
|
init_logger();
|
|
62405
|
-
import { existsSync as
|
|
62406
|
-
import { join as
|
|
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 =
|
|
62412
|
-
const newDir =
|
|
62413
|
-
if (
|
|
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:
|
|
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:
|
|
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 =
|
|
62429
|
-
const newCache =
|
|
62430
|
-
if (
|
|
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 =
|
|
62435
|
-
const newCfg =
|
|
62436
|
-
if (
|
|
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 =
|
|
62441
|
-
if (
|
|
62442
|
-
const content =
|
|
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
|
|
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 (!
|
|
62470
|
-
const content =
|
|
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
|
-
|
|
63273
|
+
writeFileSync15(r.path, next);
|
|
62473
63274
|
}
|
|
62474
63275
|
for (const g of plan.gitignoreEdits) {
|
|
62475
|
-
const content =
|
|
63276
|
+
const content = readFileSync30(g.path, "utf-8");
|
|
62476
63277
|
const next = content.replaceAll(g.from, g.to);
|
|
62477
|
-
|
|
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 (!
|
|
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 =
|
|
62493
|
-
const oldDir =
|
|
62494
|
-
if (
|
|
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 =
|
|
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
|
|
62677
|
-
import { extname as extname8, resolve as
|
|
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 ?
|
|
62681
|
-
if (!
|
|
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
|
|
62908
|
-
import { dirname as
|
|
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
|
|
62964
|
-
import { join as
|
|
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 =
|
|
63028
|
-
|
|
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 =
|
|
63034
|
-
|
|
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 =
|
|
63046
|
-
|
|
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
|
|
63253
|
-
import { join as
|
|
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
|
|
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 =
|
|
63259
|
-
var NEGATIVE_DIR =
|
|
63260
|
-
var FILELISTS_DIR =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
[
|
|
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 (!
|
|
63291
|
-
const report = JSON.parse(
|
|
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 (!
|
|
63320
|
-
if (!
|
|
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 =
|
|
63326
|
-
const negListPath =
|
|
63327
|
-
|
|
63328
|
-
|
|
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 ?
|
|
63443
|
-
|
|
63444
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
63925
|
-
import { extname as extname9, relative as relative17, resolve as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
65341
|
-
import { dirname as
|
|
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:
|
|
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 =
|
|
65364
|
-
if (!
|
|
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(
|
|
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 =
|
|
65372
|
-
|
|
65373
|
-
|
|
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 =
|
|
65384
|
-
if (!
|
|
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(
|
|
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 =
|
|
65399
|
-
|
|
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
|
-
|
|
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
|
|
65421
|
-
import { dirname as
|
|
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
|
-
|
|
65430
|
-
|
|
65431
|
-
|
|
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 ?
|
|
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
|
|
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 =
|
|
65683
|
-
const configPath =
|
|
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 (
|
|
65688
|
-
const current =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
65740
|
-
|
|
66519
|
+
const snippetPath = join32(cwd, resolveTargetPath(target));
|
|
66520
|
+
mkdirSync16(dirname21(snippetPath), { recursive: true });
|
|
65741
66521
|
const generated = target.generator(builtinRules);
|
|
65742
|
-
if (!target.isFolder &&
|
|
65743
|
-
const existing =
|
|
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
|
-
|
|
66529
|
+
writeFileSync20(snippetPath, updated, "utf8");
|
|
65750
66530
|
if (!options.quiet) logger.info(`Updated ${snippetPath}`);
|
|
65751
66531
|
continue;
|
|
65752
66532
|
}
|
|
65753
|
-
|
|
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
|
-
|
|
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
|
|
65782
|
-
import { dirname as
|
|
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
|
|
65786
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
65885
|
-
|
|
65886
|
-
|
|
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
|
|
65914
|
-
import { readFileSync as
|
|
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 (!
|
|
66703
|
+
if (!existsSync31(targetFile)) {
|
|
65924
66704
|
return { applied: false, reason: "missing-global-css-target" };
|
|
65925
66705
|
}
|
|
65926
|
-
const content =
|
|
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
|
-
|
|
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
|
|
66720
|
+
import { readFileSync as readFileSync40, writeFileSync as writeFileSync23 } from "fs";
|
|
65941
66721
|
function applyUseClientFix(filePath) {
|
|
65942
|
-
const content =
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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((
|
|
67241
|
+
return new Promise((resolve43) => {
|
|
66462
67242
|
let parsed;
|
|
66463
67243
|
try {
|
|
66464
67244
|
parsed = new URL2(url);
|
|
66465
67245
|
} catch {
|
|
66466
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|