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