slopbrick 0.40.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 +1991 -1009
- package/dist/index.d.cts +103 -1
- package/dist/index.d.ts +103 -1
- package/dist/index.js +1932 -950
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var VERSION;
|
|
|
19
19
|
var init_header = __esm({
|
|
20
20
|
"src/types/_header.ts"() {
|
|
21
21
|
"use strict";
|
|
22
|
-
VERSION = "0.
|
|
22
|
+
VERSION = "0.42.0";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
25
25
|
|
|
@@ -45804,6 +45804,15 @@ function formatCompositeScore(report) {
|
|
|
45804
45804
|
)
|
|
45805
45805
|
);
|
|
45806
45806
|
}
|
|
45807
|
+
const composite = report.compositeScore;
|
|
45808
|
+
if (composite !== void 0) {
|
|
45809
|
+
lines.push("");
|
|
45810
|
+
lines.push(
|
|
45811
|
+
chalk.dim(
|
|
45812
|
+
`composite=${composite.tier}@${composite.mean.toFixed(2)} \u2014 project-level Bayesian aggregate across ${composite.fileCount} file${composite.fileCount === 1 ? "" : "s"} (max ${composite.max.toFixed(2)}); informational, does not gate CI.`
|
|
45813
|
+
)
|
|
45814
|
+
);
|
|
45815
|
+
}
|
|
45807
45816
|
return lines.join("\n");
|
|
45808
45817
|
}
|
|
45809
45818
|
function formatCoherenceScores(report) {
|
|
@@ -46853,11 +46862,11 @@ var init_pool = __esm({
|
|
|
46853
46862
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46854
46863
|
spawnWorker();
|
|
46855
46864
|
}
|
|
46856
|
-
return new Promise((
|
|
46865
|
+
return new Promise((resolve43) => {
|
|
46857
46866
|
const check = setInterval(() => {
|
|
46858
46867
|
if (resolved) {
|
|
46859
46868
|
clearInterval(check);
|
|
46860
|
-
|
|
46869
|
+
resolve43(results);
|
|
46861
46870
|
}
|
|
46862
46871
|
}, 10);
|
|
46863
46872
|
});
|
|
@@ -46874,23 +46883,16 @@ var init_registry = __esm({
|
|
|
46874
46883
|
init_builtins();
|
|
46875
46884
|
RuleRegistry = class {
|
|
46876
46885
|
rules = /* @__PURE__ */ new Map();
|
|
46877
|
-
|
|
46878
|
-
|
|
46879
|
-
|
|
46880
|
-
|
|
46881
|
-
|
|
46882
|
-
|
|
46883
|
-
|
|
46884
|
-
|
|
46885
|
-
|
|
46886
|
-
|
|
46887
|
-
create: factory.create,
|
|
46888
|
-
analyze: factory.analyze
|
|
46889
|
-
};
|
|
46890
|
-
this.rules.set(ruleOrId, rule);
|
|
46891
|
-
} else {
|
|
46892
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
46893
|
-
}
|
|
46886
|
+
/**
|
|
46887
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
46888
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
46889
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
46890
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
46891
|
+
* External consumers that needed `register-by-id` semantics can
|
|
46892
|
+
* construct a `Rule` directly and pass it in.
|
|
46893
|
+
*/
|
|
46894
|
+
register(rule) {
|
|
46895
|
+
this.rules.set(rule.id, rule);
|
|
46894
46896
|
}
|
|
46895
46897
|
loadBuiltins(onlyRuleId) {
|
|
46896
46898
|
for (const rule of builtinRules) {
|
|
@@ -46919,6 +46921,15 @@ var init_registry = __esm({
|
|
|
46919
46921
|
all() {
|
|
46920
46922
|
return Array.from(this.rules.values());
|
|
46921
46923
|
}
|
|
46924
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
46925
|
+
* Returns undefined if not registered. */
|
|
46926
|
+
get(id) {
|
|
46927
|
+
return this.rules.get(id);
|
|
46928
|
+
}
|
|
46929
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
46930
|
+
has(id) {
|
|
46931
|
+
return this.rules.has(id);
|
|
46932
|
+
}
|
|
46922
46933
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
46923
46934
|
const context = {
|
|
46924
46935
|
config,
|
|
@@ -50570,6 +50581,10 @@ var init_signal_strength2 = __esm({
|
|
|
50570
50581
|
});
|
|
50571
50582
|
|
|
50572
50583
|
// src/engine/worker.ts
|
|
50584
|
+
var worker_exports = {};
|
|
50585
|
+
__export(worker_exports, {
|
|
50586
|
+
scanFile: () => scanFile
|
|
50587
|
+
});
|
|
50573
50588
|
import { isMainThread, parentPort, workerData } from "worker_threads";
|
|
50574
50589
|
import { extname as extname5 } from "path";
|
|
50575
50590
|
import { join as join14, relative as relative5, sep as sep2 } from "path";
|
|
@@ -50650,6 +50665,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
50650
50665
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
50651
50666
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
50652
50667
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
50668
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
50669
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
50670
|
+
compositeFireSet: fireSet
|
|
50671
|
+
});
|
|
50672
|
+
for (const { rule } of rules) {
|
|
50673
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
50674
|
+
try {
|
|
50675
|
+
const compositeIssues = rule.analyze(
|
|
50676
|
+
rule.create({
|
|
50677
|
+
config,
|
|
50678
|
+
filePath,
|
|
50679
|
+
cwd,
|
|
50680
|
+
framework: config.framework,
|
|
50681
|
+
uiLibraries: config.uiLibraries,
|
|
50682
|
+
hasTailwind: config.hasTailwind,
|
|
50683
|
+
supportsRsc: config.supportsRsc,
|
|
50684
|
+
hotspotIssues: []
|
|
50685
|
+
}),
|
|
50686
|
+
factsWithFireSet
|
|
50687
|
+
);
|
|
50688
|
+
if (compositeIssues.length > 0) {
|
|
50689
|
+
issues.push(...compositeIssues);
|
|
50690
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
50691
|
+
}
|
|
50692
|
+
} catch (err) {
|
|
50693
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
50694
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
50695
|
+
}
|
|
50696
|
+
}
|
|
50697
|
+
}
|
|
50653
50698
|
const gapValues = collectGapValues(facts);
|
|
50654
50699
|
const styleSources = collectStyleSources(facts);
|
|
50655
50700
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|
|
@@ -54226,6 +54271,21 @@ function safeRelative(cwd, filePath) {
|
|
|
54226
54271
|
return filePath;
|
|
54227
54272
|
}
|
|
54228
54273
|
}
|
|
54274
|
+
function buildInventorySummary(inventory) {
|
|
54275
|
+
const patternCounts = {};
|
|
54276
|
+
const patternNames = {};
|
|
54277
|
+
for (const [category, matches] of Object.entries(inventory.patterns)) {
|
|
54278
|
+
if (matches.length === 0) continue;
|
|
54279
|
+
const names = Array.from(new Set(matches.map((m) => m.name))).sort();
|
|
54280
|
+
patternCounts[category] = names.length;
|
|
54281
|
+
patternNames[category] = names.slice(0, TELEMETRY_INVENTORY_NAME_CAP);
|
|
54282
|
+
}
|
|
54283
|
+
return {
|
|
54284
|
+
scannedFiles: inventory.scannedFiles,
|
|
54285
|
+
patternCounts,
|
|
54286
|
+
patternNames
|
|
54287
|
+
};
|
|
54288
|
+
}
|
|
54229
54289
|
function aggregateViolations(report) {
|
|
54230
54290
|
const counts = /* @__PURE__ */ new Map();
|
|
54231
54291
|
for (const issue of report.issues) {
|
|
@@ -54305,7 +54365,7 @@ function readTelemetry(cwd) {
|
|
|
54305
54365
|
}
|
|
54306
54366
|
return payloads;
|
|
54307
54367
|
}
|
|
54308
|
-
function recordTelemetry(cwd, report, results, config) {
|
|
54368
|
+
function recordTelemetry(cwd, report, results, config, inventory) {
|
|
54309
54369
|
if (config.telemetry === false) {
|
|
54310
54370
|
return void 0;
|
|
54311
54371
|
}
|
|
@@ -54326,7 +54386,14 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54326
54386
|
framework: config.framework
|
|
54327
54387
|
},
|
|
54328
54388
|
violations: aggregateViolations(report),
|
|
54329
|
-
files: buildFileRecords(cwd, report, results)
|
|
54389
|
+
files: buildFileRecords(cwd, report, results),
|
|
54390
|
+
// v0.41.0 (Sprint 2, task 2a.1): the inventory summary is
|
|
54391
|
+
// additive — omitting it (legacy callers) keeps the JSONL line
|
|
54392
|
+
// shape identical to v0.40.x payloads, so old readers stay
|
|
54393
|
+
// green. New readers (`slopbrick drift --since <date>` in
|
|
54394
|
+
// Sprint 2a.2) treat the field as optional and fall back to
|
|
54395
|
+
// a re-scan when it's missing.
|
|
54396
|
+
...inventory ? { inventory: buildInventorySummary(inventory) } : {}
|
|
54330
54397
|
};
|
|
54331
54398
|
const path = telemetryPath2(cwd);
|
|
54332
54399
|
const dir = dirname11(path);
|
|
@@ -54337,7 +54404,7 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54337
54404
|
appendFileSync(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
54338
54405
|
return payload;
|
|
54339
54406
|
}
|
|
54340
|
-
var TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
|
|
54407
|
+
var TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES, TELEMETRY_INVENTORY_NAME_CAP;
|
|
54341
54408
|
var init_telemetry = __esm({
|
|
54342
54409
|
"src/engine/telemetry.ts"() {
|
|
54343
54410
|
"use strict";
|
|
@@ -54345,6 +54412,7 @@ var init_telemetry = __esm({
|
|
|
54345
54412
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
54346
54413
|
MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
|
|
54347
54414
|
MAX_ROTATED_FILES = 5;
|
|
54415
|
+
TELEMETRY_INVENTORY_NAME_CAP = 50;
|
|
54348
54416
|
}
|
|
54349
54417
|
});
|
|
54350
54418
|
|
|
@@ -54497,29 +54565,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
54497
54565
|
return lines.join("\n");
|
|
54498
54566
|
}
|
|
54499
54567
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
54500
|
-
await new Promise((
|
|
54568
|
+
await new Promise((resolve43, reject) => {
|
|
54501
54569
|
try {
|
|
54502
54570
|
const path = join18(workspaceDir, STRUCTURE_MD_FILE);
|
|
54503
54571
|
mkdirSync6(dirname12(path), { recursive: true });
|
|
54504
54572
|
writeFileSync6(path, md, "utf-8");
|
|
54505
|
-
|
|
54573
|
+
resolve43();
|
|
54506
54574
|
} catch (err) {
|
|
54507
54575
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
54508
54576
|
}
|
|
54509
54577
|
});
|
|
54510
54578
|
}
|
|
54511
54579
|
async function readStructureMarkdown(workspaceDir) {
|
|
54512
|
-
return new Promise((
|
|
54580
|
+
return new Promise((resolve43) => {
|
|
54513
54581
|
try {
|
|
54514
54582
|
const path = join18(workspaceDir, STRUCTURE_MD_FILE);
|
|
54515
54583
|
if (!existsSync16(path)) {
|
|
54516
|
-
|
|
54584
|
+
resolve43(null);
|
|
54517
54585
|
return;
|
|
54518
54586
|
}
|
|
54519
54587
|
const content = readFileSync20(path, "utf-8");
|
|
54520
|
-
|
|
54588
|
+
resolve43(content);
|
|
54521
54589
|
} catch {
|
|
54522
|
-
|
|
54590
|
+
resolve43(null);
|
|
54523
54591
|
}
|
|
54524
54592
|
});
|
|
54525
54593
|
}
|
|
@@ -54567,9 +54635,391 @@ var init_structure_md = __esm({
|
|
|
54567
54635
|
}
|
|
54568
54636
|
});
|
|
54569
54637
|
|
|
54638
|
+
// src/snippet/data.ts
|
|
54639
|
+
var CATEGORY_DIRECTIVES, RULE_HINTS;
|
|
54640
|
+
var init_data = __esm({
|
|
54641
|
+
"src/snippet/data.ts"() {
|
|
54642
|
+
"use strict";
|
|
54643
|
+
CATEGORY_DIRECTIVES = {
|
|
54644
|
+
visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
|
|
54645
|
+
logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
|
|
54646
|
+
wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
|
|
54647
|
+
security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
|
|
54648
|
+
perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
|
|
54649
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
54650
|
+
layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
|
|
54651
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
54652
|
+
arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
|
|
54653
|
+
test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
|
|
54654
|
+
};
|
|
54655
|
+
RULE_HINTS = {
|
|
54656
|
+
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
54657
|
+
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
54658
|
+
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
54659
|
+
// docs/research/backlog-rule-hints.md
|
|
54660
|
+
// for future implementers to paste back when the corresponding rules ship.)
|
|
54661
|
+
"security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
|
|
54662
|
+
"security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
|
|
54663
|
+
"security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
|
|
54664
|
+
"security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
|
|
54665
|
+
"security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
|
|
54666
|
+
"security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
|
|
54667
|
+
"security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
|
|
54668
|
+
"security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
|
|
54669
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
54670
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
54671
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
54672
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
54673
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
54674
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
54675
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
54676
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
54677
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
54678
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
54679
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
54680
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
54681
|
+
"component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
|
|
54682
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
54683
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
54684
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
54685
|
+
"layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
|
|
54686
|
+
"layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
|
|
54687
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
54688
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
54689
|
+
"logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54690
|
+
"logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
|
|
54691
|
+
"logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54692
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
54693
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
54694
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
54695
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
54696
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
54697
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
54698
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
54699
|
+
"dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
|
|
54700
|
+
"dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
|
|
54701
|
+
"dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
|
|
54702
|
+
"dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
|
|
54703
|
+
"dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
|
|
54704
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
54705
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
54706
|
+
"typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
|
|
54707
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
54708
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
54709
|
+
"visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
|
|
54710
|
+
"visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
|
|
54711
|
+
"visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
|
|
54712
|
+
"visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
|
|
54713
|
+
"visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
|
|
54714
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
54715
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
54716
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
54717
|
+
"test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
|
|
54718
|
+
"test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
|
|
54719
|
+
"product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
|
|
54720
|
+
"product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
|
|
54721
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
54722
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
54723
|
+
"ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
|
|
54724
|
+
"ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
|
|
54725
|
+
"ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
|
|
54726
|
+
"ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
|
|
54727
|
+
"ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
|
|
54728
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
54729
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
54730
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
54731
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
54732
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
54733
|
+
"ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
|
|
54734
|
+
"ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
|
|
54735
|
+
"ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
|
|
54736
|
+
"ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
|
|
54737
|
+
"ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
|
|
54738
|
+
"ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
|
|
54739
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
54740
|
+
"db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
|
|
54741
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
54742
|
+
"docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
|
|
54743
|
+
"docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
|
|
54744
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
54745
|
+
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
54746
|
+
"rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
|
|
54747
|
+
"rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
|
|
54748
|
+
"rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
|
|
54749
|
+
"rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
|
|
54750
|
+
// v0.19.0 — TypeScript-specific rules
|
|
54751
|
+
"ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
|
|
54752
|
+
"ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
|
|
54753
|
+
"ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
|
|
54754
|
+
"ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
|
|
54755
|
+
// v0.19.0 — Go-specific rules
|
|
54756
|
+
"go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
|
|
54757
|
+
"go/nil-slice-vs-empty": "Either declare as `var x = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it.",
|
|
54758
|
+
// v0.19.0 — Duplication detector
|
|
54759
|
+
"dup/identical-block": "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
|
|
54760
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
54761
|
+
"dup/near-duplicate": "Refactor to a shared helper. Type-2 near-duplicate: same identifiers + structure, different whitespace/comments. AI-iterated code regenerates the same function with cosmetic edits. (v0.23.0 \u2014 MinHash + LSH, k=5, thr 0.7.)",
|
|
54762
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
54763
|
+
"dup/structural-clone": "Refactor to a shared helper. Type-3 clone: same shape as another file after identifier canonicalization, with renames and/or added/removed statements. AI agents and copy-paste both produce this. Extract the shared part. (v0.24.0.)",
|
|
54764
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
54765
|
+
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
54766
|
+
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
54767
|
+
"java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
|
|
54768
|
+
"java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
|
|
54769
|
+
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
54770
|
+
"java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
|
|
54771
|
+
// v0.35.1 — Raidar-inspired content-based detection
|
|
54772
|
+
"java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
|
|
54773
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
54774
|
+
"swift/force-unwrap": "Replace with the safe form: `as?` + guard/if let, `try?`, or `guard let x = optional else { return }`. `!` crashes unconditionally in release. (v0.24.0 \u2014 DORMANT.)",
|
|
54775
|
+
"swift/print-debug": "Replace with `Logger(subsystem:..., category:...).info(...)` (os.log). `print` writes to stdout with no level and no redaction. (v0.24.0 \u2014 DORMANT.)",
|
|
54776
|
+
"swift/fatal-error-thrown": "Replace with `return` of a typed default (e.g. `nil`, `[]()`) or throw a typed `enum MyError: Error`. `fatalError` survives release builds. (v0.24.0 \u2014 DORMANT.)",
|
|
54777
|
+
"swift/implicitly-unwrapped-optional": "Declare as `var name: Type?` and unwrap with `guard let`. IUOs exist for Obj-C bridging / IBOutlets but using them everywhere is an AI signal. (v0.24.0 \u2014 DORMANT.)",
|
|
54778
|
+
"swift/strong-self-capture": "Capture with `[weak self]` (or `[unowned self]` if guaranteed non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle. (v0.24.0 \u2014 DORMANT.)",
|
|
54779
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
54780
|
+
"cpp/raw-new-delete": "Replace with `auto p = std::make_unique<T>(...)`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free. (v0.24.0 \u2014 DORMANT.)",
|
|
54781
|
+
"cpp/c-style-cast": "Use the named cast: `static_cast<int>(x)`, `reinterpret_cast<...>(p)`, `const_cast<...>(ref)`. C-style casts silently pick static / reinterpret / const \u2014 impossible to grep. (v0.24.0 \u2014 DORMANT.)",
|
|
54782
|
+
"cpp/printf-debug": "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...`. `printf` / `std::cout` have no levels, no redaction, no sink routing. (v0.24.0 \u2014 DORMANT.)",
|
|
54783
|
+
"cpp/magic-numbers": "Replace with `constexpr int MAX = 1024;` (or `static constexpr`). Named constants live next to their value, can be searched, force the reader to mean what they say. (v0.24.0 \u2014 DORMANT.)"
|
|
54784
|
+
};
|
|
54785
|
+
}
|
|
54786
|
+
});
|
|
54787
|
+
|
|
54788
|
+
// src/snippet/generators.ts
|
|
54789
|
+
function wrapWithMarkers(level, body) {
|
|
54790
|
+
const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
|
|
54791
|
+
return `${head}${MARKER_BEGIN}
|
|
54792
|
+
${body}
|
|
54793
|
+
${MARKER_END}
|
|
54794
|
+
`;
|
|
54795
|
+
}
|
|
54796
|
+
function generateCursorSnippet(rules) {
|
|
54797
|
+
const lines = [];
|
|
54798
|
+
lines.push("# slopbrick cursor rules");
|
|
54799
|
+
lines.push("");
|
|
54800
|
+
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
54801
|
+
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
54802
|
+
lines.push("");
|
|
54803
|
+
lines.push("## Category-level directives");
|
|
54804
|
+
lines.push("");
|
|
54805
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54806
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54807
|
+
}
|
|
54808
|
+
lines.push("");
|
|
54809
|
+
lines.push("## Per-rule directives");
|
|
54810
|
+
lines.push("");
|
|
54811
|
+
const ai = aiSpecificRules(rules);
|
|
54812
|
+
const byCategory = categorizeRules(ai);
|
|
54813
|
+
for (const [category, list] of byCategory) {
|
|
54814
|
+
lines.push("### " + category);
|
|
54815
|
+
lines.push("");
|
|
54816
|
+
for (const rule of list) {
|
|
54817
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54818
|
+
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
54819
|
+
}
|
|
54820
|
+
lines.push("");
|
|
54821
|
+
}
|
|
54822
|
+
lines.push("## Verification");
|
|
54823
|
+
lines.push("");
|
|
54824
|
+
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
54825
|
+
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
54826
|
+
lines.push("");
|
|
54827
|
+
return lines.join("\n");
|
|
54828
|
+
}
|
|
54829
|
+
function generateAgentsMdSnippet(rules) {
|
|
54830
|
+
return wrapWithMarkers(
|
|
54831
|
+
1,
|
|
54832
|
+
baseContent(
|
|
54833
|
+
rules,
|
|
54834
|
+
"AGENTS.md",
|
|
54835
|
+
"Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
|
|
54836
|
+
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
|
|
54837
|
+
);
|
|
54838
|
+
}
|
|
54839
|
+
function generateClaudeMdSnippet(rules) {
|
|
54840
|
+
return wrapWithMarkers(
|
|
54841
|
+
1,
|
|
54842
|
+
baseContent(
|
|
54843
|
+
rules,
|
|
54844
|
+
"CLAUDE.md",
|
|
54845
|
+
"Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
|
|
54846
|
+
) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n"
|
|
54847
|
+
);
|
|
54848
|
+
}
|
|
54849
|
+
function generateAiderSnippet(rules) {
|
|
54850
|
+
return baseContent(
|
|
54851
|
+
rules,
|
|
54852
|
+
"CONVENTIONS.md",
|
|
54853
|
+
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
54854
|
+
);
|
|
54855
|
+
}
|
|
54856
|
+
function generateWindsurfSnippet(rules) {
|
|
54857
|
+
return baseContent(
|
|
54858
|
+
rules,
|
|
54859
|
+
".windsurfrules",
|
|
54860
|
+
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
54861
|
+
);
|
|
54862
|
+
}
|
|
54863
|
+
function generateClineRules(rules) {
|
|
54864
|
+
return baseContent(
|
|
54865
|
+
rules,
|
|
54866
|
+
"AGENTS.md",
|
|
54867
|
+
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
54868
|
+
);
|
|
54869
|
+
}
|
|
54870
|
+
function generateCursorrulesLegacySnippet(rules) {
|
|
54871
|
+
return baseContent(
|
|
54872
|
+
rules,
|
|
54873
|
+
".cursorrules",
|
|
54874
|
+
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
54875
|
+
);
|
|
54876
|
+
}
|
|
54877
|
+
function generateGeminiSnippet(rules) {
|
|
54878
|
+
return baseContent(
|
|
54879
|
+
rules,
|
|
54880
|
+
"GEMINI.md",
|
|
54881
|
+
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
54882
|
+
);
|
|
54883
|
+
}
|
|
54884
|
+
function generateCopilotSnippet(rules) {
|
|
54885
|
+
return baseContent(
|
|
54886
|
+
rules,
|
|
54887
|
+
"copilot-instructions.md",
|
|
54888
|
+
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
54889
|
+
);
|
|
54890
|
+
}
|
|
54891
|
+
var MARKER_BEGIN, MARKER_END;
|
|
54892
|
+
var init_generators = __esm({
|
|
54893
|
+
"src/snippet/generators.ts"() {
|
|
54894
|
+
"use strict";
|
|
54895
|
+
init_data();
|
|
54896
|
+
init_render2();
|
|
54897
|
+
MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
|
|
54898
|
+
MARKER_END = "<!-- slopbrick:end:v3 -->";
|
|
54899
|
+
}
|
|
54900
|
+
});
|
|
54901
|
+
|
|
54902
|
+
// src/snippet/render.ts
|
|
54903
|
+
function aiSpecificRules(rules) {
|
|
54904
|
+
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
54905
|
+
}
|
|
54906
|
+
function categorizeRules(rules) {
|
|
54907
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
54908
|
+
for (const r of rules) {
|
|
54909
|
+
const list = byCategory.get(r.category) ?? [];
|
|
54910
|
+
list.push(r);
|
|
54911
|
+
byCategory.set(r.category, list);
|
|
54912
|
+
}
|
|
54913
|
+
for (const list of byCategory.values()) {
|
|
54914
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
54915
|
+
}
|
|
54916
|
+
return byCategory;
|
|
54917
|
+
}
|
|
54918
|
+
function baseContent(rules, toolLabel, intro) {
|
|
54919
|
+
const lines = [];
|
|
54920
|
+
lines.push("# " + toolLabel);
|
|
54921
|
+
lines.push("");
|
|
54922
|
+
lines.push(intro);
|
|
54923
|
+
lines.push("");
|
|
54924
|
+
const ai = aiSpecificRules(rules);
|
|
54925
|
+
const byCategory = categorizeRules(ai);
|
|
54926
|
+
lines.push("## Category-level directives");
|
|
54927
|
+
lines.push("");
|
|
54928
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54929
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54930
|
+
}
|
|
54931
|
+
lines.push("");
|
|
54932
|
+
lines.push("## Per-rule directives");
|
|
54933
|
+
lines.push("");
|
|
54934
|
+
for (const [category, list] of byCategory) {
|
|
54935
|
+
lines.push("### " + category);
|
|
54936
|
+
lines.push("");
|
|
54937
|
+
for (const rule of list) {
|
|
54938
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54939
|
+
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
54940
|
+
}
|
|
54941
|
+
lines.push("");
|
|
54942
|
+
}
|
|
54943
|
+
return lines.join("\n");
|
|
54944
|
+
}
|
|
54945
|
+
function safeRewrite(existing, freshBlockBody) {
|
|
54946
|
+
const beginIdx = existing.indexOf(MARKER_BEGIN);
|
|
54947
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
54948
|
+
if (beginIdx === -1 && endIdx === -1) {
|
|
54949
|
+
return { content: existing, rewritten: false, failClosed: true, mismatched: false };
|
|
54950
|
+
}
|
|
54951
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
54952
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
54953
|
+
}
|
|
54954
|
+
if (endIdx <= beginIdx) {
|
|
54955
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
54956
|
+
}
|
|
54957
|
+
const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
|
|
54958
|
+
const after = existing.slice(endIdx);
|
|
54959
|
+
const next = `${before}
|
|
54960
|
+
${freshBlockBody}
|
|
54961
|
+
${after}`;
|
|
54962
|
+
return { content: next, rewritten: true, failClosed: false, mismatched: false };
|
|
54963
|
+
}
|
|
54964
|
+
var init_render2 = __esm({
|
|
54965
|
+
"src/snippet/render.ts"() {
|
|
54966
|
+
"use strict";
|
|
54967
|
+
init_data();
|
|
54968
|
+
init_generators();
|
|
54969
|
+
}
|
|
54970
|
+
});
|
|
54971
|
+
|
|
54972
|
+
// src/snippet/refresh.ts
|
|
54973
|
+
var refresh_exports = {};
|
|
54974
|
+
__export(refresh_exports, {
|
|
54975
|
+
SNIPPET_TARGETS: () => SNIPPET_TARGETS,
|
|
54976
|
+
refreshSnippets: () => refreshSnippets
|
|
54977
|
+
});
|
|
54978
|
+
import { existsSync as existsSync17, readFileSync as readFileSync21, writeFileSync as writeFileSync7 } from "fs";
|
|
54979
|
+
import { join as join19 } from "path";
|
|
54980
|
+
function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
|
|
54981
|
+
const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
|
|
54982
|
+
for (const filename of targets) {
|
|
54983
|
+
const path = join19(cwd, filename);
|
|
54984
|
+
if (!existsSync17(path)) {
|
|
54985
|
+
result.absent.push(filename);
|
|
54986
|
+
continue;
|
|
54987
|
+
}
|
|
54988
|
+
const existing = readFileSync21(path, "utf-8");
|
|
54989
|
+
const agentName = filename.replace(/\.md$/i, "").toLowerCase();
|
|
54990
|
+
const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
|
|
54991
|
+
const newBlock = generator([...rules]).trim();
|
|
54992
|
+
const outcome = safeRewrite(existing, newBlock);
|
|
54993
|
+
if (outcome.rewritten) {
|
|
54994
|
+
const tmp = `${path}.tmp`;
|
|
54995
|
+
writeFileSync7(tmp, outcome.content, "utf-8");
|
|
54996
|
+
const fs = __require("fs");
|
|
54997
|
+
fs.renameSync(tmp, path);
|
|
54998
|
+
result.rewritten.push(filename);
|
|
54999
|
+
} else if (outcome.failClosed) {
|
|
55000
|
+
result.failClosed.push(filename);
|
|
55001
|
+
} else if (outcome.mismatched) {
|
|
55002
|
+
result.mismatched.push(filename);
|
|
55003
|
+
}
|
|
55004
|
+
}
|
|
55005
|
+
return result;
|
|
55006
|
+
}
|
|
55007
|
+
var SNIPPET_TARGETS;
|
|
55008
|
+
var init_refresh = __esm({
|
|
55009
|
+
"src/snippet/refresh.ts"() {
|
|
55010
|
+
"use strict";
|
|
55011
|
+
init_render2();
|
|
55012
|
+
init_generators();
|
|
55013
|
+
SNIPPET_TARGETS = [
|
|
55014
|
+
"AGENTS.md",
|
|
55015
|
+
"CLAUDE.md"
|
|
55016
|
+
];
|
|
55017
|
+
}
|
|
55018
|
+
});
|
|
55019
|
+
|
|
54570
55020
|
// src/cli/report/persistRun.ts
|
|
54571
|
-
import { existsSync as
|
|
54572
|
-
import { join as
|
|
55021
|
+
import { existsSync as existsSync18, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
55022
|
+
import { join as join20, relative as relative10 } from "path";
|
|
54573
55023
|
async function persistRun(input) {
|
|
54574
55024
|
const {
|
|
54575
55025
|
cwd,
|
|
@@ -54651,20 +55101,20 @@ async function persistRun(input) {
|
|
|
54651
55101
|
report.research = state.research;
|
|
54652
55102
|
}
|
|
54653
55103
|
if (flywheelOutput.suggestions.length > 0) {
|
|
54654
|
-
const suggestionsDir =
|
|
54655
|
-
if (!
|
|
54656
|
-
|
|
54657
|
-
|
|
55104
|
+
const suggestionsDir = join20(cwd, ".slopbrick", "flywheel");
|
|
55105
|
+
if (!existsSync18(suggestionsDir)) mkdirSync7(suggestionsDir, { recursive: true });
|
|
55106
|
+
writeFileSync8(
|
|
55107
|
+
join20(suggestionsDir, "rule-suggestions.json"),
|
|
54658
55108
|
JSON.stringify(flywheelOutput.suggestions, null, 2)
|
|
54659
55109
|
);
|
|
54660
55110
|
}
|
|
54661
55111
|
report.issues.push(...flywheelOutput.hotspotIssues);
|
|
54662
55112
|
}
|
|
54663
|
-
|
|
55113
|
+
let patternInventory;
|
|
54664
55114
|
if (config.projectMemory !== false) {
|
|
54665
55115
|
try {
|
|
54666
55116
|
const durationMs = Date.now() - startTime;
|
|
54667
|
-
|
|
55117
|
+
patternInventory = await buildPatternInventory(cwd, config);
|
|
54668
55118
|
const inventory = buildInventoryFromScan(
|
|
54669
55119
|
{ cwd, results },
|
|
54670
55120
|
patternInventory,
|
|
@@ -54698,6 +55148,22 @@ async function persistRun(input) {
|
|
|
54698
55148
|
}
|
|
54699
55149
|
}
|
|
54700
55150
|
}
|
|
55151
|
+
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
55152
|
+
if (input.autoRefreshSnippets) {
|
|
55153
|
+
try {
|
|
55154
|
+
const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
55155
|
+
const outcome = refreshSnippets2(cwd, registry.getRules());
|
|
55156
|
+
if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
|
|
55157
|
+
logger.info(
|
|
55158
|
+
`Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
|
|
55159
|
+
);
|
|
55160
|
+
}
|
|
55161
|
+
} catch (err) {
|
|
55162
|
+
if (options.quiet !== true && !machineReadableStdout) {
|
|
55163
|
+
logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
55164
|
+
}
|
|
55165
|
+
}
|
|
55166
|
+
}
|
|
54701
55167
|
}
|
|
54702
55168
|
var init_persistRun = __esm({
|
|
54703
55169
|
"src/cli/report/persistRun.ts"() {
|
|
@@ -54918,7 +55384,7 @@ var init_json = __esm({
|
|
|
54918
55384
|
|
|
54919
55385
|
// src/report/sarif.ts
|
|
54920
55386
|
import { createHash as createHash11 } from "crypto";
|
|
54921
|
-
import { readFileSync as
|
|
55387
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
54922
55388
|
import { basename as basename3, isAbsolute as isAbsolute3, relative as relative12, resolve as resolve9 } from "path";
|
|
54923
55389
|
function buildArtifactUri(filePath, cwd) {
|
|
54924
55390
|
if (!filePath) {
|
|
@@ -55025,7 +55491,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
|
|
|
55025
55491
|
let content = fileContentCache.get(sourcePath);
|
|
55026
55492
|
if (content === void 0) {
|
|
55027
55493
|
try {
|
|
55028
|
-
content =
|
|
55494
|
+
content = readFileSync22(sourcePath, "utf-8");
|
|
55029
55495
|
fileContentCache.set(sourcePath, content);
|
|
55030
55496
|
} catch {
|
|
55031
55497
|
fileContentCache.set(sourcePath, "");
|
|
@@ -55078,6 +55544,7 @@ function formatSarif(report, options) {
|
|
|
55078
55544
|
const results = report.issues.map(
|
|
55079
55545
|
(issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
|
|
55080
55546
|
);
|
|
55547
|
+
const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
|
|
55081
55548
|
const log = {
|
|
55082
55549
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
55083
55550
|
version: "2.1.0",
|
|
@@ -55088,7 +55555,8 @@ function formatSarif(report, options) {
|
|
|
55088
55555
|
name: "slopbrick",
|
|
55089
55556
|
version: report.version,
|
|
55090
55557
|
informationUri: REPO_INFORMATION_URI,
|
|
55091
|
-
rules
|
|
55558
|
+
rules,
|
|
55559
|
+
...driverProperties ? { properties: driverProperties } : {}
|
|
55092
55560
|
}
|
|
55093
55561
|
},
|
|
55094
55562
|
results
|
|
@@ -56249,7 +56717,7 @@ var init_advice = __esm({
|
|
|
56249
56717
|
});
|
|
56250
56718
|
|
|
56251
56719
|
// src/fix/layout-token.ts
|
|
56252
|
-
import { readFileSync as
|
|
56720
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "fs";
|
|
56253
56721
|
function escapeRegExp(value) {
|
|
56254
56722
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
56255
56723
|
}
|
|
@@ -56286,10 +56754,10 @@ function applyReplaceFixes(content, fixes) {
|
|
|
56286
56754
|
return { content, applied, skipped, reasons };
|
|
56287
56755
|
}
|
|
56288
56756
|
function applyLayoutTokenFix(filePath, fixes) {
|
|
56289
|
-
let content =
|
|
56757
|
+
let content = readFileSync23(filePath, "utf-8");
|
|
56290
56758
|
const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
|
|
56291
56759
|
if (patched !== content) {
|
|
56292
|
-
|
|
56760
|
+
writeFileSync9(filePath, patched);
|
|
56293
56761
|
}
|
|
56294
56762
|
return { applied, skipped, reasons };
|
|
56295
56763
|
}
|
|
@@ -56300,7 +56768,7 @@ var init_layout_token = __esm({
|
|
|
56300
56768
|
});
|
|
56301
56769
|
|
|
56302
56770
|
// src/report/unified-diff.ts
|
|
56303
|
-
import { existsSync as
|
|
56771
|
+
import { existsSync as existsSync19, readFileSync as readFileSync24 } from "fs";
|
|
56304
56772
|
import { relative as relative13 } from "path";
|
|
56305
56773
|
function collectAllFixes(issue) {
|
|
56306
56774
|
return [...issue.fix ? [issue.fix] : [], ...issue.fixes ?? []];
|
|
@@ -56377,8 +56845,8 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56377
56845
|
const parts = [];
|
|
56378
56846
|
let hasHunk = false;
|
|
56379
56847
|
for (const [filePath, fixes] of byFile) {
|
|
56380
|
-
if (!
|
|
56381
|
-
const original =
|
|
56848
|
+
if (!existsSync19(filePath)) continue;
|
|
56849
|
+
const original = readFileSync24(filePath, "utf-8");
|
|
56382
56850
|
const patched = applyFixesToString(original, fixes);
|
|
56383
56851
|
if (patched === original) continue;
|
|
56384
56852
|
if (!hasHunk) {
|
|
@@ -56484,7 +56952,7 @@ var init_heatmap = __esm({
|
|
|
56484
56952
|
|
|
56485
56953
|
// src/cli/report/renderOutput.ts
|
|
56486
56954
|
import { resolve as resolve10 } from "path";
|
|
56487
|
-
import { writeFileSync as
|
|
56955
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
56488
56956
|
function renderOutput(report, options, cwd) {
|
|
56489
56957
|
if (options.format && !VALID_FORMATS.has(options.format)) {
|
|
56490
56958
|
process.stderr.write(
|
|
@@ -56518,7 +56986,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56518
56986
|
if (options.html) {
|
|
56519
56987
|
const html = formatHtml(report);
|
|
56520
56988
|
if (typeof options.html === "string") {
|
|
56521
|
-
|
|
56989
|
+
writeFileSync10(resolve10(options.html), html);
|
|
56522
56990
|
if (!options.quiet) {
|
|
56523
56991
|
logger.info(`Wrote HTML report to ${options.html}`);
|
|
56524
56992
|
}
|
|
@@ -56530,7 +56998,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56530
56998
|
if (options.json) {
|
|
56531
56999
|
const json = formatJson(report);
|
|
56532
57000
|
if (typeof options.json === "string") {
|
|
56533
|
-
|
|
57001
|
+
writeFileSync10(resolve10(options.json), json);
|
|
56534
57002
|
if (!options.quiet) {
|
|
56535
57003
|
logger.info(`Wrote JSON report to ${options.json}`);
|
|
56536
57004
|
}
|
|
@@ -56761,7 +57229,7 @@ var init_watch = __esm({
|
|
|
56761
57229
|
});
|
|
56762
57230
|
|
|
56763
57231
|
// src/cli/scan.ts
|
|
56764
|
-
import { existsSync as
|
|
57232
|
+
import { existsSync as existsSync20, statSync as statSync7 } from "fs";
|
|
56765
57233
|
import { resolve as resolve12, relative as relative15, extname as extname7, sep as sep3 } from "path";
|
|
56766
57234
|
import { randomUUID } from "crypto";
|
|
56767
57235
|
async function runScan(options, explicitPaths) {
|
|
@@ -56769,7 +57237,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56769
57237
|
const startTime = Date.now();
|
|
56770
57238
|
const scanId = randomUUID();
|
|
56771
57239
|
const cwd = resolve12(options.workspace ?? process.cwd());
|
|
56772
|
-
if (!
|
|
57240
|
+
if (!existsSync20(cwd)) {
|
|
56773
57241
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
56774
57242
|
}
|
|
56775
57243
|
const cwdStat = statSync7(cwd);
|
|
@@ -56814,7 +57282,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56814
57282
|
const resolved = explicitPaths.map((p) => resolve12(cwd, p));
|
|
56815
57283
|
const expanded = [];
|
|
56816
57284
|
for (const p of resolved) {
|
|
56817
|
-
if (
|
|
57285
|
+
if (existsSync20(p) && statSync7(p).isDirectory()) {
|
|
56818
57286
|
const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
|
|
56819
57287
|
for (const f of found) {
|
|
56820
57288
|
if (!ALL_SOURCE_EXTENSIONS.has(extname7(f).toLowerCase())) continue;
|
|
@@ -56968,7 +57436,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56968
57436
|
const scannedPaths = new Set(results.map((result) => result.filePath));
|
|
56969
57437
|
for (const [filePath, cached] of Object.entries(baseline.scores)) {
|
|
56970
57438
|
if (scannedPaths.has(filePath)) continue;
|
|
56971
|
-
if (!
|
|
57439
|
+
if (!existsSync20(filePath)) continue;
|
|
56972
57440
|
scores.push({
|
|
56973
57441
|
filePath,
|
|
56974
57442
|
rawScore: 0,
|
|
@@ -57007,7 +57475,12 @@ async function runScan(options, explicitPaths) {
|
|
|
57007
57475
|
const { report, noIncreaseFailure } = await finalizeReport({
|
|
57008
57476
|
cwd,
|
|
57009
57477
|
config,
|
|
57010
|
-
options
|
|
57478
|
+
options: {
|
|
57479
|
+
...options,
|
|
57480
|
+
// v0.42.0 (§3a.4): forward the opt-in flag through to
|
|
57481
|
+
// `persistRun`, which calls `refreshSnippets` post-scan.
|
|
57482
|
+
autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
|
|
57483
|
+
},
|
|
57011
57484
|
results,
|
|
57012
57485
|
aggregated,
|
|
57013
57486
|
allIssues,
|
|
@@ -57075,396 +57548,44 @@ var init_scan2 = __esm({
|
|
|
57075
57548
|
}
|
|
57076
57549
|
});
|
|
57077
57550
|
|
|
57078
|
-
// src/
|
|
57079
|
-
|
|
57080
|
-
|
|
57081
|
-
|
|
57082
|
-
|
|
57083
|
-
content: [{ type: "text", text: cached }]
|
|
57084
|
-
};
|
|
57085
|
-
}
|
|
57086
|
-
const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
57087
|
-
const result = await handleToolCall2("slop_suggest", args, ctx);
|
|
57088
|
-
if (result.isError) return result;
|
|
57089
|
-
try {
|
|
57090
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
57091
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
57092
|
-
parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
57093
|
-
return {
|
|
57094
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
57095
|
-
};
|
|
57096
|
-
}
|
|
57097
|
-
} catch {
|
|
57098
|
-
}
|
|
57099
|
-
return result;
|
|
57100
|
-
}
|
|
57101
|
-
var STRUCTURE_NOT_FOUND_HINT;
|
|
57102
|
-
var init_slop_suggest_structure = __esm({
|
|
57103
|
-
"src/mcp/slop-suggest-structure.ts"() {
|
|
57104
|
-
"use strict";
|
|
57105
|
-
init_structure_md();
|
|
57106
|
-
STRUCTURE_NOT_FOUND_HINT = "No .slopbrick/structure.md found. Run `slopbrick scan` to persist the pattern inventory, then call this tool again for the O(read file) fast path.";
|
|
57107
|
-
}
|
|
57108
|
-
});
|
|
57109
|
-
|
|
57110
|
-
// src/mcp/tools.ts
|
|
57111
|
-
var tools_exports = {};
|
|
57112
|
-
__export(tools_exports, {
|
|
57113
|
-
TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
|
|
57114
|
-
canonicalToolNames: () => canonicalToolNames,
|
|
57115
|
-
getDeprecation: () => getDeprecation,
|
|
57116
|
-
handleToolCall: () => handleToolCall
|
|
57551
|
+
// src/rules/composite-seed.ts
|
|
57552
|
+
var composite_seed_exports = {};
|
|
57553
|
+
__export(composite_seed_exports, {
|
|
57554
|
+
HAND_CURATED_SEED: () => HAND_CURATED_SEED,
|
|
57555
|
+
maybeAppendSeed: () => maybeAppendSeed
|
|
57117
57556
|
});
|
|
57118
|
-
|
|
57119
|
-
|
|
57120
|
-
|
|
57121
|
-
return {
|
|
57122
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
57123
|
-
isError: true
|
|
57124
|
-
};
|
|
57125
|
-
}
|
|
57126
|
-
async function runScanFile(args, ctx) {
|
|
57127
|
-
const path = args.path;
|
|
57128
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57129
|
-
const result = await scanFile(path, ctx.config);
|
|
57130
|
-
const simplified = {
|
|
57131
|
-
filePath: result.filePath,
|
|
57132
|
-
componentCount: result.componentCount,
|
|
57133
|
-
parseError: result.parseError,
|
|
57134
|
-
issues: result.issues.map((i) => ({
|
|
57135
|
-
ruleId: i.ruleId,
|
|
57136
|
-
category: i.category,
|
|
57137
|
-
severity: i.severity,
|
|
57138
|
-
line: i.line,
|
|
57139
|
-
column: i.column,
|
|
57140
|
-
message: i.message,
|
|
57141
|
-
advice: i.advice
|
|
57142
|
-
}))
|
|
57143
|
-
};
|
|
57144
|
-
return {
|
|
57145
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
57146
|
-
};
|
|
57147
|
-
}
|
|
57148
|
-
function explainRule2(args, ctx) {
|
|
57149
|
-
const ruleId = args.ruleId;
|
|
57150
|
-
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
57151
|
-
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
57152
|
-
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
57153
|
-
const explanation = {
|
|
57154
|
-
ruleId: rule.id,
|
|
57155
|
-
category: rule.category,
|
|
57156
|
-
severity: rule.severity,
|
|
57157
|
-
aiSpecific: rule.aiSpecific,
|
|
57158
|
-
rationale: "This rule flags " + rule.category + " patterns associated with AI-generated code. It is marked as " + (rule.aiSpecific ? "AI-specific" : "cross-cutting") + ". Severity: " + rule.severity + ".",
|
|
57159
|
-
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
57160
|
-
};
|
|
57161
|
-
return {
|
|
57162
|
-
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
57163
|
-
};
|
|
57164
|
-
}
|
|
57165
|
-
function listRules(args, ctx) {
|
|
57166
|
-
const category = args.category;
|
|
57167
|
-
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
57168
|
-
const rules = filtered.map((r) => ({
|
|
57169
|
-
id: r.id,
|
|
57170
|
-
category: r.category,
|
|
57171
|
-
severity: r.severity,
|
|
57172
|
-
aiSpecific: r.aiSpecific
|
|
57173
|
-
}));
|
|
57174
|
-
return {
|
|
57175
|
-
content: [
|
|
57176
|
-
{
|
|
57177
|
-
type: "text",
|
|
57178
|
-
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
57179
|
-
}
|
|
57180
|
-
]
|
|
57181
|
-
};
|
|
57182
|
-
}
|
|
57183
|
-
async function runSuggest(args, ctx) {
|
|
57184
|
-
const maxFilesRaw = args.maxFiles;
|
|
57185
|
-
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
57186
|
-
try {
|
|
57187
|
-
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
57188
|
-
const doNotCreate = [
|
|
57189
|
-
...ctx.config.constitution?.forbidden ?? []
|
|
57190
|
-
];
|
|
57191
|
-
const declared = /* @__PURE__ */ new Set();
|
|
57192
|
-
for (const list of [
|
|
57193
|
-
ctx.config.constitution?.stateManagement ?? [],
|
|
57194
|
-
ctx.config.constitution?.dataFetching ?? [],
|
|
57195
|
-
ctx.config.constitution?.uiLibrary ?? [],
|
|
57196
|
-
ctx.config.constitution?.forms ?? [],
|
|
57197
|
-
ctx.config.constitution?.styling ?? [],
|
|
57198
|
-
ctx.config.constitution?.routing ?? []
|
|
57199
|
-
]) {
|
|
57200
|
-
for (const lib of list) declared.add(lib);
|
|
57201
|
-
}
|
|
57202
|
-
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
57203
|
-
return {
|
|
57204
|
-
content: [
|
|
57205
|
-
{
|
|
57206
|
-
type: "text",
|
|
57207
|
-
text: JSON.stringify(
|
|
57208
|
-
{
|
|
57209
|
-
hint: "Use these patterns instead of creating new ones. Pick the closest existing entry and import it. The `doNotCreate` list is the deny-list \u2014 never import any of these.",
|
|
57210
|
-
doNotCreate: doNotCreateCapped,
|
|
57211
|
-
declaredStack: Array.from(declared),
|
|
57212
|
-
existingPatterns: inventory
|
|
57213
|
-
},
|
|
57214
|
-
null,
|
|
57215
|
-
2
|
|
57216
|
-
)
|
|
57217
|
-
}
|
|
57218
|
-
]
|
|
57219
|
-
};
|
|
57220
|
-
} catch (err) {
|
|
57221
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57222
|
-
}
|
|
57223
|
-
}
|
|
57224
|
-
function runCheckConstitution(args, ctx) {
|
|
57225
|
-
const path = args.path;
|
|
57226
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57227
|
-
const absPath = resolve17(ctx.cwd, path);
|
|
57228
|
-
let source;
|
|
57229
|
-
try {
|
|
57230
|
-
source = readFileSync27(absPath, "utf-8");
|
|
57231
|
-
} catch (err) {
|
|
57232
|
-
return toolError(
|
|
57233
|
-
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
57234
|
-
);
|
|
57235
|
-
}
|
|
57236
|
-
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
57237
|
-
return {
|
|
57238
|
-
content: [
|
|
57239
|
-
{
|
|
57240
|
-
type: "text",
|
|
57241
|
-
text: JSON.stringify(
|
|
57242
|
-
{
|
|
57243
|
-
file: absPath,
|
|
57244
|
-
importCount: result.imports.length,
|
|
57245
|
-
violationCount: result.violations.length,
|
|
57246
|
-
imports: result.imports,
|
|
57247
|
-
violations: result.violations,
|
|
57248
|
-
// Field name kept stable for backward compatibility with
|
|
57249
|
-
// older consumers; the value reflects whether the merged
|
|
57250
|
-
// `config.constitution` was declared, detected, or absent.
|
|
57251
|
-
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
57252
|
-
},
|
|
57253
|
-
null,
|
|
57254
|
-
2
|
|
57255
|
-
)
|
|
57256
|
-
}
|
|
57257
|
-
]
|
|
57258
|
-
};
|
|
57259
|
-
}
|
|
57260
|
-
async function runFindSimilar(args, ctx) {
|
|
57261
|
-
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57262
|
-
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
57263
|
-
const props = Array.isArray(args.props) ? args.props : [];
|
|
57264
|
-
const limitRaw = args.limit;
|
|
57265
|
-
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
57266
|
-
try {
|
|
57267
|
-
const matches = await findSimilarFunctions2(
|
|
57268
|
-
{
|
|
57269
|
-
name: typeof args.name === "string" ? args.name : void 0,
|
|
57270
|
-
hooks,
|
|
57271
|
-
props,
|
|
57272
|
-
limit,
|
|
57273
|
-
workspaceDir: ctx.cwd
|
|
57274
|
-
},
|
|
57275
|
-
{ cwd: ctx.cwd }
|
|
57276
|
-
);
|
|
57277
|
-
return {
|
|
57278
|
-
content: [
|
|
57279
|
-
{
|
|
57280
|
-
type: "text",
|
|
57281
|
-
text: JSON.stringify(
|
|
57282
|
-
{
|
|
57283
|
-
hint: "Each match is ranked by Jaccard similarity over (hooks \u222A props \u222A params). similarity=1 means the matched signature has an identical feature set. Agents should prefer the top match instead of writing a new implementation.",
|
|
57284
|
-
count: matches.length,
|
|
57285
|
-
matches: matches.map((m) => ({
|
|
57286
|
-
name: m.signature.name,
|
|
57287
|
-
file: m.signature.fileRel,
|
|
57288
|
-
line: m.signature.line,
|
|
57289
|
-
similarity: Number(m.similarity.toFixed(3)),
|
|
57290
|
-
fingerprint: m.fingerprint,
|
|
57291
|
-
hooks: m.signature.hooks,
|
|
57292
|
-
props: m.signature.props,
|
|
57293
|
-
params: m.signature.params
|
|
57294
|
-
}))
|
|
57295
|
-
},
|
|
57296
|
-
null,
|
|
57297
|
-
2
|
|
57298
|
-
)
|
|
57299
|
-
}
|
|
57300
|
-
]
|
|
57301
|
-
};
|
|
57302
|
-
} catch (err) {
|
|
57303
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57304
|
-
}
|
|
57305
|
-
}
|
|
57306
|
-
async function handleToolCall(toolName, args, ctx) {
|
|
57307
|
-
const deprecation = getDeprecation(toolName);
|
|
57308
|
-
const deprecationNotice = deprecation ? {
|
|
57309
|
-
tool: toolName,
|
|
57310
|
-
replacedBy: deprecation.replacedBy,
|
|
57311
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
57312
|
-
reason: deprecation.reason
|
|
57313
|
-
} : void 0;
|
|
57314
|
-
switch (toolName) {
|
|
57315
|
-
case "slop_scan_file":
|
|
57316
|
-
return runScanFile(args, ctx);
|
|
57317
|
-
case "slop_explain_rule":
|
|
57318
|
-
return explainRule2(args, ctx);
|
|
57319
|
-
case "slop_list_rules":
|
|
57320
|
-
return listRules(args, ctx);
|
|
57321
|
-
case "slop_suggest":
|
|
57322
|
-
return runSuggest(args, ctx);
|
|
57323
|
-
case "slop_suggest_with_structure":
|
|
57324
|
-
return runSuggestWithStructure(args, ctx);
|
|
57325
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57326
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57327
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57328
|
-
// Their runner functions (runGovernance, runArchitectureScore,
|
|
57329
|
-
// runBusinessLogicScore) are kept in the file for now
|
|
57330
|
-
// (marked @deprecated) to keep the diff small; they can be
|
|
57331
|
-
// deleted in a follow-up. New clients will never see these
|
|
57332
|
-
// tools listed in the MCP tools/list response.
|
|
57333
|
-
case "slop_check_constitution":
|
|
57334
|
-
return runCheckConstitution(args, ctx);
|
|
57335
|
-
case "slop_find_similar":
|
|
57336
|
-
return runFindSimilar(args, ctx);
|
|
57337
|
-
default:
|
|
57338
|
-
return toolError("Unknown tool: " + toolName);
|
|
57557
|
+
function maybeAppendSeed(entries) {
|
|
57558
|
+
if (entries.length === 0) {
|
|
57559
|
+
return [...HAND_CURATED_SEED];
|
|
57339
57560
|
}
|
|
57561
|
+
return entries;
|
|
57340
57562
|
}
|
|
57341
|
-
|
|
57342
|
-
|
|
57343
|
-
|
|
57344
|
-
function getDeprecation(toolName) {
|
|
57345
|
-
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
57346
|
-
}
|
|
57347
|
-
var TOOL_DEFINITIONS;
|
|
57348
|
-
var init_tools = __esm({
|
|
57349
|
-
"src/mcp/tools.ts"() {
|
|
57563
|
+
var HAND_CURATED_SEED;
|
|
57564
|
+
var init_composite_seed = __esm({
|
|
57565
|
+
"src/rules/composite-seed.ts"() {
|
|
57350
57566
|
"use strict";
|
|
57351
|
-
|
|
57352
|
-
init_patterns();
|
|
57353
|
-
init_architecture_score();
|
|
57354
|
-
init_business_logic();
|
|
57355
|
-
init_slop_suggest_structure();
|
|
57356
|
-
TOOL_DEFINITIONS = [
|
|
57357
|
-
{
|
|
57358
|
-
name: "slop_scan_file",
|
|
57359
|
-
description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
|
|
57360
|
-
inputSchema: {
|
|
57361
|
-
type: "object",
|
|
57362
|
-
properties: {
|
|
57363
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
57364
|
-
framework: {
|
|
57365
|
-
type: "string",
|
|
57366
|
-
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
57367
|
-
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
57368
|
-
}
|
|
57369
|
-
},
|
|
57370
|
-
required: ["path"]
|
|
57371
|
-
}
|
|
57372
|
-
},
|
|
57373
|
-
{
|
|
57374
|
-
name: "slop_explain_rule",
|
|
57375
|
-
description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
|
|
57376
|
-
inputSchema: {
|
|
57377
|
-
type: "object",
|
|
57378
|
-
properties: {
|
|
57379
|
-
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
57380
|
-
},
|
|
57381
|
-
required: ["ruleId"]
|
|
57382
|
-
}
|
|
57383
|
-
},
|
|
57384
|
-
{
|
|
57385
|
-
name: "slop_list_rules",
|
|
57386
|
-
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
57387
|
-
inputSchema: {
|
|
57388
|
-
type: "object",
|
|
57389
|
-
properties: {
|
|
57390
|
-
category: { type: "string", description: "Optional category filter." }
|
|
57391
|
-
}
|
|
57392
|
-
}
|
|
57393
|
-
},
|
|
57567
|
+
HAND_CURATED_SEED = [
|
|
57394
57568
|
{
|
|
57395
|
-
|
|
57396
|
-
|
|
57397
|
-
|
|
57398
|
-
|
|
57399
|
-
|
|
57400
|
-
|
|
57401
|
-
|
|
57402
|
-
|
|
57403
|
-
|
|
57404
|
-
|
|
57405
|
-
|
|
57406
|
-
|
|
57407
|
-
|
|
57408
|
-
|
|
57409
|
-
|
|
57410
|
-
|
|
57411
|
-
|
|
57412
|
-
|
|
57413
|
-
|
|
57414
|
-
|
|
57415
|
-
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
57416
|
-
}
|
|
57417
|
-
}
|
|
57418
|
-
}
|
|
57419
|
-
},
|
|
57420
|
-
{
|
|
57421
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57422
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57423
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57424
|
-
// They were strict subsets of slop_suggest; users should
|
|
57425
|
-
// call slop_suggest and read repositoryHealth /
|
|
57426
|
-
// architectureConsistency / businessLogicCoherence.
|
|
57427
|
-
name: "slop_check_constitution",
|
|
57428
|
-
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
|
|
57429
|
-
inputSchema: {
|
|
57430
|
-
type: "object",
|
|
57431
|
-
properties: {
|
|
57432
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
57433
|
-
},
|
|
57434
|
-
required: ["path"]
|
|
57435
|
-
}
|
|
57436
|
-
},
|
|
57437
|
-
{
|
|
57438
|
-
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
57439
|
-
// Reference) primitive for slop_suggest. Given a function signature
|
|
57440
|
-
// (name + hooks + props), find the most similar existing
|
|
57441
|
-
// implementations across the codebase. Uses AST fingerprints
|
|
57442
|
-
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
57443
|
-
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
57444
|
-
name: "slop_find_similar",
|
|
57445
|
-
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
|
|
57446
|
-
inputSchema: {
|
|
57447
|
-
type: "object",
|
|
57448
|
-
properties: {
|
|
57449
|
-
name: {
|
|
57450
|
-
type: "string",
|
|
57451
|
-
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
57452
|
-
},
|
|
57453
|
-
hooks: {
|
|
57454
|
-
type: "array",
|
|
57455
|
-
items: { type: "string" },
|
|
57456
|
-
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
57457
|
-
},
|
|
57458
|
-
props: {
|
|
57459
|
-
type: "array",
|
|
57460
|
-
items: { type: "string" },
|
|
57461
|
-
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
57462
|
-
},
|
|
57463
|
-
limit: {
|
|
57464
|
-
type: "number",
|
|
57465
|
-
description: "Top-k results to return. Default 10. Capped at 50."
|
|
57466
|
-
}
|
|
57467
|
-
}
|
|
57569
|
+
id: "composite/ai-fingerprint-cluster",
|
|
57570
|
+
ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
|
|
57571
|
+
minMatch: 2,
|
|
57572
|
+
severity: "medium",
|
|
57573
|
+
defaultOff: true,
|
|
57574
|
+
description: "Composite rule: at least 2 of {ai/compression-profile+ai/comment-ratio+...} fire on the same file. Hand-curated seed from the v0.42.0 brief; superseded by `slopbrick composite discover` once empirical clusters dominate the ledger.",
|
|
57575
|
+
calibration: {
|
|
57576
|
+
recall: 0,
|
|
57577
|
+
FP: 0,
|
|
57578
|
+
precision: 0,
|
|
57579
|
+
F1: 0,
|
|
57580
|
+
nFiles: 0
|
|
57581
|
+
},
|
|
57582
|
+
provenance: {
|
|
57583
|
+
seed: "hand-curated-by-brief",
|
|
57584
|
+
discoveredAt: "2026-07-05T00:00:00.000Z",
|
|
57585
|
+
nFiles: 0,
|
|
57586
|
+
members: 3,
|
|
57587
|
+
npmi: 0,
|
|
57588
|
+
fisherP: 1
|
|
57468
57589
|
}
|
|
57469
57590
|
}
|
|
57470
57591
|
];
|
|
@@ -57477,7 +57598,7 @@ init_config2();
|
|
|
57477
57598
|
init_dist2();
|
|
57478
57599
|
|
|
57479
57600
|
// src/cli/program.ts
|
|
57480
|
-
import { resolve as
|
|
57601
|
+
import { resolve as resolve42 } from "path";
|
|
57481
57602
|
import { Command } from "commander";
|
|
57482
57603
|
|
|
57483
57604
|
// src/cli/options.ts
|
|
@@ -57525,13 +57646,13 @@ init_discover();
|
|
|
57525
57646
|
init_git();
|
|
57526
57647
|
init_cache();
|
|
57527
57648
|
init_logger();
|
|
57528
|
-
import { writeFileSync as
|
|
57529
|
-
import { join as
|
|
57649
|
+
import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync9, rmSync as rmSync2 } from "fs";
|
|
57650
|
+
import { join as join23, dirname as dirname14 } from "path";
|
|
57530
57651
|
import { createInterface } from "readline";
|
|
57531
57652
|
|
|
57532
57653
|
// src/rules/registry-loader.ts
|
|
57533
|
-
import { existsSync as
|
|
57534
|
-
import { dirname as dirname13, join as
|
|
57654
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync8, readFileSync as readFileSync25, writeFileSync as writeFileSync11 } from "fs";
|
|
57655
|
+
import { dirname as dirname13, join as join21 } from "path";
|
|
57535
57656
|
|
|
57536
57657
|
// src/data/shadcn-registry.json
|
|
57537
57658
|
var shadcn_registry_default = {
|
|
@@ -57665,11 +57786,11 @@ var shadcn_registry_default = {
|
|
|
57665
57786
|
var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
|
|
57666
57787
|
var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
|
|
57667
57788
|
function cachePath2(cwd) {
|
|
57668
|
-
return
|
|
57789
|
+
return join21(cwd, ".slopbrick", "cache", "registry-snapshot.json");
|
|
57669
57790
|
}
|
|
57670
57791
|
function ensureCacheDir(cwd) {
|
|
57671
57792
|
const dir = dirname13(cachePath2(cwd));
|
|
57672
|
-
if (!
|
|
57793
|
+
if (!existsSync21(dir)) {
|
|
57673
57794
|
mkdirSync8(dir, { recursive: true });
|
|
57674
57795
|
}
|
|
57675
57796
|
}
|
|
@@ -57682,9 +57803,9 @@ function isValidSnapshot(value) {
|
|
|
57682
57803
|
}
|
|
57683
57804
|
function isRegistryFresh(cwd) {
|
|
57684
57805
|
const cached = cachePath2(cwd);
|
|
57685
|
-
if (!
|
|
57806
|
+
if (!existsSync21(cached)) return false;
|
|
57686
57807
|
try {
|
|
57687
|
-
const parsed = JSON.parse(
|
|
57808
|
+
const parsed = JSON.parse(readFileSync25(cached, "utf8"));
|
|
57688
57809
|
if (!isValidSnapshot(parsed)) return false;
|
|
57689
57810
|
return parsed.version === BUNDLED_REGISTRY_VERSION;
|
|
57690
57811
|
} catch {
|
|
@@ -57695,7 +57816,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57695
57816
|
try {
|
|
57696
57817
|
const cacheFile = cachePath2(cwd);
|
|
57697
57818
|
ensureCacheDir(cwd);
|
|
57698
|
-
|
|
57819
|
+
writeFileSync11(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
|
|
57699
57820
|
} catch {
|
|
57700
57821
|
}
|
|
57701
57822
|
return {
|
|
@@ -57707,7 +57828,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57707
57828
|
}
|
|
57708
57829
|
function copyBundledSnapshotToCache(cwd) {
|
|
57709
57830
|
ensureCacheDir(cwd);
|
|
57710
|
-
|
|
57831
|
+
writeFileSync11(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
|
|
57711
57832
|
}
|
|
57712
57833
|
|
|
57713
57834
|
// src/cli/init.ts
|
|
@@ -57736,17 +57857,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
57736
57857
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
57737
57858
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
57738
57859
|
function promptText(rl, question, detected) {
|
|
57739
|
-
return new Promise((
|
|
57860
|
+
return new Promise((resolve43) => {
|
|
57740
57861
|
const lines = [
|
|
57741
57862
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
57742
57863
|
];
|
|
57743
57864
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57744
57865
|
const trimmed = answer.trim();
|
|
57745
57866
|
if (trimmed === "") {
|
|
57746
|
-
|
|
57867
|
+
resolve43(void 0);
|
|
57747
57868
|
return;
|
|
57748
57869
|
}
|
|
57749
|
-
|
|
57870
|
+
resolve43(trimmed);
|
|
57750
57871
|
});
|
|
57751
57872
|
});
|
|
57752
57873
|
}
|
|
@@ -57754,7 +57875,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57754
57875
|
const defaultIndex = options.indexOf(defaultValue);
|
|
57755
57876
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
57756
57877
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
57757
|
-
return new Promise((
|
|
57878
|
+
return new Promise((resolve43) => {
|
|
57758
57879
|
function ask() {
|
|
57759
57880
|
const lines = [
|
|
57760
57881
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -57764,7 +57885,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57764
57885
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57765
57886
|
const trimmed = answer.trim();
|
|
57766
57887
|
if (trimmed === "") {
|
|
57767
|
-
|
|
57888
|
+
resolve43(safeDefaultValue);
|
|
57768
57889
|
return;
|
|
57769
57890
|
}
|
|
57770
57891
|
const num = parseInt(trimmed, 10);
|
|
@@ -57773,7 +57894,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57773
57894
|
ask();
|
|
57774
57895
|
return;
|
|
57775
57896
|
}
|
|
57776
|
-
|
|
57897
|
+
resolve43(options[num - 1]);
|
|
57777
57898
|
});
|
|
57778
57899
|
}
|
|
57779
57900
|
ask();
|
|
@@ -57783,7 +57904,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57783
57904
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
57784
57905
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
57785
57906
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
57786
|
-
return new Promise((
|
|
57907
|
+
return new Promise((resolve43) => {
|
|
57787
57908
|
function ask() {
|
|
57788
57909
|
const lines = [
|
|
57789
57910
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -57794,7 +57915,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57794
57915
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57795
57916
|
const trimmed = answer.trim();
|
|
57796
57917
|
if (trimmed === "") {
|
|
57797
|
-
|
|
57918
|
+
resolve43(defaultValue.length > 0 ? defaultValue : []);
|
|
57798
57919
|
return;
|
|
57799
57920
|
}
|
|
57800
57921
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -57804,11 +57925,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57804
57925
|
return;
|
|
57805
57926
|
}
|
|
57806
57927
|
if (numbers.includes(1)) {
|
|
57807
|
-
|
|
57928
|
+
resolve43([]);
|
|
57808
57929
|
return;
|
|
57809
57930
|
}
|
|
57810
57931
|
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
57811
|
-
|
|
57932
|
+
resolve43(selected);
|
|
57812
57933
|
});
|
|
57813
57934
|
}
|
|
57814
57935
|
ask();
|
|
@@ -57896,9 +58017,9 @@ async function runDoctor(cwd) {
|
|
|
57896
58017
|
}
|
|
57897
58018
|
try {
|
|
57898
58019
|
const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57899
|
-
const testFile =
|
|
58020
|
+
const testFile = join23(cwd, ".slopbrick", ".doctor-test.ts");
|
|
57900
58021
|
mkdirSync9(dirname14(testFile), { recursive: true });
|
|
57901
|
-
|
|
58022
|
+
writeFileSync12(testFile, "export const x = 1;\n");
|
|
57902
58023
|
await tryParse(testFile);
|
|
57903
58024
|
rmSync2(testFile, { force: true });
|
|
57904
58025
|
ok("Parser is working.");
|
|
@@ -58032,6 +58153,470 @@ function registerSuggest(program) {
|
|
|
58032
58153
|
});
|
|
58033
58154
|
}
|
|
58034
58155
|
|
|
58156
|
+
// src/cli/commands/composite.ts
|
|
58157
|
+
init_logger();
|
|
58158
|
+
init_telemetry();
|
|
58159
|
+
import { resolve as resolve15 } from "path";
|
|
58160
|
+
|
|
58161
|
+
// src/engine/cluster.ts
|
|
58162
|
+
import { createHash as createHash12 } from "crypto";
|
|
58163
|
+
var DEFAULT_CLUSTER_PARAMS = {
|
|
58164
|
+
minSupport: 0.05,
|
|
58165
|
+
// 5% of files
|
|
58166
|
+
minNPMI: 0.3,
|
|
58167
|
+
// Cohen's-Landis-and-Koch "fair" agreement
|
|
58168
|
+
fisherAlpha: 0.01,
|
|
58169
|
+
// Bonferroni-style floor for many-pair tests
|
|
58170
|
+
minClusterSize: 2
|
|
58171
|
+
};
|
|
58172
|
+
function adaptiveMinSupport(fileCount) {
|
|
58173
|
+
if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
|
|
58174
|
+
return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
|
|
58175
|
+
}
|
|
58176
|
+
function computeRuleSupport(fireMatrix) {
|
|
58177
|
+
const support = /* @__PURE__ */ new Map();
|
|
58178
|
+
for (const ruleIds of fireMatrix.values()) {
|
|
58179
|
+
for (const ruleId of ruleIds) {
|
|
58180
|
+
support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
|
|
58181
|
+
}
|
|
58182
|
+
}
|
|
58183
|
+
return support;
|
|
58184
|
+
}
|
|
58185
|
+
function prefilterBySupport(support, totalFiles, minSupport) {
|
|
58186
|
+
const kept = /* @__PURE__ */ new Set();
|
|
58187
|
+
const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
|
|
58188
|
+
for (const [ruleId, count] of support) {
|
|
58189
|
+
if (count >= minCount) kept.add(ruleId);
|
|
58190
|
+
}
|
|
58191
|
+
return kept;
|
|
58192
|
+
}
|
|
58193
|
+
function buildContingency(rA, rB, totalFiles) {
|
|
58194
|
+
let aAndB = 0;
|
|
58195
|
+
let aOnly = 0;
|
|
58196
|
+
let bOnly = 0;
|
|
58197
|
+
for (const file of rA) {
|
|
58198
|
+
if (rB.has(file)) aAndB++;
|
|
58199
|
+
else aOnly++;
|
|
58200
|
+
}
|
|
58201
|
+
for (const file of rB) {
|
|
58202
|
+
if (!rA.has(file)) bOnly++;
|
|
58203
|
+
}
|
|
58204
|
+
return {
|
|
58205
|
+
aAndB,
|
|
58206
|
+
aOnly,
|
|
58207
|
+
bOnly,
|
|
58208
|
+
neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
|
|
58209
|
+
};
|
|
58210
|
+
}
|
|
58211
|
+
function fisherExactTwoSided(c) {
|
|
58212
|
+
const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
|
|
58213
|
+
const K1 = c.aAndB + c.aOnly;
|
|
58214
|
+
const K2 = c.aAndB + c.bOnly;
|
|
58215
|
+
const n = K1;
|
|
58216
|
+
const kObs = c.aAndB;
|
|
58217
|
+
const logPmf = (k) => {
|
|
58218
|
+
if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
|
|
58219
|
+
return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
|
|
58220
|
+
};
|
|
58221
|
+
const pObs = Math.exp(logPmf(kObs));
|
|
58222
|
+
const kMin = Math.max(0, K1 + K2 - N);
|
|
58223
|
+
const kMax = Math.min(K1, K2);
|
|
58224
|
+
let p = 0;
|
|
58225
|
+
for (let k = kMin; k <= kMax; k++) {
|
|
58226
|
+
const pk = Math.exp(logPmf(k));
|
|
58227
|
+
if (pk <= pObs + 1e-12) {
|
|
58228
|
+
p += pk;
|
|
58229
|
+
}
|
|
58230
|
+
}
|
|
58231
|
+
return Math.min(1, p);
|
|
58232
|
+
}
|
|
58233
|
+
function lnChoose(n, k) {
|
|
58234
|
+
if (k < 0 || k > n) return -Infinity;
|
|
58235
|
+
if (k === 0 || k === n) return 0;
|
|
58236
|
+
k = Math.min(k, n - k);
|
|
58237
|
+
let s = 0;
|
|
58238
|
+
for (let i = 1; i <= k; i++) {
|
|
58239
|
+
s += Math.log((n - k + i) / i);
|
|
58240
|
+
}
|
|
58241
|
+
return s;
|
|
58242
|
+
}
|
|
58243
|
+
function npmi(c, total) {
|
|
58244
|
+
if (total === 0 || c.aAndB === 0) return -1;
|
|
58245
|
+
const pXY = c.aAndB / total;
|
|
58246
|
+
const pX = (c.aAndB + c.aOnly) / total;
|
|
58247
|
+
const pY = (c.aAndB + c.bOnly) / total;
|
|
58248
|
+
if (pX === 0 || pY === 0 || pXY === 0) return -1;
|
|
58249
|
+
const pmi = Math.log2(pXY / (pX * pY));
|
|
58250
|
+
const norm = -Math.log2(pXY);
|
|
58251
|
+
return norm === 0 ? 1 : pmi / norm;
|
|
58252
|
+
}
|
|
58253
|
+
function computeEdges(fireMatrix, rules, total, params) {
|
|
58254
|
+
const ruleToFiles = /* @__PURE__ */ new Map();
|
|
58255
|
+
for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
|
|
58256
|
+
for (const [file, fired] of fireMatrix) {
|
|
58257
|
+
for (const ruleId of fired) {
|
|
58258
|
+
if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
|
|
58259
|
+
}
|
|
58260
|
+
}
|
|
58261
|
+
const ruleList = Array.from(rules).sort();
|
|
58262
|
+
const edges = [];
|
|
58263
|
+
for (let i = 0; i < ruleList.length; i++) {
|
|
58264
|
+
for (let j = i + 1; j < ruleList.length; j++) {
|
|
58265
|
+
const a = ruleList[i];
|
|
58266
|
+
const b = ruleList[j];
|
|
58267
|
+
const rA = ruleToFiles.get(a);
|
|
58268
|
+
const rB = ruleToFiles.get(b);
|
|
58269
|
+
const c = buildContingency(rA, rB, total);
|
|
58270
|
+
const n = npmi(c, total);
|
|
58271
|
+
if (n < params.minNPMI) continue;
|
|
58272
|
+
const p = fisherExactTwoSided(c);
|
|
58273
|
+
if (p > params.fisherAlpha) continue;
|
|
58274
|
+
edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
|
|
58275
|
+
}
|
|
58276
|
+
}
|
|
58277
|
+
return edges;
|
|
58278
|
+
}
|
|
58279
|
+
function singleLinkageCluster(edges, minClusterSize) {
|
|
58280
|
+
const parent = /* @__PURE__ */ new Map();
|
|
58281
|
+
const find = (x) => {
|
|
58282
|
+
if (!parent.has(x)) {
|
|
58283
|
+
parent.set(x, x);
|
|
58284
|
+
return x;
|
|
58285
|
+
}
|
|
58286
|
+
if (parent.get(x) === x) return x;
|
|
58287
|
+
const root = find(parent.get(x));
|
|
58288
|
+
parent.set(x, root);
|
|
58289
|
+
return root;
|
|
58290
|
+
};
|
|
58291
|
+
const union = (a, b) => {
|
|
58292
|
+
const ra = find(a);
|
|
58293
|
+
const rb = find(b);
|
|
58294
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
58295
|
+
};
|
|
58296
|
+
for (const e of edges) {
|
|
58297
|
+
union(e.ruleIds[0], e.ruleIds[1]);
|
|
58298
|
+
}
|
|
58299
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58300
|
+
for (const k of parent.keys()) {
|
|
58301
|
+
const root = find(k);
|
|
58302
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
58303
|
+
groups.get(root).push(k);
|
|
58304
|
+
}
|
|
58305
|
+
const clusters = [];
|
|
58306
|
+
for (const members of groups.values()) {
|
|
58307
|
+
if (members.length < minClusterSize) continue;
|
|
58308
|
+
members.sort();
|
|
58309
|
+
clusters.push(members);
|
|
58310
|
+
}
|
|
58311
|
+
clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
|
58312
|
+
return clusters;
|
|
58313
|
+
}
|
|
58314
|
+
function filesForComposite(fireMatrix, members, minMatch) {
|
|
58315
|
+
const fired = /* @__PURE__ */ new Set();
|
|
58316
|
+
for (const [file, ids] of fireMatrix) {
|
|
58317
|
+
let n = 0;
|
|
58318
|
+
for (const m of members) if (ids.has(m)) n++;
|
|
58319
|
+
if (n >= minMatch) fired.add(file);
|
|
58320
|
+
}
|
|
58321
|
+
return fired;
|
|
58322
|
+
}
|
|
58323
|
+
var DEFAULT_CALIBRATION = {
|
|
58324
|
+
// 1.5× is the documented floor.
|
|
58325
|
+
recallFpRatio: 1.5,
|
|
58326
|
+
// 0.05 absolute FP — the same `min` value most rules get via the
|
|
58327
|
+
// v0.18.x calibration contract.
|
|
58328
|
+
minFpFloor: 0.05
|
|
58329
|
+
};
|
|
58330
|
+
function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
|
|
58331
|
+
if (cluster2.length < 2) return null;
|
|
58332
|
+
const clusterSet = new Set(cluster2);
|
|
58333
|
+
const clusterEdges = edges.filter(
|
|
58334
|
+
(e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
|
|
58335
|
+
);
|
|
58336
|
+
if (clusterEdges.length === 0) return null;
|
|
58337
|
+
const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
|
|
58338
|
+
let best = null;
|
|
58339
|
+
for (let k = 1; k <= cluster2.length; k++) {
|
|
58340
|
+
const firedFiles = filesForComposite(fireMatrix, cluster2, k);
|
|
58341
|
+
const nFiles = firedFiles.size;
|
|
58342
|
+
if (nFiles === 0) continue;
|
|
58343
|
+
let recall = 0;
|
|
58344
|
+
let fp = 0;
|
|
58345
|
+
let precision = 0;
|
|
58346
|
+
let canCalibrate = false;
|
|
58347
|
+
if (positiveFiles && positiveFiles.size > 0) {
|
|
58348
|
+
let tp = 0;
|
|
58349
|
+
let fpc = 0;
|
|
58350
|
+
for (const file of firedFiles) {
|
|
58351
|
+
if (positiveFiles.has(file)) tp++;
|
|
58352
|
+
else fpc++;
|
|
58353
|
+
}
|
|
58354
|
+
const totalPos = positiveFiles.size;
|
|
58355
|
+
const totalNeg = Math.max(1, totalFiles - totalPos);
|
|
58356
|
+
recall = totalPos > 0 ? tp / totalPos : 0;
|
|
58357
|
+
fp = fpc / totalNeg;
|
|
58358
|
+
precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
|
|
58359
|
+
canCalibrate = true;
|
|
58360
|
+
}
|
|
58361
|
+
const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
|
|
58362
|
+
if (canCalibrate) {
|
|
58363
|
+
if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
|
|
58364
|
+
if (fp < thresholds.minFpFloor && recall < 0.5) continue;
|
|
58365
|
+
}
|
|
58366
|
+
if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
|
|
58367
|
+
best = {
|
|
58368
|
+
minMatch: k,
|
|
58369
|
+
npmi: strongest.weight,
|
|
58370
|
+
fisherP: strongest.fisherP,
|
|
58371
|
+
recall,
|
|
58372
|
+
FP: fp,
|
|
58373
|
+
precision,
|
|
58374
|
+
F1,
|
|
58375
|
+
nFiles
|
|
58376
|
+
};
|
|
58377
|
+
}
|
|
58378
|
+
}
|
|
58379
|
+
return best;
|
|
58380
|
+
}
|
|
58381
|
+
function compositeId(members) {
|
|
58382
|
+
const sorted = [...members].sort();
|
|
58383
|
+
return "composite/" + createHash12("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
|
|
58384
|
+
}
|
|
58385
|
+
function worstSeverity(members, memberSeverities) {
|
|
58386
|
+
let worst = "low";
|
|
58387
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
58388
|
+
for (const m of members) {
|
|
58389
|
+
const s = memberSeverities?.get(m);
|
|
58390
|
+
if (s && order[s] > order[worst]) worst = s;
|
|
58391
|
+
}
|
|
58392
|
+
return worst;
|
|
58393
|
+
}
|
|
58394
|
+
function describeComposite(members, minMatch) {
|
|
58395
|
+
const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
|
|
58396
|
+
return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
|
|
58397
|
+
}
|
|
58398
|
+
function runClusterer(input) {
|
|
58399
|
+
const total = input.fireMatrix.size;
|
|
58400
|
+
const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
|
|
58401
|
+
const params = {
|
|
58402
|
+
minSupport,
|
|
58403
|
+
minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
|
|
58404
|
+
fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
|
|
58405
|
+
minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
|
|
58406
|
+
};
|
|
58407
|
+
const support = computeRuleSupport(input.fireMatrix);
|
|
58408
|
+
const rulesKept = prefilterBySupport(support, total, params.minSupport);
|
|
58409
|
+
const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
|
|
58410
|
+
const clusters = singleLinkageCluster(edges, params.minClusterSize);
|
|
58411
|
+
const entries = [];
|
|
58412
|
+
const clusterMaxNPMI = /* @__PURE__ */ new Map();
|
|
58413
|
+
for (const cluster2 of clusters) {
|
|
58414
|
+
const cal = calibrateCluster(
|
|
58415
|
+
cluster2,
|
|
58416
|
+
edges,
|
|
58417
|
+
input.fireMatrix,
|
|
58418
|
+
input.positiveFiles,
|
|
58419
|
+
total
|
|
58420
|
+
);
|
|
58421
|
+
if (!cal) continue;
|
|
58422
|
+
const id = compositeId(cluster2);
|
|
58423
|
+
const severity = worstSeverity(cluster2, input.memberSeverities);
|
|
58424
|
+
entries.push({
|
|
58425
|
+
id,
|
|
58426
|
+
ruleIds: [...cluster2].sort(),
|
|
58427
|
+
minMatch: cal.minMatch,
|
|
58428
|
+
severity,
|
|
58429
|
+
defaultOff: true,
|
|
58430
|
+
description: describeComposite(cluster2, cal.minMatch),
|
|
58431
|
+
calibration: {
|
|
58432
|
+
recall: round3(cal.recall),
|
|
58433
|
+
FP: round3(cal.FP),
|
|
58434
|
+
precision: round3(cal.precision),
|
|
58435
|
+
F1: round3(cal.F1),
|
|
58436
|
+
nFiles: cal.nFiles
|
|
58437
|
+
},
|
|
58438
|
+
provenance: {
|
|
58439
|
+
seed: "auto-cluster",
|
|
58440
|
+
discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
58441
|
+
nFiles: total,
|
|
58442
|
+
members: cluster2.length,
|
|
58443
|
+
npmi: round3(cal.npmi),
|
|
58444
|
+
fisherP: round3(cal.fisherP)
|
|
58445
|
+
}
|
|
58446
|
+
});
|
|
58447
|
+
clusterMaxNPMI.set(id, cal.npmi);
|
|
58448
|
+
}
|
|
58449
|
+
entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
|
|
58450
|
+
return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
|
|
58451
|
+
}
|
|
58452
|
+
function round3(n) {
|
|
58453
|
+
return Math.round(n * 1e3) / 1e3;
|
|
58454
|
+
}
|
|
58455
|
+
|
|
58456
|
+
// src/rules/composite-loader.ts
|
|
58457
|
+
import { existsSync as existsSync23, readFileSync as readFileSync27, mkdirSync as mkdirSync10, writeFileSync as writeFileSync13 } from "fs";
|
|
58458
|
+
import { dirname as dirname15, join as join24 } from "path";
|
|
58459
|
+
var COMPOSITES_FILE = "composites.json";
|
|
58460
|
+
function compositesPath(cwd) {
|
|
58461
|
+
return join24(cwd, COMPOSITES_FILE);
|
|
58462
|
+
}
|
|
58463
|
+
function readComposites(cwd) {
|
|
58464
|
+
const path = compositesPath(cwd);
|
|
58465
|
+
if (!existsSync23(path)) return [];
|
|
58466
|
+
try {
|
|
58467
|
+
const raw = JSON.parse(readFileSync27(path, "utf-8"));
|
|
58468
|
+
if (!Array.isArray(raw)) return [];
|
|
58469
|
+
return raw.filter(isCompositeRuleEntry);
|
|
58470
|
+
} catch {
|
|
58471
|
+
return [];
|
|
58472
|
+
}
|
|
58473
|
+
}
|
|
58474
|
+
function isCompositeRuleEntry(value) {
|
|
58475
|
+
if (!value || typeof value !== "object") return false;
|
|
58476
|
+
const v = value;
|
|
58477
|
+
return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
|
|
58478
|
+
}
|
|
58479
|
+
function writeComposites(cwd, entries) {
|
|
58480
|
+
const path = compositesPath(cwd);
|
|
58481
|
+
mkdirSync10(dirname15(path), { recursive: true });
|
|
58482
|
+
const tmp = `${path}.tmp`;
|
|
58483
|
+
writeFileSync13(tmp, JSON.stringify(entries, null, 2), "utf-8");
|
|
58484
|
+
const fs = __require("fs");
|
|
58485
|
+
fs.renameSync(tmp, path);
|
|
58486
|
+
}
|
|
58487
|
+
|
|
58488
|
+
// src/cli/commands/composite.ts
|
|
58489
|
+
function registerComposite(program) {
|
|
58490
|
+
const cmd = program.command("composite").description(
|
|
58491
|
+
"discover and enable composite rules (Sprint 3, \xA73b). Run `slopbrick composite discover` to scan scans.jsonl, audit the candidates, then `slopbrick composite enable <id>` to opt a specific composite into slopbrick.config.mjs."
|
|
58492
|
+
);
|
|
58493
|
+
cmd.command("discover").description(
|
|
58494
|
+
"run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
|
|
58495
|
+
).action(async (_cmdOptions, command) => {
|
|
58496
|
+
const options = command.optsWithGlobals();
|
|
58497
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58498
|
+
const payloads = readTelemetry(cwd);
|
|
58499
|
+
if (payloads.length === 0) {
|
|
58500
|
+
logger.error(
|
|
58501
|
+
"`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
|
|
58502
|
+
);
|
|
58503
|
+
process.exit(1);
|
|
58504
|
+
}
|
|
58505
|
+
const fireMatrix = /* @__PURE__ */ new Map();
|
|
58506
|
+
for (const payload of payloads) {
|
|
58507
|
+
for (const f of payload.files) {
|
|
58508
|
+
const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
|
|
58509
|
+
for (const id of f.ruleIds) set.add(id);
|
|
58510
|
+
if (set.size > 0) fireMatrix.set(f.hash, set);
|
|
58511
|
+
}
|
|
58512
|
+
}
|
|
58513
|
+
const out = runClusterer({
|
|
58514
|
+
fireMatrix,
|
|
58515
|
+
// No labeled positive set; let the clusterer surface for audit.
|
|
58516
|
+
positiveFiles: void 0,
|
|
58517
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
58518
|
+
});
|
|
58519
|
+
const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
|
|
58520
|
+
writeComposites(cwd, finalEntries);
|
|
58521
|
+
if (out.entries.length === 0) {
|
|
58522
|
+
logger.info("Composite discovery: no clusters passed the empirical gate.");
|
|
58523
|
+
logger.info(
|
|
58524
|
+
" (Try lowering --min-support, or run more scans to densify the ledger.)"
|
|
58525
|
+
);
|
|
58526
|
+
return;
|
|
58527
|
+
}
|
|
58528
|
+
logger.info(
|
|
58529
|
+
`Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
|
|
58530
|
+
);
|
|
58531
|
+
const lines = [];
|
|
58532
|
+
for (const e of out.entries) {
|
|
58533
|
+
lines.push("");
|
|
58534
|
+
lines.push(
|
|
58535
|
+
` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
|
|
58536
|
+
);
|
|
58537
|
+
lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
|
|
58538
|
+
lines.push(
|
|
58539
|
+
` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
|
|
58540
|
+
);
|
|
58541
|
+
}
|
|
58542
|
+
logger.info(lines.join("\n"));
|
|
58543
|
+
logger.info(
|
|
58544
|
+
"\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
|
|
58545
|
+
);
|
|
58546
|
+
});
|
|
58547
|
+
cmd.command("enable <id>").description(
|
|
58548
|
+
"opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
|
|
58549
|
+
).action(async (id, _cmdOptions, command) => {
|
|
58550
|
+
const options = command.optsWithGlobals();
|
|
58551
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58552
|
+
const composites = readComposites(cwd);
|
|
58553
|
+
const target = composites.find((c) => c.id === id);
|
|
58554
|
+
if (!target) {
|
|
58555
|
+
logger.error(
|
|
58556
|
+
`Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
|
|
58557
|
+
);
|
|
58558
|
+
process.exit(1);
|
|
58559
|
+
}
|
|
58560
|
+
const cfgPath = resolve15(cwd, "slopbrick.config.mjs");
|
|
58561
|
+
const fs = __require("fs");
|
|
58562
|
+
if (!fs.existsSync(cfgPath)) {
|
|
58563
|
+
logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
|
|
58564
|
+
process.exit(1);
|
|
58565
|
+
}
|
|
58566
|
+
const src = fs.readFileSync(cfgPath, "utf-8");
|
|
58567
|
+
const compositeSnippet = JSON.stringify(
|
|
58568
|
+
{
|
|
58569
|
+
id: target.id,
|
|
58570
|
+
category: "ai",
|
|
58571
|
+
severity: target.severity,
|
|
58572
|
+
aiSpecific: true,
|
|
58573
|
+
description: target.description,
|
|
58574
|
+
defaultOff: true,
|
|
58575
|
+
ruleIds: target.ruleIds,
|
|
58576
|
+
minMatch: target.minMatch,
|
|
58577
|
+
create: "() => ({})",
|
|
58578
|
+
analyze: "() => []"
|
|
58579
|
+
},
|
|
58580
|
+
null,
|
|
58581
|
+
2
|
|
58582
|
+
);
|
|
58583
|
+
if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
|
|
58584
|
+
logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
|
|
58585
|
+
return;
|
|
58586
|
+
}
|
|
58587
|
+
const updated = src.replace(
|
|
58588
|
+
/(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
|
|
58589
|
+
(_match, head, tail) => `${head} compositeRules: [
|
|
58590
|
+
${compositeSnippet}
|
|
58591
|
+
],
|
|
58592
|
+
${tail}`
|
|
58593
|
+
);
|
|
58594
|
+
fs.writeFileSync(cfgPath, updated, "utf-8");
|
|
58595
|
+
logger.info(`Composite "${id}" enabled.`);
|
|
58596
|
+
logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
|
|
58597
|
+
});
|
|
58598
|
+
cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
|
|
58599
|
+
const options = command.optsWithGlobals();
|
|
58600
|
+
const cwd = resolve15(options.workspace ?? process.cwd());
|
|
58601
|
+
const composites = readComposites(cwd);
|
|
58602
|
+
if (composites.length === 0) {
|
|
58603
|
+
logger.info("No composites registered. Run `slopbrick composite discover` first.");
|
|
58604
|
+
return;
|
|
58605
|
+
}
|
|
58606
|
+
const lines = ["Registered composites:"];
|
|
58607
|
+
for (const e of composites) {
|
|
58608
|
+
lines.push(
|
|
58609
|
+
`
|
|
58610
|
+
${e.id}
|
|
58611
|
+
${describeComposite(e.ruleIds, e.minMatch)}
|
|
58612
|
+
members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
|
|
58613
|
+
F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
|
|
58614
|
+
);
|
|
58615
|
+
}
|
|
58616
|
+
logger.info(lines.join("\n"));
|
|
58617
|
+
});
|
|
58618
|
+
}
|
|
58619
|
+
|
|
58035
58620
|
// src/cli/explain.ts
|
|
58036
58621
|
var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
58037
58622
|
function ruleIdToFilename2(ruleId) {
|
|
@@ -58081,152 +58666,7 @@ function formatExplain(result) {
|
|
|
58081
58666
|
// src/cli/commands/explain.ts
|
|
58082
58667
|
init_logger();
|
|
58083
58668
|
init_builtins();
|
|
58084
|
-
|
|
58085
|
-
// src/snippet/data.ts
|
|
58086
|
-
var CATEGORY_DIRECTIVES = {
|
|
58087
|
-
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.',
|
|
58088
|
-
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.",
|
|
58089
|
-
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.',
|
|
58090
|
-
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".',
|
|
58091
|
-
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.",
|
|
58092
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
58093
|
-
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].",
|
|
58094
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
58095
|
-
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.",
|
|
58096
|
-
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'."
|
|
58097
|
-
};
|
|
58098
|
-
var RULE_HINTS = {
|
|
58099
|
-
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
58100
|
-
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
58101
|
-
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
58102
|
-
// docs/research/backlog-rule-hints.md
|
|
58103
|
-
// for future implementers to paste back when the corresponding rules ship.)
|
|
58104
|
-
"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.",
|
|
58105
|
-
"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.",
|
|
58106
|
-
"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.",
|
|
58107
|
-
"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.",
|
|
58108
|
-
"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.",
|
|
58109
|
-
"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.",
|
|
58110
|
-
"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.',
|
|
58111
|
-
"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.",
|
|
58112
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
58113
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
58114
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
58115
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
58116
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
58117
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
58118
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
58119
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
58120
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
58121
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
58122
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
58123
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
58124
|
-
"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.",
|
|
58125
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
58126
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
58127
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
58128
|
-
"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.",
|
|
58129
|
-
"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.",
|
|
58130
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
58131
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
58132
|
-
"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.)",
|
|
58133
|
-
"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.)",
|
|
58134
|
-
"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.)",
|
|
58135
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
58136
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
58137
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
58138
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
58139
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
58140
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
58141
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
58142
|
-
"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.",
|
|
58143
|
-
"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.",
|
|
58144
|
-
"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.",
|
|
58145
|
-
"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.",
|
|
58146
|
-
"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.",
|
|
58147
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
58148
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
58149
|
-
"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.',
|
|
58150
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
58151
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
58152
|
-
"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.",
|
|
58153
|
-
"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.",
|
|
58154
|
-
"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.",
|
|
58155
|
-
"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%.",
|
|
58156
|
-
"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.",
|
|
58157
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
58158
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
58159
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
58160
|
-
"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.",
|
|
58161
|
-
"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`).",
|
|
58162
|
-
"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.",
|
|
58163
|
-
"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.",
|
|
58164
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
58165
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
58166
|
-
"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).",
|
|
58167
|
-
"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).",
|
|
58168
|
-
"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).",
|
|
58169
|
-
"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).",
|
|
58170
|
-
"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).",
|
|
58171
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
58172
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
58173
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
58174
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
58175
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
58176
|
-
"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.",
|
|
58177
|
-
"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.",
|
|
58178
|
-
"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.",
|
|
58179
|
-
"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.",
|
|
58180
|
-
"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.",
|
|
58181
|
-
"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.",
|
|
58182
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
58183
|
-
"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.',
|
|
58184
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
58185
|
-
"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.",
|
|
58186
|
-
"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.",
|
|
58187
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
58188
|
-
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
58189
|
-
"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.",
|
|
58190
|
-
"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.",
|
|
58191
|
-
"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.",
|
|
58192
|
-
"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.',
|
|
58193
|
-
// v0.19.0 — TypeScript-specific rules
|
|
58194
|
-
"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.",
|
|
58195
|
-
"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.",
|
|
58196
|
-
"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.",
|
|
58197
|
-
"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.",
|
|
58198
|
-
// v0.19.0 — Go-specific rules
|
|
58199
|
-
"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).',
|
|
58200
|
-
"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.",
|
|
58201
|
-
// v0.19.0 — Duplication detector
|
|
58202
|
-
"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.",
|
|
58203
|
-
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
58204
|
-
"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.)",
|
|
58205
|
-
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
58206
|
-
"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.)",
|
|
58207
|
-
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
58208
|
-
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
58209
|
-
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
58210
|
-
"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.)",
|
|
58211
|
-
"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.)",
|
|
58212
|
-
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
58213
|
-
"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.",
|
|
58214
|
-
// v0.35.1 — Raidar-inspired content-based detection
|
|
58215
|
-
"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)`.',
|
|
58216
|
-
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
58217
|
-
"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.)",
|
|
58218
|
-
"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.)",
|
|
58219
|
-
"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.)",
|
|
58220
|
-
"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.)",
|
|
58221
|
-
"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.)",
|
|
58222
|
-
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
58223
|
-
"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.)",
|
|
58224
|
-
"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.)",
|
|
58225
|
-
"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.)",
|
|
58226
|
-
"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.)"
|
|
58227
|
-
};
|
|
58228
|
-
|
|
58229
|
-
// src/cli/commands/explain.ts
|
|
58669
|
+
init_data();
|
|
58230
58670
|
function registerExplain(program) {
|
|
58231
58671
|
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
58232
58672
|
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
@@ -58236,7 +58676,7 @@ function registerExplain(program) {
|
|
|
58236
58676
|
}
|
|
58237
58677
|
|
|
58238
58678
|
// src/cli/commands/calibration.ts
|
|
58239
|
-
import { dirname as
|
|
58679
|
+
import { dirname as dirname16 } from "path";
|
|
58240
58680
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
58241
58681
|
|
|
58242
58682
|
// src/rules/signal-strength.json
|
|
@@ -61829,7 +62269,7 @@ var signal_strength_default2 = {
|
|
|
61829
62269
|
};
|
|
61830
62270
|
|
|
61831
62271
|
// src/cli/commands/calibration.ts
|
|
61832
|
-
var __dirname =
|
|
62272
|
+
var __dirname = dirname16(fileURLToPath2(import.meta.url));
|
|
61833
62273
|
function loadData() {
|
|
61834
62274
|
return signal_strength_default2;
|
|
61835
62275
|
}
|
|
@@ -61952,17 +62392,17 @@ function registerCalibration(program) {
|
|
|
61952
62392
|
// src/cli/commands/install.ts
|
|
61953
62393
|
init_logger();
|
|
61954
62394
|
init_git();
|
|
61955
|
-
import { resolve as
|
|
62395
|
+
import { resolve as resolve16 } from "path";
|
|
61956
62396
|
|
|
61957
62397
|
// src/cli/installer.ts
|
|
61958
62398
|
import {
|
|
61959
62399
|
chmodSync,
|
|
61960
|
-
existsSync as
|
|
61961
|
-
mkdirSync as
|
|
61962
|
-
readFileSync as
|
|
61963
|
-
writeFileSync as
|
|
62400
|
+
existsSync as existsSync24,
|
|
62401
|
+
mkdirSync as mkdirSync11,
|
|
62402
|
+
readFileSync as readFileSync28,
|
|
62403
|
+
writeFileSync as writeFileSync14
|
|
61964
62404
|
} from "fs";
|
|
61965
|
-
import { dirname as
|
|
62405
|
+
import { dirname as dirname17, join as join26 } from "path";
|
|
61966
62406
|
var BEGIN_SENTINEL = "# slopbrick-hook-begin";
|
|
61967
62407
|
var END_SENTINEL = "# slopbrick-hook-end";
|
|
61968
62408
|
var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
|
|
@@ -61970,14 +62410,14 @@ npx slopbrick --staged
|
|
|
61970
62410
|
${END_SENTINEL}
|
|
61971
62411
|
`;
|
|
61972
62412
|
function hookPath(gitRoot) {
|
|
61973
|
-
const huskyDir =
|
|
61974
|
-
if (
|
|
61975
|
-
return
|
|
62413
|
+
const huskyDir = join26(gitRoot, ".husky");
|
|
62414
|
+
if (existsSync24(huskyDir)) {
|
|
62415
|
+
return join26(huskyDir, "pre-commit");
|
|
61976
62416
|
}
|
|
61977
|
-
return
|
|
62417
|
+
return join26(gitRoot, ".git", "hooks", "pre-commit");
|
|
61978
62418
|
}
|
|
61979
62419
|
function readHookContent(path) {
|
|
61980
|
-
return
|
|
62420
|
+
return readFileSync28(path, "utf8");
|
|
61981
62421
|
}
|
|
61982
62422
|
function sentinelsPresent(content) {
|
|
61983
62423
|
const lines = content.split(/\r?\n/);
|
|
@@ -61999,7 +62439,7 @@ function replaceSentinelBlock(content) {
|
|
|
61999
62439
|
}
|
|
62000
62440
|
function installHook(gitRoot) {
|
|
62001
62441
|
const path = hookPath(gitRoot);
|
|
62002
|
-
if (
|
|
62442
|
+
if (existsSync24(path)) {
|
|
62003
62443
|
const content = readHookContent(path);
|
|
62004
62444
|
const { begin, end } = sentinelsPresent(content);
|
|
62005
62445
|
if (begin && end) {
|
|
@@ -62011,7 +62451,7 @@ function installHook(gitRoot) {
|
|
|
62011
62451
|
exitCode: 0
|
|
62012
62452
|
};
|
|
62013
62453
|
}
|
|
62014
|
-
|
|
62454
|
+
writeFileSync14(path, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
62015
62455
|
`);
|
|
62016
62456
|
chmodSync(path, 493);
|
|
62017
62457
|
return {
|
|
@@ -62031,7 +62471,7 @@ function installHook(gitRoot) {
|
|
|
62031
62471
|
}
|
|
62032
62472
|
const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
|
|
62033
62473
|
` : content;
|
|
62034
|
-
|
|
62474
|
+
writeFileSync14(path, `${normalized}${SENTINEL_BLOCK}`);
|
|
62035
62475
|
chmodSync(path, 493);
|
|
62036
62476
|
return {
|
|
62037
62477
|
ok: true,
|
|
@@ -62039,8 +62479,8 @@ function installHook(gitRoot) {
|
|
|
62039
62479
|
exitCode: 0
|
|
62040
62480
|
};
|
|
62041
62481
|
}
|
|
62042
|
-
|
|
62043
|
-
|
|
62482
|
+
mkdirSync11(dirname17(path), { recursive: true });
|
|
62483
|
+
writeFileSync14(path, SENTINEL_BLOCK, { mode: 493 });
|
|
62044
62484
|
chmodSync(path, 493);
|
|
62045
62485
|
return {
|
|
62046
62486
|
ok: true,
|
|
@@ -62050,7 +62490,7 @@ function installHook(gitRoot) {
|
|
|
62050
62490
|
}
|
|
62051
62491
|
function uninstallHook(gitRoot) {
|
|
62052
62492
|
const path = hookPath(gitRoot);
|
|
62053
|
-
if (!
|
|
62493
|
+
if (!existsSync24(path)) {
|
|
62054
62494
|
return {
|
|
62055
62495
|
ok: true,
|
|
62056
62496
|
message: "Hook not installed",
|
|
@@ -62086,7 +62526,7 @@ function uninstallHook(gitRoot) {
|
|
|
62086
62526
|
}
|
|
62087
62527
|
const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
|
|
62088
62528
|
const result = remaining.join("\n");
|
|
62089
|
-
|
|
62529
|
+
writeFileSync14(path, result.endsWith("\n") ? result : `${result}
|
|
62090
62530
|
`);
|
|
62091
62531
|
return {
|
|
62092
62532
|
ok: true,
|
|
@@ -62099,7 +62539,7 @@ function uninstallHook(gitRoot) {
|
|
|
62099
62539
|
function registerInstall(program) {
|
|
62100
62540
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
62101
62541
|
const options = command.optsWithGlobals();
|
|
62102
|
-
const cwd =
|
|
62542
|
+
const cwd = resolve16(options.workspace ?? process.cwd());
|
|
62103
62543
|
const root = getGitRoot(cwd);
|
|
62104
62544
|
if (!root) {
|
|
62105
62545
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -62116,11 +62556,11 @@ function registerInstall(program) {
|
|
|
62116
62556
|
// src/cli/commands/uninstall.ts
|
|
62117
62557
|
init_logger();
|
|
62118
62558
|
init_git();
|
|
62119
|
-
import { resolve as
|
|
62559
|
+
import { resolve as resolve17 } from "path";
|
|
62120
62560
|
function registerUninstall(program) {
|
|
62121
62561
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
62122
62562
|
const options = command.optsWithGlobals();
|
|
62123
|
-
const cwd =
|
|
62563
|
+
const cwd = resolve17(options.workspace ?? process.cwd());
|
|
62124
62564
|
const root = getGitRoot(cwd);
|
|
62125
62565
|
if (!root) {
|
|
62126
62566
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -62141,7 +62581,374 @@ init_logger();
|
|
|
62141
62581
|
init_builtins();
|
|
62142
62582
|
init_config2();
|
|
62143
62583
|
init_header();
|
|
62144
|
-
|
|
62584
|
+
|
|
62585
|
+
// src/mcp/tools.ts
|
|
62586
|
+
init_worker();
|
|
62587
|
+
init_patterns();
|
|
62588
|
+
init_architecture_score();
|
|
62589
|
+
init_business_logic();
|
|
62590
|
+
init_structure_md();
|
|
62591
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
62592
|
+
import { resolve as resolve18 } from "path";
|
|
62593
|
+
var TOOL_DEFINITIONS = [
|
|
62594
|
+
{
|
|
62595
|
+
name: "slop_scan_file",
|
|
62596
|
+
description: "Scan a single TypeScript/JavaScript file for AI-generated frontend slop. Returns issues (ruleId, severity, line, column, message, advice) and the file-level Slop Index.",
|
|
62597
|
+
inputSchema: {
|
|
62598
|
+
type: "object",
|
|
62599
|
+
properties: {
|
|
62600
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
62601
|
+
framework: {
|
|
62602
|
+
type: "string",
|
|
62603
|
+
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
62604
|
+
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
62605
|
+
}
|
|
62606
|
+
},
|
|
62607
|
+
required: ["path"]
|
|
62608
|
+
}
|
|
62609
|
+
},
|
|
62610
|
+
{
|
|
62611
|
+
name: "slop_explain_rule",
|
|
62612
|
+
description: "Return metadata for a single rule (id, category, severity, aiSpecific) plus a rationale and the recommended fix. Use this before auto-applying --fix to understand what the rule catches.",
|
|
62613
|
+
inputSchema: {
|
|
62614
|
+
type: "object",
|
|
62615
|
+
properties: {
|
|
62616
|
+
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
62617
|
+
},
|
|
62618
|
+
required: ["ruleId"]
|
|
62619
|
+
}
|
|
62620
|
+
},
|
|
62621
|
+
{
|
|
62622
|
+
name: "slop_list_rules",
|
|
62623
|
+
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
62624
|
+
inputSchema: {
|
|
62625
|
+
type: "object",
|
|
62626
|
+
properties: {
|
|
62627
|
+
category: { type: "string", description: "Optional category filter." }
|
|
62628
|
+
}
|
|
62629
|
+
}
|
|
62630
|
+
},
|
|
62631
|
+
{
|
|
62632
|
+
name: "slop_suggest",
|
|
62633
|
+
description: "**Primary entry point for AI agents.** Returns the project's existing patterns (modals, buttons, api clients, state libs, data-fetching libs), the do-not-create list (forbidden imports + canonical patterns not to duplicate), top issues by rule, hot files by issue count, and the composite Repository Health score. Call this BEFORE writing new code so the agent reuses existing patterns instead of duplicating them.",
|
|
62634
|
+
inputSchema: {
|
|
62635
|
+
type: "object",
|
|
62636
|
+
properties: {
|
|
62637
|
+
maxFiles: {
|
|
62638
|
+
type: "number",
|
|
62639
|
+
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
62640
|
+
}
|
|
62641
|
+
}
|
|
62642
|
+
}
|
|
62643
|
+
},
|
|
62644
|
+
{
|
|
62645
|
+
name: "slop_suggest_with_structure",
|
|
62646
|
+
description: "Fast-path variant of `slop_suggest` that reads `.slopbrick/structure.md` from disk instead of re-scanning the codebase. Requires a prior `slopbrick scan` to have persisted the inventory (100\u20131000\xD7 latency win on the agent integration). If `structure.md` is missing, falls back to `slop_suggest` and annotates the response with `structureHint` so the caller knows to run `slopbrick scan` first.",
|
|
62647
|
+
inputSchema: {
|
|
62648
|
+
type: "object",
|
|
62649
|
+
properties: {
|
|
62650
|
+
maxFiles: {
|
|
62651
|
+
type: "number",
|
|
62652
|
+
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
62653
|
+
}
|
|
62654
|
+
}
|
|
62655
|
+
}
|
|
62656
|
+
},
|
|
62657
|
+
{
|
|
62658
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
62659
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
62660
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
62661
|
+
// They were strict subsets of slop_suggest; users should
|
|
62662
|
+
// call slop_suggest and read repositoryHealth /
|
|
62663
|
+
// architectureConsistency / businessLogicCoherence.
|
|
62664
|
+
name: "slop_check_constitution",
|
|
62665
|
+
description: "Check a single file against the project's declared constitution (stateManagement, dataFetching, uiLibrary, forms, styling, routing, plus a forbidden deny-list in slopbrick.config.mjs). Returns a list of imports that violate declared values or hit the deny-list. Use this on a newly-written or modified file before suggesting a PR.",
|
|
62666
|
+
inputSchema: {
|
|
62667
|
+
type: "object",
|
|
62668
|
+
properties: {
|
|
62669
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
62670
|
+
},
|
|
62671
|
+
required: ["path"]
|
|
62672
|
+
}
|
|
62673
|
+
},
|
|
62674
|
+
{
|
|
62675
|
+
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
62676
|
+
// Reference) primitive for slop_suggest. Given a function signature
|
|
62677
|
+
// (name + hooks + props), find the most similar existing
|
|
62678
|
+
// implementations across the codebase. Uses AST fingerprints
|
|
62679
|
+
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
62680
|
+
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
62681
|
+
name: "slop_find_similar",
|
|
62682
|
+
description: "Find the most similar existing function/component implementations across the codebase, ranked by Jaccard similarity over the union of (hooks \u222A props \u222A params). Use this BEFORE writing new code so the agent reuses an existing pattern instead of inventing a new one. Returns top-k matches with name, file, line, fingerprint, and similarity score in [0, 1].",
|
|
62683
|
+
inputSchema: {
|
|
62684
|
+
type: "object",
|
|
62685
|
+
properties: {
|
|
62686
|
+
name: {
|
|
62687
|
+
type: "string",
|
|
62688
|
+
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
62689
|
+
},
|
|
62690
|
+
hooks: {
|
|
62691
|
+
type: "array",
|
|
62692
|
+
items: { type: "string" },
|
|
62693
|
+
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
62694
|
+
},
|
|
62695
|
+
props: {
|
|
62696
|
+
type: "array",
|
|
62697
|
+
items: { type: "string" },
|
|
62698
|
+
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
62699
|
+
},
|
|
62700
|
+
limit: {
|
|
62701
|
+
type: "number",
|
|
62702
|
+
description: "Top-k results to return. Default 10. Capped at 50."
|
|
62703
|
+
}
|
|
62704
|
+
}
|
|
62705
|
+
}
|
|
62706
|
+
}
|
|
62707
|
+
];
|
|
62708
|
+
function toolError(message) {
|
|
62709
|
+
return {
|
|
62710
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
62711
|
+
isError: true
|
|
62712
|
+
};
|
|
62713
|
+
}
|
|
62714
|
+
async function runScanFile(args, ctx) {
|
|
62715
|
+
const path = args.path;
|
|
62716
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62717
|
+
const result = await scanFile(path, ctx.config);
|
|
62718
|
+
const simplified = {
|
|
62719
|
+
filePath: result.filePath,
|
|
62720
|
+
componentCount: result.componentCount,
|
|
62721
|
+
parseError: result.parseError,
|
|
62722
|
+
issues: result.issues.map((i) => ({
|
|
62723
|
+
ruleId: i.ruleId,
|
|
62724
|
+
category: i.category,
|
|
62725
|
+
severity: i.severity,
|
|
62726
|
+
line: i.line,
|
|
62727
|
+
column: i.column,
|
|
62728
|
+
message: i.message,
|
|
62729
|
+
advice: i.advice
|
|
62730
|
+
}))
|
|
62731
|
+
};
|
|
62732
|
+
return {
|
|
62733
|
+
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
62734
|
+
};
|
|
62735
|
+
}
|
|
62736
|
+
function explainRule2(args, ctx) {
|
|
62737
|
+
const ruleId = args.ruleId;
|
|
62738
|
+
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
62739
|
+
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
62740
|
+
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
62741
|
+
const explanation = {
|
|
62742
|
+
ruleId: rule.id,
|
|
62743
|
+
category: rule.category,
|
|
62744
|
+
severity: rule.severity,
|
|
62745
|
+
aiSpecific: rule.aiSpecific,
|
|
62746
|
+
rationale: "This rule flags " + rule.category + " patterns associated with AI-generated code. It is marked as " + (rule.aiSpecific ? "AI-specific" : "cross-cutting") + ". Severity: " + rule.severity + ".",
|
|
62747
|
+
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
62748
|
+
};
|
|
62749
|
+
return {
|
|
62750
|
+
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
62751
|
+
};
|
|
62752
|
+
}
|
|
62753
|
+
function listRules(args, ctx) {
|
|
62754
|
+
const category = args.category;
|
|
62755
|
+
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
62756
|
+
const rules = filtered.map((r) => ({
|
|
62757
|
+
id: r.id,
|
|
62758
|
+
category: r.category,
|
|
62759
|
+
severity: r.severity,
|
|
62760
|
+
aiSpecific: r.aiSpecific
|
|
62761
|
+
}));
|
|
62762
|
+
return {
|
|
62763
|
+
content: [
|
|
62764
|
+
{
|
|
62765
|
+
type: "text",
|
|
62766
|
+
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
62767
|
+
}
|
|
62768
|
+
]
|
|
62769
|
+
};
|
|
62770
|
+
}
|
|
62771
|
+
var STRUCTURE_NOT_FOUND_HINT = "No .slopbrick/structure.md found. Run `slopbrick scan` to persist the pattern inventory, then call this tool again for the O(read file) fast path.";
|
|
62772
|
+
async function runSuggest(args, ctx, options = {}) {
|
|
62773
|
+
const { includeStructure = false } = options;
|
|
62774
|
+
if (includeStructure) {
|
|
62775
|
+
const cached = await readStructureMarkdown(ctx.cwd);
|
|
62776
|
+
if (cached !== null) {
|
|
62777
|
+
return {
|
|
62778
|
+
content: [{ type: "text", text: cached }]
|
|
62779
|
+
};
|
|
62780
|
+
}
|
|
62781
|
+
}
|
|
62782
|
+
const maxFilesRaw = args.maxFiles;
|
|
62783
|
+
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
62784
|
+
try {
|
|
62785
|
+
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
62786
|
+
const doNotCreate = [
|
|
62787
|
+
...ctx.config.constitution?.forbidden ?? []
|
|
62788
|
+
];
|
|
62789
|
+
const declared = /* @__PURE__ */ new Set();
|
|
62790
|
+
for (const list of [
|
|
62791
|
+
ctx.config.constitution?.stateManagement ?? [],
|
|
62792
|
+
ctx.config.constitution?.dataFetching ?? [],
|
|
62793
|
+
ctx.config.constitution?.uiLibrary ?? [],
|
|
62794
|
+
ctx.config.constitution?.forms ?? [],
|
|
62795
|
+
ctx.config.constitution?.styling ?? [],
|
|
62796
|
+
ctx.config.constitution?.routing ?? []
|
|
62797
|
+
]) {
|
|
62798
|
+
for (const lib of list) declared.add(lib);
|
|
62799
|
+
}
|
|
62800
|
+
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
62801
|
+
const payload = {
|
|
62802
|
+
hint: "Use these patterns instead of creating new ones. Pick the closest existing entry and import it. The `doNotCreate` list is the deny-list \u2014 never import any of these.",
|
|
62803
|
+
doNotCreate: doNotCreateCapped,
|
|
62804
|
+
declaredStack: Array.from(declared),
|
|
62805
|
+
existingPatterns: inventory
|
|
62806
|
+
};
|
|
62807
|
+
if (includeStructure) {
|
|
62808
|
+
payload.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
62809
|
+
}
|
|
62810
|
+
try {
|
|
62811
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62812
|
+
const health = loadHealth2(ctx.cwd);
|
|
62813
|
+
if (health?.compositeScore) {
|
|
62814
|
+
payload.compositeScore = health.compositeScore;
|
|
62815
|
+
}
|
|
62816
|
+
} catch {
|
|
62817
|
+
}
|
|
62818
|
+
return {
|
|
62819
|
+
content: [
|
|
62820
|
+
{
|
|
62821
|
+
type: "text",
|
|
62822
|
+
text: JSON.stringify(payload, null, 2)
|
|
62823
|
+
}
|
|
62824
|
+
]
|
|
62825
|
+
};
|
|
62826
|
+
} catch (err) {
|
|
62827
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62828
|
+
}
|
|
62829
|
+
}
|
|
62830
|
+
function runCheckConstitution(args, ctx) {
|
|
62831
|
+
const path = args.path;
|
|
62832
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62833
|
+
const absPath = resolve18(ctx.cwd, path);
|
|
62834
|
+
let source;
|
|
62835
|
+
try {
|
|
62836
|
+
source = readFileSync29(absPath, "utf-8");
|
|
62837
|
+
} catch (err) {
|
|
62838
|
+
return toolError(
|
|
62839
|
+
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
62840
|
+
);
|
|
62841
|
+
}
|
|
62842
|
+
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
62843
|
+
return {
|
|
62844
|
+
content: [
|
|
62845
|
+
{
|
|
62846
|
+
type: "text",
|
|
62847
|
+
text: JSON.stringify(
|
|
62848
|
+
{
|
|
62849
|
+
file: absPath,
|
|
62850
|
+
importCount: result.imports.length,
|
|
62851
|
+
violationCount: result.violations.length,
|
|
62852
|
+
imports: result.imports,
|
|
62853
|
+
violations: result.violations,
|
|
62854
|
+
// Field name kept stable for backward compatibility with
|
|
62855
|
+
// older consumers; the value reflects whether the merged
|
|
62856
|
+
// `config.constitution` was declared, detected, or absent.
|
|
62857
|
+
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
62858
|
+
},
|
|
62859
|
+
null,
|
|
62860
|
+
2
|
|
62861
|
+
)
|
|
62862
|
+
}
|
|
62863
|
+
]
|
|
62864
|
+
};
|
|
62865
|
+
}
|
|
62866
|
+
async function runFindSimilar(args, ctx) {
|
|
62867
|
+
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
62868
|
+
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
62869
|
+
const props = Array.isArray(args.props) ? args.props : [];
|
|
62870
|
+
const limitRaw = args.limit;
|
|
62871
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
62872
|
+
try {
|
|
62873
|
+
const matches = await findSimilarFunctions2(
|
|
62874
|
+
{
|
|
62875
|
+
name: typeof args.name === "string" ? args.name : void 0,
|
|
62876
|
+
hooks,
|
|
62877
|
+
props,
|
|
62878
|
+
limit,
|
|
62879
|
+
workspaceDir: ctx.cwd
|
|
62880
|
+
},
|
|
62881
|
+
{ cwd: ctx.cwd }
|
|
62882
|
+
);
|
|
62883
|
+
return {
|
|
62884
|
+
content: [
|
|
62885
|
+
{
|
|
62886
|
+
type: "text",
|
|
62887
|
+
text: JSON.stringify(
|
|
62888
|
+
{
|
|
62889
|
+
hint: "Each match is ranked by Jaccard similarity over (hooks \u222A props \u222A params). similarity=1 means the matched signature has an identical feature set. Agents should prefer the top match instead of writing a new implementation.",
|
|
62890
|
+
count: matches.length,
|
|
62891
|
+
matches: matches.map((m) => ({
|
|
62892
|
+
name: m.signature.name,
|
|
62893
|
+
file: m.signature.fileRel,
|
|
62894
|
+
line: m.signature.line,
|
|
62895
|
+
similarity: Number(m.similarity.toFixed(3)),
|
|
62896
|
+
fingerprint: m.fingerprint,
|
|
62897
|
+
hooks: m.signature.hooks,
|
|
62898
|
+
props: m.signature.props,
|
|
62899
|
+
params: m.signature.params
|
|
62900
|
+
}))
|
|
62901
|
+
},
|
|
62902
|
+
null,
|
|
62903
|
+
2
|
|
62904
|
+
)
|
|
62905
|
+
}
|
|
62906
|
+
]
|
|
62907
|
+
};
|
|
62908
|
+
} catch (err) {
|
|
62909
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62910
|
+
}
|
|
62911
|
+
}
|
|
62912
|
+
async function handleToolCall(toolName, args, ctx) {
|
|
62913
|
+
const deprecation = getDeprecation(toolName);
|
|
62914
|
+
const deprecationNotice = deprecation ? {
|
|
62915
|
+
tool: toolName,
|
|
62916
|
+
replacedBy: deprecation.replacedBy,
|
|
62917
|
+
removedIn: deprecation.removedIn ?? "next major",
|
|
62918
|
+
reason: deprecation.reason
|
|
62919
|
+
} : void 0;
|
|
62920
|
+
switch (toolName) {
|
|
62921
|
+
case "slop_scan_file":
|
|
62922
|
+
return runScanFile(args, ctx);
|
|
62923
|
+
case "slop_explain_rule":
|
|
62924
|
+
return explainRule2(args, ctx);
|
|
62925
|
+
case "slop_list_rules":
|
|
62926
|
+
return listRules(args, ctx);
|
|
62927
|
+
case "slop_suggest":
|
|
62928
|
+
return runSuggest(args, ctx);
|
|
62929
|
+
case "slop_suggest_with_structure":
|
|
62930
|
+
return runSuggest(args, ctx, { includeStructure: true });
|
|
62931
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
62932
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
62933
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
62934
|
+
// Their runner functions (runGovernance, runArchitectureScore,
|
|
62935
|
+
// runBusinessLogicScore) are kept in the file for now
|
|
62936
|
+
// (marked @deprecated) to keep the diff small; they can be
|
|
62937
|
+
// deleted in a follow-up. New clients will never see these
|
|
62938
|
+
// tools listed in the MCP tools/list response.
|
|
62939
|
+
case "slop_check_constitution":
|
|
62940
|
+
return runCheckConstitution(args, ctx);
|
|
62941
|
+
case "slop_find_similar":
|
|
62942
|
+
return runFindSimilar(args, ctx);
|
|
62943
|
+
default:
|
|
62944
|
+
return toolError("Unknown tool: " + toolName);
|
|
62945
|
+
}
|
|
62946
|
+
}
|
|
62947
|
+
function getDeprecation(toolName) {
|
|
62948
|
+
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
62949
|
+
}
|
|
62950
|
+
|
|
62951
|
+
// src/mcp/server.ts
|
|
62145
62952
|
var SERVER_INFO = {
|
|
62146
62953
|
name: "slopbrick",
|
|
62147
62954
|
// v0.39.0: use the VERSION constant (same source as the CLI)
|
|
@@ -62209,7 +63016,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
62209
63016
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
62210
63017
|
}
|
|
62211
63018
|
async function runMcpServer(input, output, cwd) {
|
|
62212
|
-
return new Promise((
|
|
63019
|
+
return new Promise((resolve43) => {
|
|
62213
63020
|
let buffer = "";
|
|
62214
63021
|
input.setEncoding("utf-8");
|
|
62215
63022
|
input.on("data", (chunk) => {
|
|
@@ -62247,8 +63054,8 @@ async function runMcpServer(input, output, cwd) {
|
|
|
62247
63054
|
nlIdx = buffer.indexOf("\n");
|
|
62248
63055
|
}
|
|
62249
63056
|
});
|
|
62250
|
-
input.on("end", () =>
|
|
62251
|
-
input.on("close", () =>
|
|
63057
|
+
input.on("end", () => resolve43());
|
|
63058
|
+
input.on("close", () => resolve43());
|
|
62252
63059
|
});
|
|
62253
63060
|
}
|
|
62254
63061
|
|
|
@@ -62272,7 +63079,7 @@ function registerDoctor(program) {
|
|
|
62272
63079
|
|
|
62273
63080
|
// src/cli/commands/watch.ts
|
|
62274
63081
|
init_watch();
|
|
62275
|
-
import { resolve as
|
|
63082
|
+
import { resolve as resolve19 } from "path";
|
|
62276
63083
|
function registerWatch(program, scanAction) {
|
|
62277
63084
|
program.command("watch").description("re-run scan on every file change. Flags new violations as you write. The LockBrick prevention loop entry.").action(async (_cmdOptions, command) => {
|
|
62278
63085
|
const rawGlobals = command.optsWithGlobals();
|
|
@@ -62280,7 +63087,7 @@ function registerWatch(program, scanAction) {
|
|
|
62280
63087
|
...rawGlobals,
|
|
62281
63088
|
noIncrease: rawGlobals.increase === false
|
|
62282
63089
|
};
|
|
62283
|
-
const cwd =
|
|
63090
|
+
const cwd = resolve19(options.workspace ?? process.cwd());
|
|
62284
63091
|
await scanAction([], options, command);
|
|
62285
63092
|
await watchProject(options, cwd, []);
|
|
62286
63093
|
});
|
|
@@ -62288,11 +63095,11 @@ function registerWatch(program, scanAction) {
|
|
|
62288
63095
|
|
|
62289
63096
|
// src/cli/commands/lock.ts
|
|
62290
63097
|
init_logger();
|
|
62291
|
-
import { resolve as
|
|
63098
|
+
import { resolve as resolve20 } from "path";
|
|
62292
63099
|
function registerLock(program) {
|
|
62293
63100
|
program.command("lock").description("install a Git pre-commit hook that runs `slopbrick scan --staged` on every commit. The LockBrick prevention loop: block AI-introduced slop from ever reaching the repo.").option("--uninstall", "remove the pre-commit hook instead of installing it").option("--husky", "force-install under .husky/pre-commit (Husky v9). Default auto-detects via .husky/ dir.").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62294
63101
|
(cmdOptions) => {
|
|
62295
|
-
const cwd =
|
|
63102
|
+
const cwd = resolve20(cmdOptions.workspace ?? process.cwd());
|
|
62296
63103
|
if (cmdOptions.uninstall) {
|
|
62297
63104
|
const result2 = uninstallHook(cwd);
|
|
62298
63105
|
logger.info(result2.message);
|
|
@@ -62315,7 +63122,7 @@ function registerLock(program) {
|
|
|
62315
63122
|
// src/cli/commands/ci.ts
|
|
62316
63123
|
init_logger();
|
|
62317
63124
|
init_dist();
|
|
62318
|
-
import { resolve as
|
|
63125
|
+
import { resolve as resolve21 } from "path";
|
|
62319
63126
|
function registerCi(program, scanAction) {
|
|
62320
63127
|
program.command("ci").description("CI gate: run a scan and exit 1 on constitution violations, threshold breach, or new issues since the last run. Use this in GitHub Actions / GitLab CI.").option("--max-slop <n>", "exit 1 if slopIndex exceeds this number", parseCount).option("--max-new-issues <n>", "exit 1 if new issues (vs .slop-audit-cache.json) exceed this number", parseCount).option("--strict-constitution", "exit 1 on any constitution violation").option("--format <pretty|json>", "output format", "json").action(
|
|
62321
63128
|
async (cmdOptions, command) => {
|
|
@@ -62328,7 +63135,7 @@ function registerCi(program, scanAction) {
|
|
|
62328
63135
|
// scan only changed files
|
|
62329
63136
|
format: cmdOptions.format ?? "json"
|
|
62330
63137
|
};
|
|
62331
|
-
const cwd =
|
|
63138
|
+
const cwd = resolve21(options.workspace ?? process.cwd());
|
|
62332
63139
|
await scanAction([], options, command);
|
|
62333
63140
|
const health = loadHealth(cwd);
|
|
62334
63141
|
if (!health) {
|
|
@@ -62357,11 +63164,11 @@ function registerCi(program, scanAction) {
|
|
|
62357
63164
|
|
|
62358
63165
|
// src/cli/commands/memory.ts
|
|
62359
63166
|
init_logger();
|
|
62360
|
-
import { resolve as
|
|
63167
|
+
import { resolve as resolve22 } from "path";
|
|
62361
63168
|
function registerMemory(program) {
|
|
62362
63169
|
program.command("memory").description("show or regenerate .slopbrick/structure.md (the agent-readable repository summary) without re-scanning").option("--show", "print the current .slopbrick/structure.md to stdout (default if no flag is passed)").option("--regenerate", "re-render structure.md from the existing inventory.json + constitution.json (no scan)").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62363
63170
|
async (cmdOptions) => {
|
|
62364
|
-
const cwd =
|
|
63171
|
+
const cwd = resolve22(cmdOptions.workspace ?? process.cwd());
|
|
62365
63172
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
62366
63173
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62367
63174
|
if (cmdOptions.regenerate) {
|
|
@@ -62392,48 +63199,48 @@ function registerMemory(program) {
|
|
|
62392
63199
|
|
|
62393
63200
|
// src/cli/commands/migrate.ts
|
|
62394
63201
|
init_logger();
|
|
62395
|
-
import { resolve as
|
|
63202
|
+
import { resolve as resolve23 } from "path";
|
|
62396
63203
|
|
|
62397
63204
|
// src/cli/migrate.ts
|
|
62398
63205
|
init_logger();
|
|
62399
|
-
import { existsSync as
|
|
62400
|
-
import { join as
|
|
63206
|
+
import { existsSync as existsSync25, readFileSync as readFileSync30, renameSync as renameSync4, writeFileSync as writeFileSync15 } from "fs";
|
|
63207
|
+
import { join as join27 } from "path";
|
|
62401
63208
|
function planMigration(workspaceDir) {
|
|
62402
63209
|
const moves = [];
|
|
62403
63210
|
const rewrites = [];
|
|
62404
63211
|
const gitignoreEdits = [];
|
|
62405
|
-
const oldDir =
|
|
62406
|
-
const newDir =
|
|
62407
|
-
if (
|
|
63212
|
+
const oldDir = join27(workspaceDir, ".slop-audit");
|
|
63213
|
+
const newDir = join27(workspaceDir, ".slopbrick");
|
|
63214
|
+
if (existsSync25(oldDir)) {
|
|
62408
63215
|
moves.push({ from: oldDir, to: newDir, kind: "dir" });
|
|
62409
63216
|
rewrites.push({
|
|
62410
|
-
path:
|
|
63217
|
+
path: join27(newDir, "inventory.json"),
|
|
62411
63218
|
field: "version",
|
|
62412
63219
|
from: '"1"',
|
|
62413
63220
|
to: '"2"'
|
|
62414
63221
|
});
|
|
62415
63222
|
rewrites.push({
|
|
62416
|
-
path:
|
|
63223
|
+
path: join27(newDir, "constitution.json"),
|
|
62417
63224
|
field: "version",
|
|
62418
63225
|
from: '"1"',
|
|
62419
63226
|
to: '"2"'
|
|
62420
63227
|
});
|
|
62421
63228
|
}
|
|
62422
|
-
const oldCache =
|
|
62423
|
-
const newCache =
|
|
62424
|
-
if (
|
|
63229
|
+
const oldCache = join27(workspaceDir, ".slop-audit-cache.json");
|
|
63230
|
+
const newCache = join27(workspaceDir, ".slopbrick-cache.json");
|
|
63231
|
+
if (existsSync25(oldCache)) {
|
|
62425
63232
|
moves.push({ from: oldCache, to: newCache, kind: "file" });
|
|
62426
63233
|
}
|
|
62427
63234
|
for (const ext of ["mjs", "cjs", "js"]) {
|
|
62428
|
-
const oldCfg =
|
|
62429
|
-
const newCfg =
|
|
62430
|
-
if (
|
|
63235
|
+
const oldCfg = join27(workspaceDir, `slop-audit.config.${ext}`);
|
|
63236
|
+
const newCfg = join27(workspaceDir, `slopbrick.config.${ext}`);
|
|
63237
|
+
if (existsSync25(oldCfg)) {
|
|
62431
63238
|
moves.push({ from: oldCfg, to: newCfg, kind: "config" });
|
|
62432
63239
|
}
|
|
62433
63240
|
}
|
|
62434
|
-
const gi =
|
|
62435
|
-
if (
|
|
62436
|
-
const content =
|
|
63241
|
+
const gi = join27(workspaceDir, ".gitignore");
|
|
63242
|
+
if (existsSync25(gi)) {
|
|
63243
|
+
const content = readFileSync30(gi, "utf-8");
|
|
62437
63244
|
if (content.includes(".slop-audit/")) {
|
|
62438
63245
|
gitignoreEdits.push({
|
|
62439
63246
|
path: gi,
|
|
@@ -62452,7 +63259,7 @@ function planMigration(workspaceDir) {
|
|
|
62452
63259
|
return { moves, rewrites, gitignoreEdits };
|
|
62453
63260
|
}
|
|
62454
63261
|
function isAlreadyMigrated(workspaceDir) {
|
|
62455
|
-
return
|
|
63262
|
+
return existsSync25(join27(workspaceDir, ".slopbrick")) && !existsSync25(join27(workspaceDir, ".slop-audit"));
|
|
62456
63263
|
}
|
|
62457
63264
|
function applyMigration(plan, options = {}) {
|
|
62458
63265
|
if (options.dryRun) return;
|
|
@@ -62460,20 +63267,20 @@ function applyMigration(plan, options = {}) {
|
|
|
62460
63267
|
renameSync4(m.from, m.to);
|
|
62461
63268
|
}
|
|
62462
63269
|
for (const r of plan.rewrites) {
|
|
62463
|
-
if (!
|
|
62464
|
-
const content =
|
|
63270
|
+
if (!existsSync25(r.path)) continue;
|
|
63271
|
+
const content = readFileSync30(r.path, "utf-8");
|
|
62465
63272
|
const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
|
|
62466
|
-
|
|
63273
|
+
writeFileSync15(r.path, next);
|
|
62467
63274
|
}
|
|
62468
63275
|
for (const g of plan.gitignoreEdits) {
|
|
62469
|
-
const content =
|
|
63276
|
+
const content = readFileSync30(g.path, "utf-8");
|
|
62470
63277
|
const next = content.replaceAll(g.from, g.to);
|
|
62471
|
-
|
|
63278
|
+
writeFileSync15(g.path, next);
|
|
62472
63279
|
}
|
|
62473
63280
|
}
|
|
62474
63281
|
function runMigrate(options) {
|
|
62475
63282
|
const { workspace, dryRun = false, force = false } = options;
|
|
62476
|
-
if (!
|
|
63283
|
+
if (!existsSync25(workspace)) {
|
|
62477
63284
|
return {
|
|
62478
63285
|
ok: false,
|
|
62479
63286
|
alreadyMigrated: false,
|
|
@@ -62483,9 +63290,9 @@ function runMigrate(options) {
|
|
|
62483
63290
|
};
|
|
62484
63291
|
}
|
|
62485
63292
|
const alreadyMigrated = isAlreadyMigrated(workspace);
|
|
62486
|
-
const newDir =
|
|
62487
|
-
const oldDir =
|
|
62488
|
-
if (
|
|
63293
|
+
const newDir = join27(workspace, ".slopbrick");
|
|
63294
|
+
const oldDir = join27(workspace, ".slop-audit");
|
|
63295
|
+
if (existsSync25(newDir) && existsSync25(oldDir) && !force) {
|
|
62489
63296
|
return {
|
|
62490
63297
|
ok: false,
|
|
62491
63298
|
alreadyMigrated: false,
|
|
@@ -62561,7 +63368,7 @@ function registerMigrate(program) {
|
|
|
62561
63368
|
(cmdOptions, command) => {
|
|
62562
63369
|
const globals = command.optsWithGlobals();
|
|
62563
63370
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
62564
|
-
const cwd =
|
|
63371
|
+
const cwd = resolve23(cmdOptions.workspace ?? process.cwd());
|
|
62565
63372
|
const result = runMigrate({
|
|
62566
63373
|
workspace: cwd,
|
|
62567
63374
|
dryRun: cmdOptions.dryRun,
|
|
@@ -62667,12 +63474,12 @@ function registerRules(program) {
|
|
|
62667
63474
|
// src/cli/commands/validate-config.ts
|
|
62668
63475
|
init_logger();
|
|
62669
63476
|
init_validation();
|
|
62670
|
-
import { existsSync as
|
|
62671
|
-
import { extname as extname8, resolve as
|
|
63477
|
+
import { existsSync as existsSync26 } from "fs";
|
|
63478
|
+
import { extname as extname8, resolve as resolve24 } from "path";
|
|
62672
63479
|
function registerValidateConfig(program) {
|
|
62673
63480
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
62674
|
-
const path = configPath ?
|
|
62675
|
-
if (!
|
|
63481
|
+
const path = configPath ? resolve24(configPath) : resolve24(process.cwd(), "slopbrick.config.mjs");
|
|
63482
|
+
if (!existsSync26(path)) {
|
|
62676
63483
|
logger.error(`Error: config file not found: ${path}`);
|
|
62677
63484
|
process.exit(2);
|
|
62678
63485
|
}
|
|
@@ -62898,8 +63705,8 @@ ${formatMarkdown(result.report)}`);
|
|
|
62898
63705
|
|
|
62899
63706
|
// src/cli/commands/calibrate.ts
|
|
62900
63707
|
init_logger();
|
|
62901
|
-
import { mkdirSync as
|
|
62902
|
-
import { dirname as
|
|
63708
|
+
import { mkdirSync as mkdirSync14, writeFileSync as writeFileSync18 } from "fs";
|
|
63709
|
+
import { dirname as dirname19, resolve as resolve26 } from "path";
|
|
62903
63710
|
|
|
62904
63711
|
// src/research/providers/openai.ts
|
|
62905
63712
|
var OpenAiProvider = class {
|
|
@@ -62954,8 +63761,8 @@ function createProvider(config) {
|
|
|
62954
63761
|
}
|
|
62955
63762
|
|
|
62956
63763
|
// src/research/generator.ts
|
|
62957
|
-
import { mkdirSync as
|
|
62958
|
-
import { join as
|
|
63764
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync16 } from "fs";
|
|
63765
|
+
import { join as join28 } from "path";
|
|
62959
63766
|
|
|
62960
63767
|
// src/research/prompts.ts
|
|
62961
63768
|
var DEFAULT_PROMPT_TEMPLATES = [
|
|
@@ -63018,14 +63825,14 @@ async function generateSamples(options) {
|
|
|
63018
63825
|
}
|
|
63019
63826
|
const samples = [];
|
|
63020
63827
|
const ext = extForFramework(framework);
|
|
63021
|
-
const dir =
|
|
63022
|
-
|
|
63828
|
+
const dir = join28(outputDir, framework, componentType);
|
|
63829
|
+
mkdirSync12(dir, { recursive: true });
|
|
63023
63830
|
for (let i = 1; i <= count; i += 1) {
|
|
63024
63831
|
const raw = await provider.generateSample(renderPrompt(template), { temperature });
|
|
63025
63832
|
const code = extractCodeFromMarkdown(raw);
|
|
63026
63833
|
const fileName = `sample-${i}${ext}`;
|
|
63027
|
-
const filePath =
|
|
63028
|
-
|
|
63834
|
+
const filePath = join28(dir, fileName);
|
|
63835
|
+
writeFileSync16(filePath, code, "utf8");
|
|
63029
63836
|
const sample = {
|
|
63030
63837
|
filePath,
|
|
63031
63838
|
framework,
|
|
@@ -63036,8 +63843,8 @@ async function generateSamples(options) {
|
|
|
63036
63843
|
};
|
|
63037
63844
|
samples.push(sample);
|
|
63038
63845
|
}
|
|
63039
|
-
const metadataPath =
|
|
63040
|
-
|
|
63846
|
+
const metadataPath = join28(dir, "metadata.json");
|
|
63847
|
+
writeFileSync16(metadataPath, JSON.stringify(samples, null, 2), "utf8");
|
|
63041
63848
|
return samples;
|
|
63042
63849
|
}
|
|
63043
63850
|
|
|
@@ -63243,46 +64050,46 @@ function slugify(value) {
|
|
|
63243
64050
|
|
|
63244
64051
|
// src/research/calibrator.ts
|
|
63245
64052
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
63246
|
-
import { existsSync as
|
|
63247
|
-
import { join as
|
|
64053
|
+
import { existsSync as existsSync27, readFileSync as readFileSync31, writeFileSync as writeFileSync17, mkdirSync as mkdirSync13 } from "fs";
|
|
64054
|
+
import { join as join30, resolve as resolve25 } from "path";
|
|
63248
64055
|
|
|
63249
64056
|
// src/corpus-paths.ts
|
|
63250
|
-
import { join as
|
|
64057
|
+
import { join as join29 } from "path";
|
|
63251
64058
|
var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
|
|
63252
|
-
var POSITIVE_DIR =
|
|
63253
|
-
var NEGATIVE_DIR =
|
|
63254
|
-
var FILELISTS_DIR =
|
|
64059
|
+
var POSITIVE_DIR = join29(CORPUS_ROOT, "positive");
|
|
64060
|
+
var NEGATIVE_DIR = join29(CORPUS_ROOT, "negative");
|
|
64061
|
+
var FILELISTS_DIR = join29(CORPUS_ROOT, "filelists");
|
|
63255
64062
|
|
|
63256
64063
|
// src/research/calibrator.ts
|
|
63257
64064
|
var DEFAULT_POSITIVE = POSITIVE_DIR;
|
|
63258
64065
|
var DEFAULT_NEGATIVE = NEGATIVE_DIR;
|
|
63259
64066
|
function buildFileList(dir, extensions) {
|
|
63260
|
-
const tmpList =
|
|
64067
|
+
const tmpList = join30("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
63261
64068
|
const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
|
|
63262
64069
|
execFileSync2("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
|
|
63263
|
-
const out =
|
|
64070
|
+
const out = readFileSync31(tmpList, "utf8");
|
|
63264
64071
|
execFileSync2("rm", ["-f", tmpList]);
|
|
63265
64072
|
return out.trim().split("\n").filter(Boolean);
|
|
63266
64073
|
}
|
|
63267
64074
|
function runScan2(fileListPath) {
|
|
63268
|
-
const files =
|
|
64075
|
+
const files = readFileSync31(fileListPath, "utf8").trim().split("\n").filter(Boolean);
|
|
63269
64076
|
const CHUNK = 600;
|
|
63270
64077
|
const ruleFires = /* @__PURE__ */ new Map();
|
|
63271
64078
|
const uniqueFilesPerRule = /* @__PURE__ */ new Map();
|
|
63272
64079
|
let fileCount = 0;
|
|
63273
|
-
const tmpOut =
|
|
64080
|
+
const tmpOut = join30("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
63274
64081
|
for (let i = 0; i < files.length; i += CHUNK) {
|
|
63275
64082
|
const chunk = files.slice(i, i + CHUNK);
|
|
63276
64083
|
try {
|
|
63277
64084
|
execFileSync2(
|
|
63278
64085
|
"node",
|
|
63279
|
-
[
|
|
64086
|
+
[join30(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
|
|
63280
64087
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
63281
64088
|
);
|
|
63282
64089
|
} catch {
|
|
63283
64090
|
}
|
|
63284
|
-
if (!
|
|
63285
|
-
const report = JSON.parse(
|
|
64091
|
+
if (!existsSync27(tmpOut)) continue;
|
|
64092
|
+
const report = JSON.parse(readFileSync31(tmpOut, "utf8"));
|
|
63286
64093
|
fileCount += report.fileCount;
|
|
63287
64094
|
for (const issue of report.issues) {
|
|
63288
64095
|
ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
|
|
@@ -63310,16 +64117,16 @@ function classify(posFiles, negFiles) {
|
|
|
63310
64117
|
async function calibrate(cwd, options = {}) {
|
|
63311
64118
|
const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
|
|
63312
64119
|
const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
|
|
63313
|
-
if (!
|
|
63314
|
-
if (!
|
|
64120
|
+
if (!existsSync27(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
|
|
64121
|
+
if (!existsSync27(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
|
|
63315
64122
|
const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
|
|
63316
64123
|
const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
|
|
63317
64124
|
const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
|
|
63318
64125
|
const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
|
|
63319
|
-
const posListPath =
|
|
63320
|
-
const negListPath =
|
|
63321
|
-
|
|
63322
|
-
|
|
64126
|
+
const posListPath = join30("/tmp", `cal-pos-${Date.now()}.txt`);
|
|
64127
|
+
const negListPath = join30("/tmp", `cal-neg-${Date.now()}.txt`);
|
|
64128
|
+
writeFileSync17(posListPath, posSample.join("\n"));
|
|
64129
|
+
writeFileSync17(negListPath, negSample.join("\n"));
|
|
63323
64130
|
const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
|
|
63324
64131
|
const builtinRules2 = builtins.builtinRules ?? [];
|
|
63325
64132
|
const metaById = /* @__PURE__ */ new Map();
|
|
@@ -63433,9 +64240,9 @@ function registerCalibrate(program) {
|
|
|
63433
64240
|
positiveLimit: cmdOptions.positiveLimit,
|
|
63434
64241
|
negativeLimit: cmdOptions.negativeLimit
|
|
63435
64242
|
});
|
|
63436
|
-
const outputPath = cmdOptions.output ?
|
|
63437
|
-
|
|
63438
|
-
|
|
64243
|
+
const outputPath = cmdOptions.output ? resolve26(cwd, cmdOptions.output) : resolve26(cwd, "corpus", "calibration-empirical.md");
|
|
64244
|
+
mkdirSync14(dirname19(outputPath), { recursive: true });
|
|
64245
|
+
writeFileSync18(outputPath, reportToMarkdown(report), "utf8");
|
|
63439
64246
|
logger.info(
|
|
63440
64247
|
"Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
|
|
63441
64248
|
);
|
|
@@ -63574,12 +64381,13 @@ function registerTrend(program) {
|
|
|
63574
64381
|
// src/cli/commands/drift.ts
|
|
63575
64382
|
init_logger();
|
|
63576
64383
|
init_scan2();
|
|
63577
|
-
import { resolve as
|
|
64384
|
+
import { resolve as resolve27 } from "path";
|
|
63578
64385
|
|
|
63579
64386
|
// src/cli/drift.ts
|
|
63580
64387
|
init_discover();
|
|
63581
64388
|
init_patterns();
|
|
63582
|
-
|
|
64389
|
+
init_telemetry();
|
|
64390
|
+
import { readFileSync as readFileSync32 } from "fs";
|
|
63583
64391
|
import { basename as basename4, relative as relative16 } from "path";
|
|
63584
64392
|
async function runDrift(cwd, config, options = {}) {
|
|
63585
64393
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
@@ -63591,7 +64399,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63591
64399
|
for (const absPath of limited) {
|
|
63592
64400
|
let source;
|
|
63593
64401
|
try {
|
|
63594
|
-
source =
|
|
64402
|
+
source = readFileSync32(absPath, "utf-8");
|
|
63595
64403
|
} catch {
|
|
63596
64404
|
continue;
|
|
63597
64405
|
}
|
|
@@ -63688,26 +64496,217 @@ function formatDrift(result, opts = {}) {
|
|
|
63688
64496
|
function driftExitCode(result) {
|
|
63689
64497
|
return result.totalViolations > 0 ? 1 : 0;
|
|
63690
64498
|
}
|
|
64499
|
+
async function runDriftOverTime(cwd, config, options) {
|
|
64500
|
+
const payloads = readTelemetry(cwd);
|
|
64501
|
+
const sortedAsc = [...payloads].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
64502
|
+
const withInventory = sortedAsc.filter((p) => p.inventory !== void 0);
|
|
64503
|
+
const baselineSource = options.since === "baseline" ? "baseline" : "since";
|
|
64504
|
+
let baseline;
|
|
64505
|
+
if (baselineSource === "baseline") {
|
|
64506
|
+
baseline = withInventory[0];
|
|
64507
|
+
} else {
|
|
64508
|
+
baseline = withInventory.find((p) => p.timestamp >= options.since);
|
|
64509
|
+
}
|
|
64510
|
+
const current = withInventory.at(-1);
|
|
64511
|
+
if (!baseline || !current) {
|
|
64512
|
+
return {
|
|
64513
|
+
scannedFiles: 0,
|
|
64514
|
+
filesWithViolations: 0,
|
|
64515
|
+
totalViolations: 0,
|
|
64516
|
+
byCategory: {},
|
|
64517
|
+
byFile: [],
|
|
64518
|
+
conventionSource: deriveSource(config.constitution),
|
|
64519
|
+
constitution: config.constitution,
|
|
64520
|
+
introduced: [],
|
|
64521
|
+
removed: [],
|
|
64522
|
+
introducedUndeclared: [],
|
|
64523
|
+
snapshotsConsidered: withInventory.length,
|
|
64524
|
+
driftScore: 0,
|
|
64525
|
+
baselineAt: baseline?.timestamp ?? "",
|
|
64526
|
+
currentAt: current?.timestamp ?? "",
|
|
64527
|
+
baselineSource
|
|
64528
|
+
};
|
|
64529
|
+
}
|
|
64530
|
+
const baselineNames = flattenPatternNames(baseline.inventory.patternNames);
|
|
64531
|
+
const currentNames = flattenPatternNames(current.inventory.patternNames);
|
|
64532
|
+
const baselineSet = new Set(baselineNames.map((p) => `${p.category}\0${p.name}`));
|
|
64533
|
+
const currentSet = new Set(currentNames.map((p) => `${p.category}\0${p.name}`));
|
|
64534
|
+
const introduced = [];
|
|
64535
|
+
const removed = [];
|
|
64536
|
+
for (const p of currentNames) {
|
|
64537
|
+
const key = `${p.category}\0${p.name}`;
|
|
64538
|
+
if (!baselineSet.has(key)) introduced.push(p);
|
|
64539
|
+
}
|
|
64540
|
+
for (const p of baselineNames) {
|
|
64541
|
+
const key = `${p.category}\0${p.name}`;
|
|
64542
|
+
if (!currentSet.has(key)) removed.push(p);
|
|
64543
|
+
}
|
|
64544
|
+
introduced.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64545
|
+
removed.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64546
|
+
const declared = collectDeclaredNames(config.constitution);
|
|
64547
|
+
const introducedUndeclared = introduced.filter((p) => !declared.has(p.name.toLowerCase()));
|
|
64548
|
+
const baselineTotal = Math.max(baselineSet.size, 1);
|
|
64549
|
+
const driftScore = Math.min(
|
|
64550
|
+
100,
|
|
64551
|
+
Math.round((introduced.length + removed.length) / baselineTotal * 100)
|
|
64552
|
+
);
|
|
64553
|
+
return {
|
|
64554
|
+
scannedFiles: current.inventory.scannedFiles,
|
|
64555
|
+
filesWithViolations: 0,
|
|
64556
|
+
totalViolations: 0,
|
|
64557
|
+
byCategory: {},
|
|
64558
|
+
byFile: [],
|
|
64559
|
+
conventionSource: deriveSource(config.constitution),
|
|
64560
|
+
constitution: config.constitution,
|
|
64561
|
+
introduced,
|
|
64562
|
+
removed,
|
|
64563
|
+
introducedUndeclared,
|
|
64564
|
+
snapshotsConsidered: withInventory.length,
|
|
64565
|
+
driftScore,
|
|
64566
|
+
baselineAt: baseline.timestamp,
|
|
64567
|
+
currentAt: current.timestamp,
|
|
64568
|
+
baselineSource
|
|
64569
|
+
};
|
|
64570
|
+
}
|
|
64571
|
+
function flattenPatternNames(patternNames) {
|
|
64572
|
+
const out = [];
|
|
64573
|
+
for (const [category, names] of Object.entries(patternNames)) {
|
|
64574
|
+
for (const name of names) {
|
|
64575
|
+
out.push({ category, name });
|
|
64576
|
+
}
|
|
64577
|
+
}
|
|
64578
|
+
out.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64579
|
+
return out;
|
|
64580
|
+
}
|
|
64581
|
+
function collectDeclaredNames(constitution) {
|
|
64582
|
+
const out = /* @__PURE__ */ new Set();
|
|
64583
|
+
if (!constitution) return out;
|
|
64584
|
+
const fields = ["stateManagement", "dataFetching", "uiLibrary", "forms", "styling", "routing"];
|
|
64585
|
+
for (const f of fields) {
|
|
64586
|
+
for (const v of constitution[f] ?? []) {
|
|
64587
|
+
out.add(v.toLowerCase());
|
|
64588
|
+
}
|
|
64589
|
+
}
|
|
64590
|
+
for (const list of Object.values(constitution.custom ?? {})) {
|
|
64591
|
+
for (const v of list) {
|
|
64592
|
+
out.add(v.toLowerCase());
|
|
64593
|
+
}
|
|
64594
|
+
}
|
|
64595
|
+
for (const v of constitution.forbidden ?? []) {
|
|
64596
|
+
out.add(v.toLowerCase());
|
|
64597
|
+
}
|
|
64598
|
+
return out;
|
|
64599
|
+
}
|
|
64600
|
+
function formatDriftOverTime(result) {
|
|
64601
|
+
const lines = [];
|
|
64602
|
+
lines.push("Temporal drift report");
|
|
64603
|
+
lines.push("");
|
|
64604
|
+
if (result.snapshotsConsidered === 0 || !result.baselineAt || !result.currentAt) {
|
|
64605
|
+
lines.push(" No historical telemetry found at .slopbrick/flywheel/scans.jsonl.");
|
|
64606
|
+
lines.push(" Run a few scans with `slopbrick scan` first; the temporal drift");
|
|
64607
|
+
lines.push(" detector needs \u2265 2 scan payloads to compute a baseline window.");
|
|
64608
|
+
return lines.join("\n");
|
|
64609
|
+
}
|
|
64610
|
+
const sinceLabel = result.baselineSource === "baseline" ? `baseline (oldest scan)` : `since ${result.baselineAt}`;
|
|
64611
|
+
lines.push(` Window: ${sinceLabel} \u2192 ${result.currentAt}`);
|
|
64612
|
+
lines.push(` Snapshots considered: ${result.snapshotsConsidered}`);
|
|
64613
|
+
lines.push(` Drift score: ${result.driftScore} / 100 (informational)`);
|
|
64614
|
+
lines.push("");
|
|
64615
|
+
lines.push(` Patterns introduced: ${result.introduced.length}`);
|
|
64616
|
+
for (const p of result.introduced) {
|
|
64617
|
+
lines.push(` + ${p.category}/${p.name}`);
|
|
64618
|
+
}
|
|
64619
|
+
if (result.introducedUndeclared.length > 0) {
|
|
64620
|
+
lines.push("");
|
|
64621
|
+
lines.push(
|
|
64622
|
+
` Patterns introduced but not in declared constitution: ${result.introducedUndeclared.length}`
|
|
64623
|
+
);
|
|
64624
|
+
for (const p of result.introducedUndeclared) {
|
|
64625
|
+
lines.push(` ! ${p.category}/${p.name}`);
|
|
64626
|
+
}
|
|
64627
|
+
}
|
|
64628
|
+
lines.push("");
|
|
64629
|
+
lines.push(` Patterns removed: ${result.removed.length}`);
|
|
64630
|
+
for (const p of result.removed) {
|
|
64631
|
+
lines.push(` - ${p.category}/${p.name}`);
|
|
64632
|
+
}
|
|
64633
|
+
lines.push("");
|
|
64634
|
+
if (result.introducedUndeclared.length > 0) {
|
|
64635
|
+
lines.push(
|
|
64636
|
+
" Tip: add undeclared patterns to slopbrick.config.mjs#constitution to"
|
|
64637
|
+
);
|
|
64638
|
+
lines.push(" promote them into the declared set on the next scan.");
|
|
64639
|
+
} else if (result.introduced.length === 0 && result.removed.length === 0) {
|
|
64640
|
+
lines.push(" \u2713 No pattern churn since the baseline window.");
|
|
64641
|
+
}
|
|
64642
|
+
return lines.join("\n");
|
|
64643
|
+
}
|
|
64644
|
+
function driftOverTimeExitCode(result) {
|
|
64645
|
+
return result.introducedUndeclared.length > 0 ? 1 : 0;
|
|
64646
|
+
}
|
|
64647
|
+
|
|
64648
|
+
// src/cli/commands/drift.ts
|
|
64649
|
+
init_load();
|
|
64650
|
+
|
|
64651
|
+
// src/cli/commands/_shared.ts
|
|
64652
|
+
init_logger();
|
|
64653
|
+
import { CommanderError } from "commander";
|
|
64654
|
+
function setExitOverride(program) {
|
|
64655
|
+
program.exitOverride();
|
|
64656
|
+
}
|
|
64657
|
+
async function dispatch(program, runFn) {
|
|
64658
|
+
try {
|
|
64659
|
+
await runFn();
|
|
64660
|
+
} catch (err) {
|
|
64661
|
+
if (err instanceof CommanderError) {
|
|
64662
|
+
if (err.code !== "commander.helpDisplayed" && err.code !== "commander.help") {
|
|
64663
|
+
logger.error(err.message);
|
|
64664
|
+
}
|
|
64665
|
+
process.exit(err.exitCode);
|
|
64666
|
+
return;
|
|
64667
|
+
}
|
|
64668
|
+
throw err;
|
|
64669
|
+
}
|
|
64670
|
+
}
|
|
64671
|
+
function withExitCode(result, compute, message) {
|
|
64672
|
+
const code = compute(result);
|
|
64673
|
+
if (code === 0) return;
|
|
64674
|
+
throw new CommanderError(code, "slopbrick.exit", message);
|
|
64675
|
+
}
|
|
63691
64676
|
|
|
63692
64677
|
// src/cli/commands/drift.ts
|
|
63693
64678
|
function registerDrift(program) {
|
|
63694
64679
|
program.command("drift").description(
|
|
63695
64680
|
"detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
|
|
63696
|
-
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).
|
|
64681
|
+
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).option(
|
|
64682
|
+
"--temporal-since <date>",
|
|
64683
|
+
`temporal drift since the given date (ISO-8601, or the literal string \`baseline\` to use the oldest scan in scans.jsonl)`
|
|
64684
|
+
).action(
|
|
63697
64685
|
async (cmdOptions, command) => {
|
|
63698
|
-
|
|
63699
|
-
|
|
63700
|
-
|
|
63701
|
-
|
|
63702
|
-
|
|
63703
|
-
const
|
|
63704
|
-
|
|
63705
|
-
|
|
63706
|
-
|
|
63707
|
-
|
|
63708
|
-
|
|
63709
|
-
|
|
64686
|
+
const options = command.optsWithGlobals();
|
|
64687
|
+
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64688
|
+
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64689
|
+
const cwd = resolve27(options.workspace ?? process.cwd());
|
|
64690
|
+
if (cmdOptions.temporalSince !== void 0) {
|
|
64691
|
+
const temporalSince = cmdOptions.temporalSince.trim();
|
|
64692
|
+
if (temporalSince.length === 0) {
|
|
64693
|
+
throw new Error("--temporal-since expects a non-empty ISO date or `baseline`");
|
|
64694
|
+
}
|
|
64695
|
+
const sinceArg = temporalSince === "baseline" ? "baseline" : temporalSince;
|
|
64696
|
+
const config2 = await loadConfig(cwd);
|
|
64697
|
+
const result2 = await runDriftOverTime(cwd, config2, { since: sinceArg });
|
|
64698
|
+
logger.info(formatDriftOverTime(result2));
|
|
64699
|
+
withExitCode(
|
|
64700
|
+
result2,
|
|
64701
|
+
driftOverTimeExitCode,
|
|
64702
|
+
`drift --temporal-since: ${result2.introducedUndeclared.length} undeclared patterns introduced`
|
|
64703
|
+
);
|
|
64704
|
+
return;
|
|
63710
64705
|
}
|
|
64706
|
+
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64707
|
+
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
64708
|
+
logger.info(formatDrift(result, { json: format === "json" }));
|
|
64709
|
+
withExitCode(result, driftExitCode, `drift: ${result.totalViolations} violations`);
|
|
63711
64710
|
}
|
|
63712
64711
|
);
|
|
63713
64712
|
}
|
|
@@ -63715,7 +64714,7 @@ function registerDrift(program) {
|
|
|
63715
64714
|
// src/cli/commands/pr.ts
|
|
63716
64715
|
init_logger();
|
|
63717
64716
|
init_scan2();
|
|
63718
|
-
import { resolve as
|
|
64717
|
+
import { resolve as resolve29 } from "path";
|
|
63719
64718
|
|
|
63720
64719
|
// src/cli/pr.ts
|
|
63721
64720
|
init_git();
|
|
@@ -63723,8 +64722,8 @@ init_worker();
|
|
|
63723
64722
|
init_metrics();
|
|
63724
64723
|
init_patterns();
|
|
63725
64724
|
init_discover();
|
|
63726
|
-
import { readFileSync as
|
|
63727
|
-
import { extname as extname9, relative as relative17, resolve as
|
|
64725
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
64726
|
+
import { extname as extname9, relative as relative17, resolve as resolve28 } from "path";
|
|
63728
64727
|
import { execFile as execFileCb } from "child_process";
|
|
63729
64728
|
import { promisify as promisify2 } from "util";
|
|
63730
64729
|
import { minimatch as minimatch3 } from "minimatch";
|
|
@@ -63765,7 +64764,7 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
63765
64764
|
if (gitFiles.length === 0) return [];
|
|
63766
64765
|
const sourceFiles = [];
|
|
63767
64766
|
for (const relOrAbs of gitFiles) {
|
|
63768
|
-
const abs =
|
|
64767
|
+
const abs = resolve28(cwd, relOrAbs);
|
|
63769
64768
|
const ext = extname9(abs).toLowerCase();
|
|
63770
64769
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
63771
64770
|
const rel = relative17(cwd, abs).split("\\").join("/");
|
|
@@ -63796,7 +64795,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
63796
64795
|
for (const absPath of candidates) {
|
|
63797
64796
|
let source;
|
|
63798
64797
|
try {
|
|
63799
|
-
source =
|
|
64798
|
+
source = readFileSync33(absPath, "utf-8");
|
|
63800
64799
|
} catch {
|
|
63801
64800
|
continue;
|
|
63802
64801
|
}
|
|
@@ -63971,20 +64970,135 @@ function formatPrMarkdown(result) {
|
|
|
63971
64970
|
function prExitCode(result) {
|
|
63972
64971
|
return result.totalScore > result.threshold ? 1 : 0;
|
|
63973
64972
|
}
|
|
64973
|
+
function parseUnifiedDiffPaths(diff) {
|
|
64974
|
+
const paths = [];
|
|
64975
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64976
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
64977
|
+
const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
|
|
64978
|
+
if (!m) continue;
|
|
64979
|
+
const path = m[1];
|
|
64980
|
+
if (path === "/dev/null" || path === "dev/null") continue;
|
|
64981
|
+
if (seen.has(path)) continue;
|
|
64982
|
+
seen.add(path);
|
|
64983
|
+
paths.push(path);
|
|
64984
|
+
}
|
|
64985
|
+
return paths;
|
|
64986
|
+
}
|
|
64987
|
+
async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
64988
|
+
const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
|
|
64989
|
+
const maxFiles = options.maxFiles ?? 500;
|
|
64990
|
+
const { existsSync: existsSync32, readFileSync: readFileSync42 } = await import("fs");
|
|
64991
|
+
const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
|
|
64992
|
+
if (!existsSync32(diffPath)) {
|
|
64993
|
+
throw new Error(`Diff file not found: ${diffPath}`);
|
|
64994
|
+
}
|
|
64995
|
+
const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
|
|
64996
|
+
const diff = readFileSync42(diffAbs, "utf-8");
|
|
64997
|
+
const changedPaths = parseUnifiedDiffPaths(diff);
|
|
64998
|
+
if (changedPaths.length === 0) {
|
|
64999
|
+
return emptyPrResult(threshold);
|
|
65000
|
+
}
|
|
65001
|
+
return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
|
|
65002
|
+
}
|
|
65003
|
+
async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
65004
|
+
const { existsSync: existsSync32, statSync: statSync8 } = await import("fs");
|
|
65005
|
+
const { resolve: resolvePath } = await import("path");
|
|
65006
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
65007
|
+
const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
|
|
65008
|
+
const limited = paths.slice(0, maxFiles);
|
|
65009
|
+
const files = [];
|
|
65010
|
+
let totalScore = 0;
|
|
65011
|
+
const byCategory = {};
|
|
65012
|
+
const bySeverity = { low: 0, medium: 0, high: 0 };
|
|
65013
|
+
let constitutionViolations = 0;
|
|
65014
|
+
for (const rel of limited) {
|
|
65015
|
+
const abs = resolvePath(cwd, rel);
|
|
65016
|
+
if (!existsSync32(abs) || !statSync8(abs).isFile()) continue;
|
|
65017
|
+
let source;
|
|
65018
|
+
try {
|
|
65019
|
+
source = await readFile4(abs, "utf-8");
|
|
65020
|
+
} catch {
|
|
65021
|
+
continue;
|
|
65022
|
+
}
|
|
65023
|
+
let result;
|
|
65024
|
+
try {
|
|
65025
|
+
result = await scanFile2(abs, config, void 0, cwd);
|
|
65026
|
+
} catch {
|
|
65027
|
+
continue;
|
|
65028
|
+
}
|
|
65029
|
+
if (result.parseError) continue;
|
|
65030
|
+
const issues = result.issues;
|
|
65031
|
+
let slopPoints = 0;
|
|
65032
|
+
const issueEntries = [];
|
|
65033
|
+
for (const issue of issues) {
|
|
65034
|
+
slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
|
|
65035
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
65036
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
|
|
65037
|
+
issueEntries.push({
|
|
65038
|
+
ruleId: issue.ruleId,
|
|
65039
|
+
severity: issue.severity,
|
|
65040
|
+
line: issue.line,
|
|
65041
|
+
category: issue.category,
|
|
65042
|
+
message: issue.message
|
|
65043
|
+
});
|
|
65044
|
+
}
|
|
65045
|
+
totalScore += slopPoints;
|
|
65046
|
+
files.push({
|
|
65047
|
+
file: abs,
|
|
65048
|
+
relPath: rel,
|
|
65049
|
+
score: slopPoints,
|
|
65050
|
+
issueCount: issueEntries.length,
|
|
65051
|
+
constitutionViolationCount: 0,
|
|
65052
|
+
slopPoints,
|
|
65053
|
+
issues: issueEntries,
|
|
65054
|
+
constitutionViolations: []
|
|
65055
|
+
});
|
|
65056
|
+
}
|
|
65057
|
+
return {
|
|
65058
|
+
base: "diff",
|
|
65059
|
+
head: "diff",
|
|
65060
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65061
|
+
passed: totalScore <= threshold,
|
|
65062
|
+
threshold,
|
|
65063
|
+
totalScore,
|
|
65064
|
+
filesChanged: files.length,
|
|
65065
|
+
byCategory,
|
|
65066
|
+
bySeverity,
|
|
65067
|
+
files
|
|
65068
|
+
};
|
|
65069
|
+
}
|
|
65070
|
+
function emptyPrResult(threshold) {
|
|
65071
|
+
return {
|
|
65072
|
+
base: "diff",
|
|
65073
|
+
head: "diff",
|
|
65074
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65075
|
+
passed: true,
|
|
65076
|
+
threshold,
|
|
65077
|
+
totalScore: 0,
|
|
65078
|
+
filesChanged: 0,
|
|
65079
|
+
byCategory: {},
|
|
65080
|
+
bySeverity: { low: 0, medium: 0, high: 0 },
|
|
65081
|
+
files: []
|
|
65082
|
+
};
|
|
65083
|
+
}
|
|
63974
65084
|
|
|
63975
65085
|
// src/cli/commands/pr.ts
|
|
63976
65086
|
function registerPr(program) {
|
|
63977
65087
|
program.command("pr").description(
|
|
63978
65088
|
"Score the slop cost of the current PR. Compares --base (default: main) to --head (default: HEAD), scans only changed source files, returns a single weighted score. Exits 1 when score > --threshold (default 20)."
|
|
63979
|
-
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).action(
|
|
65089
|
+
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).option("--diff <file>", "unified-diff file (overrides --base/--head; meant for CI/Action use)").action(
|
|
63980
65090
|
async (cmdOptions, command) => {
|
|
63981
65091
|
try {
|
|
63982
65092
|
const options = command.optsWithGlobals();
|
|
63983
|
-
const cwd =
|
|
65093
|
+
const cwd = resolve29(options.workspace ?? process.cwd());
|
|
63984
65094
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
63985
65095
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
63986
65096
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
63987
|
-
const result = await
|
|
65097
|
+
const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
|
|
65098
|
+
format,
|
|
65099
|
+
threshold: cmdOptions.threshold,
|
|
65100
|
+
maxFiles: cmdOptions.maxFiles
|
|
65101
|
+
}) : await runPrScan(cwd, config, {
|
|
63988
65102
|
base: cmdOptions.base,
|
|
63989
65103
|
head: cmdOptions.head,
|
|
63990
65104
|
format,
|
|
@@ -64005,7 +65119,7 @@ function registerPr(program) {
|
|
|
64005
65119
|
init_logger();
|
|
64006
65120
|
init_scan2();
|
|
64007
65121
|
init_ai_security_risk();
|
|
64008
|
-
import { resolve as
|
|
65122
|
+
import { resolve as resolve30 } from "path";
|
|
64009
65123
|
function registerSecurity(program) {
|
|
64010
65124
|
program.command("security").description(
|
|
64011
65125
|
"AI Security Risk \u2014 categorical severity for security findings disproportionately introduced by AI-generated code"
|
|
@@ -64015,7 +65129,7 @@ function registerSecurity(program) {
|
|
|
64015
65129
|
const options = command.optsWithGlobals();
|
|
64016
65130
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64017
65131
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64018
|
-
const cwd =
|
|
65132
|
+
const cwd = resolve30(options.workspace ?? process.cwd());
|
|
64019
65133
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
64020
65134
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
64021
65135
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -64063,13 +65177,13 @@ function registerSecurity(program) {
|
|
|
64063
65177
|
// src/cli/commands/test.ts
|
|
64064
65178
|
init_logger();
|
|
64065
65179
|
init_scan2();
|
|
64066
|
-
import { resolve as
|
|
65180
|
+
import { resolve as resolve32 } from "path";
|
|
64067
65181
|
|
|
64068
65182
|
// src/cli/test.ts
|
|
64069
65183
|
init_scan2();
|
|
64070
65184
|
init_test_quality();
|
|
64071
65185
|
init_logger();
|
|
64072
|
-
import { resolve as
|
|
65186
|
+
import { resolve as resolve31 } from "path";
|
|
64073
65187
|
var TEST_INCLUDE_GLOBS = [
|
|
64074
65188
|
"**/*.test.{ts,tsx,js,jsx}",
|
|
64075
65189
|
"**/*.spec.{ts,tsx,js,jsx}",
|
|
@@ -64137,7 +65251,7 @@ function formatTestReport(result, opts = {}) {
|
|
|
64137
65251
|
const sortedFiles = [...byFile.keys()].sort();
|
|
64138
65252
|
for (const file of sortedFiles) {
|
|
64139
65253
|
const issues = byFile.get(file) ?? [];
|
|
64140
|
-
const rel = file.startsWith(
|
|
65254
|
+
const rel = file.startsWith(resolve31(process.cwd())) ? file : file;
|
|
64141
65255
|
lines.push("");
|
|
64142
65256
|
lines.push(` ${rel}`);
|
|
64143
65257
|
for (const issue of issues.slice(0, 20)) {
|
|
@@ -64166,7 +65280,7 @@ function registerTest(program) {
|
|
|
64166
65280
|
const options = command.optsWithGlobals();
|
|
64167
65281
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64168
65282
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64169
|
-
const cwd =
|
|
65283
|
+
const cwd = resolve32(options.workspace ?? process.cwd());
|
|
64170
65284
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64171
65285
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
64172
65286
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -64183,7 +65297,7 @@ function registerTest(program) {
|
|
|
64183
65297
|
init_logger();
|
|
64184
65298
|
init_scan2();
|
|
64185
65299
|
init_architecture_score();
|
|
64186
|
-
import { resolve as
|
|
65300
|
+
import { resolve as resolve33 } from "path";
|
|
64187
65301
|
function registerArchitecture(program) {
|
|
64188
65302
|
program.command("architecture").description(
|
|
64189
65303
|
"compute the Architecture Consistency Score (0-100) \u2014 one number for cross-file pattern duplication"
|
|
@@ -64193,7 +65307,7 @@ function registerArchitecture(program) {
|
|
|
64193
65307
|
const options = command.optsWithGlobals();
|
|
64194
65308
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64195
65309
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64196
|
-
const cwd =
|
|
65310
|
+
const cwd = resolve33(options.workspace ?? process.cwd());
|
|
64197
65311
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64198
65312
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
64199
65313
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -64210,12 +65324,12 @@ function registerArchitecture(program) {
|
|
|
64210
65324
|
// src/cli/commands/business-logic.ts
|
|
64211
65325
|
init_logger();
|
|
64212
65326
|
init_scan2();
|
|
64213
|
-
import { resolve as
|
|
65327
|
+
import { resolve as resolve34 } from "path";
|
|
64214
65328
|
|
|
64215
65329
|
// src/cli/business-logic.ts
|
|
64216
65330
|
init_discover();
|
|
64217
65331
|
init_business_logic();
|
|
64218
|
-
import { readFileSync as
|
|
65332
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
64219
65333
|
import { relative as relative18 } from "path";
|
|
64220
65334
|
async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
64221
65335
|
const maxFiles = options.maxFiles ?? 500;
|
|
@@ -64226,7 +65340,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64226
65340
|
for (const absPath of limited) {
|
|
64227
65341
|
let source;
|
|
64228
65342
|
try {
|
|
64229
|
-
source =
|
|
65343
|
+
source = readFileSync34(absPath, "utf-8");
|
|
64230
65344
|
} catch {
|
|
64231
65345
|
continue;
|
|
64232
65346
|
}
|
|
@@ -64338,7 +65452,7 @@ function registerBusinessLogic(program) {
|
|
|
64338
65452
|
const options = command.optsWithGlobals();
|
|
64339
65453
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64340
65454
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64341
|
-
const cwd =
|
|
65455
|
+
const cwd = resolve34(options.workspace ?? process.cwd());
|
|
64342
65456
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64343
65457
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
64344
65458
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -64356,7 +65470,7 @@ function registerBusinessLogic(program) {
|
|
|
64356
65470
|
// src/cli/commands/maintenance-cost.ts
|
|
64357
65471
|
init_logger();
|
|
64358
65472
|
init_scan2();
|
|
64359
|
-
import { resolve as
|
|
65473
|
+
import { resolve as resolve35 } from "path";
|
|
64360
65474
|
|
|
64361
65475
|
// src/cli/maintenance-cost.ts
|
|
64362
65476
|
init_scan2();
|
|
@@ -64480,7 +65594,7 @@ function registerMaintenanceCost(program) {
|
|
|
64480
65594
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64481
65595
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
64482
65596
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64483
|
-
const cwd =
|
|
65597
|
+
const cwd = resolve35(options.workspace ?? process.cwd());
|
|
64484
65598
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64485
65599
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
64486
65600
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64499,7 +65613,7 @@ function registerMaintenanceCost(program) {
|
|
|
64499
65613
|
// src/cli/commands/docs.ts
|
|
64500
65614
|
init_logger();
|
|
64501
65615
|
init_scan2();
|
|
64502
|
-
import { resolve as
|
|
65616
|
+
import { resolve as resolve36 } from "path";
|
|
64503
65617
|
|
|
64504
65618
|
// src/cli/docs.ts
|
|
64505
65619
|
init_scan2();
|
|
@@ -64624,7 +65738,7 @@ function registerDocs(program) {
|
|
|
64624
65738
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64625
65739
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64626
65740
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64627
|
-
const cwd =
|
|
65741
|
+
const cwd = resolve36(options.workspace ?? process.cwd());
|
|
64628
65742
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64629
65743
|
const result = await runDocsScan(cwd, config, {
|
|
64630
65744
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -64648,7 +65762,7 @@ function registerDocs(program) {
|
|
|
64648
65762
|
// src/cli/commands/db.ts
|
|
64649
65763
|
init_logger();
|
|
64650
65764
|
init_scan2();
|
|
64651
|
-
import { resolve as
|
|
65765
|
+
import { resolve as resolve37 } from "path";
|
|
64652
65766
|
|
|
64653
65767
|
// src/cli/db.ts
|
|
64654
65768
|
init_scan2();
|
|
@@ -64767,7 +65881,7 @@ function registerDb(program) {
|
|
|
64767
65881
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64768
65882
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64769
65883
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64770
|
-
const cwd =
|
|
65884
|
+
const cwd = resolve37(options.workspace ?? process.cwd());
|
|
64771
65885
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64772
65886
|
const result = await runDbScan(cwd, config, {
|
|
64773
65887
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64791,12 +65905,12 @@ function registerDb(program) {
|
|
|
64791
65905
|
// src/cli/commands/patterns.ts
|
|
64792
65906
|
init_logger();
|
|
64793
65907
|
init_scan2();
|
|
64794
|
-
import { resolve as
|
|
65908
|
+
import { resolve as resolve38 } from "path";
|
|
64795
65909
|
|
|
64796
65910
|
// src/engine/patterns.ts
|
|
64797
65911
|
init_patterns();
|
|
64798
65912
|
init_discover();
|
|
64799
|
-
import { readFileSync as
|
|
65913
|
+
import { readFileSync as readFileSync35 } from "fs";
|
|
64800
65914
|
import { basename as basename5, relative as relative19 } from "path";
|
|
64801
65915
|
var PATTERN_CATEGORIES = [
|
|
64802
65916
|
"modal",
|
|
@@ -64899,7 +66013,7 @@ function detectFormsFromFiles(files) {
|
|
|
64899
66013
|
for (const f of files) {
|
|
64900
66014
|
let source;
|
|
64901
66015
|
try {
|
|
64902
|
-
source =
|
|
66016
|
+
source = readFileSync35(f, "utf-8");
|
|
64903
66017
|
} catch {
|
|
64904
66018
|
continue;
|
|
64905
66019
|
}
|
|
@@ -65120,7 +66234,7 @@ function registerPatterns(program) {
|
|
|
65120
66234
|
const options = command.optsWithGlobals();
|
|
65121
66235
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
65122
66236
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
65123
|
-
const cwd =
|
|
66237
|
+
const cwd = resolve38(options.workspace ?? process.cwd());
|
|
65124
66238
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
65125
66239
|
const result = await runPatternsScan(cwd, config, {
|
|
65126
66240
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -65139,8 +66253,8 @@ function registerPatterns(program) {
|
|
|
65139
66253
|
// src/cli/commands/research.ts
|
|
65140
66254
|
init_logger();
|
|
65141
66255
|
init_config2();
|
|
65142
|
-
import { existsSync as
|
|
65143
|
-
import { dirname as
|
|
66256
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync15, readFileSync as readFileSync36, writeFileSync as writeFileSync19 } from "fs";
|
|
66257
|
+
import { dirname as dirname20, resolve as resolve39 } from "path";
|
|
65144
66258
|
function registerResearch(program) {
|
|
65145
66259
|
const research = program.command("research").description("research commands for the AI UI learning loop");
|
|
65146
66260
|
research.command("generate").description("generate synthetic UI samples").requiredOption("--count <n>", "number of samples", parseCount).requiredOption("--framework <name>", "target framework").requiredOption("--component-type <type>", "component type").requiredOption("--provider <name>", "AI provider (openai)").option("--api-key <key>", "API key for provider").option("--model <name>", "model name").option("--temperature <n>", "sampling temperature", parseFloat, 0.7).option("--output-dir <path>", "output directory", ".slopbrick/corpus/generated").action(async (cmdOptions) => {
|
|
@@ -65155,24 +66269,24 @@ function registerResearch(program) {
|
|
|
65155
66269
|
framework: cmdOptions.framework,
|
|
65156
66270
|
componentType: cmdOptions.componentType,
|
|
65157
66271
|
provider,
|
|
65158
|
-
outputDir:
|
|
66272
|
+
outputDir: resolve39(cmdOptions.outputDir),
|
|
65159
66273
|
temperature: cmdOptions.temperature
|
|
65160
66274
|
});
|
|
65161
66275
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
65162
66276
|
});
|
|
65163
66277
|
research.command("analyze").description("analyze generated samples and report coverage").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "analysis output path", ".slopbrick/flywheel/analysis.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").action(async (cmdOptions) => {
|
|
65164
66278
|
try {
|
|
65165
|
-
const metadataPath =
|
|
65166
|
-
if (!
|
|
66279
|
+
const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
|
|
66280
|
+
if (!existsSync28(metadataPath)) {
|
|
65167
66281
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65168
66282
|
process.exit(2);
|
|
65169
66283
|
}
|
|
65170
|
-
const samples = JSON.parse(
|
|
66284
|
+
const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
|
|
65171
66285
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65172
66286
|
const analysis = await analyzeSamples(samples, config);
|
|
65173
|
-
const outputPath =
|
|
65174
|
-
|
|
65175
|
-
|
|
66287
|
+
const outputPath = resolve39(cmdOptions.output);
|
|
66288
|
+
mkdirSync15(dirname20(outputPath), { recursive: true });
|
|
66289
|
+
writeFileSync19(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
65176
66290
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
65177
66291
|
logger.info(`Wrote analysis to ${outputPath}`);
|
|
65178
66292
|
} catch (error) {
|
|
@@ -65182,12 +66296,12 @@ function registerResearch(program) {
|
|
|
65182
66296
|
});
|
|
65183
66297
|
research.command("candidates").description("extract patterns from generated samples and emit candidate rules").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "output path", ".slopbrick/flywheel/rule-candidates.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").option("--min-frequency <n>", "minimum cluster frequency", parseCount, 2).option("--include-covered", "include samples already covered by AI-specific rules").action(async (cmdOptions) => {
|
|
65184
66298
|
try {
|
|
65185
|
-
const metadataPath =
|
|
65186
|
-
if (!
|
|
66299
|
+
const metadataPath = resolve39(cmdOptions.inputDir, "metadata.json");
|
|
66300
|
+
if (!existsSync28(metadataPath)) {
|
|
65187
66301
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65188
66302
|
process.exit(2);
|
|
65189
66303
|
}
|
|
65190
|
-
const samples = JSON.parse(
|
|
66304
|
+
const samples = JSON.parse(readFileSync36(metadataPath, "utf8"));
|
|
65191
66305
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65192
66306
|
const analysis = await analyzeSamples(samples, config);
|
|
65193
66307
|
const extraction = extractAndCluster(analysis.samples, {
|
|
@@ -65197,8 +66311,8 @@ function registerResearch(program) {
|
|
|
65197
66311
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
65198
66312
|
minFrequency: cmdOptions.minFrequency
|
|
65199
66313
|
});
|
|
65200
|
-
const outputPath =
|
|
65201
|
-
|
|
66314
|
+
const outputPath = resolve39(cmdOptions.output);
|
|
66315
|
+
mkdirSync15(dirname20(outputPath), { recursive: true });
|
|
65202
66316
|
const payload = {
|
|
65203
66317
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65204
66318
|
sampleCount: analysis.summary.total,
|
|
@@ -65206,7 +66320,7 @@ function registerResearch(program) {
|
|
|
65206
66320
|
fingerprintCount: extraction.total,
|
|
65207
66321
|
candidates
|
|
65208
66322
|
};
|
|
65209
|
-
|
|
66323
|
+
writeFileSync19(outputPath, JSON.stringify(payload, null, 2), "utf8");
|
|
65210
66324
|
logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
|
|
65211
66325
|
logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
|
|
65212
66326
|
} catch (error) {
|
|
@@ -65219,8 +66333,8 @@ function registerResearch(program) {
|
|
|
65219
66333
|
// src/cli/commands/init.ts
|
|
65220
66334
|
init_logger();
|
|
65221
66335
|
init_builtins();
|
|
65222
|
-
import { existsSync as
|
|
65223
|
-
import { dirname as
|
|
66336
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync16, readFileSync as readFileSync37, writeFileSync as writeFileSync20 } from "fs";
|
|
66337
|
+
import { dirname as dirname21, join as join32, resolve as resolve40 } from "path";
|
|
65224
66338
|
init_scan2();
|
|
65225
66339
|
init_config2();
|
|
65226
66340
|
init_threshold();
|
|
@@ -65228,145 +66342,9 @@ init_git();
|
|
|
65228
66342
|
init_cache();
|
|
65229
66343
|
|
|
65230
66344
|
// src/snippet/targets.ts
|
|
65231
|
-
|
|
65232
|
-
|
|
65233
|
-
|
|
65234
|
-
function aiSpecificRules(rules) {
|
|
65235
|
-
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
65236
|
-
}
|
|
65237
|
-
function categorizeRules(rules) {
|
|
65238
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
65239
|
-
for (const r of rules) {
|
|
65240
|
-
const list = byCategory.get(r.category) ?? [];
|
|
65241
|
-
list.push(r);
|
|
65242
|
-
byCategory.set(r.category, list);
|
|
65243
|
-
}
|
|
65244
|
-
for (const list of byCategory.values()) {
|
|
65245
|
-
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
65246
|
-
}
|
|
65247
|
-
return byCategory;
|
|
65248
|
-
}
|
|
65249
|
-
function baseContent(rules, toolLabel, intro) {
|
|
65250
|
-
const lines = [];
|
|
65251
|
-
lines.push("# " + toolLabel);
|
|
65252
|
-
lines.push("");
|
|
65253
|
-
lines.push(intro);
|
|
65254
|
-
lines.push("");
|
|
65255
|
-
const ai = aiSpecificRules(rules);
|
|
65256
|
-
const byCategory = categorizeRules(ai);
|
|
65257
|
-
lines.push("## Category-level directives");
|
|
65258
|
-
lines.push("");
|
|
65259
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65260
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65261
|
-
}
|
|
65262
|
-
lines.push("");
|
|
65263
|
-
lines.push("## Per-rule directives");
|
|
65264
|
-
lines.push("");
|
|
65265
|
-
for (const [category, list] of byCategory) {
|
|
65266
|
-
lines.push("### " + category);
|
|
65267
|
-
lines.push("");
|
|
65268
|
-
for (const rule of list) {
|
|
65269
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65270
|
-
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
65271
|
-
}
|
|
65272
|
-
lines.push("");
|
|
65273
|
-
}
|
|
65274
|
-
return lines.join("\n");
|
|
65275
|
-
}
|
|
65276
|
-
|
|
65277
|
-
// src/snippet/generators.ts
|
|
65278
|
-
function generateCursorSnippet(rules) {
|
|
65279
|
-
const lines = [];
|
|
65280
|
-
lines.push("# slopbrick cursor rules");
|
|
65281
|
-
lines.push("");
|
|
65282
|
-
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
65283
|
-
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
65284
|
-
lines.push("");
|
|
65285
|
-
lines.push("## Category-level directives");
|
|
65286
|
-
lines.push("");
|
|
65287
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65288
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65289
|
-
}
|
|
65290
|
-
lines.push("");
|
|
65291
|
-
lines.push("## Per-rule directives");
|
|
65292
|
-
lines.push("");
|
|
65293
|
-
const ai = aiSpecificRules(rules);
|
|
65294
|
-
const byCategory = categorizeRules(ai);
|
|
65295
|
-
for (const [category, list] of byCategory) {
|
|
65296
|
-
lines.push("### " + category);
|
|
65297
|
-
lines.push("");
|
|
65298
|
-
for (const rule of list) {
|
|
65299
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65300
|
-
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
65301
|
-
}
|
|
65302
|
-
lines.push("");
|
|
65303
|
-
}
|
|
65304
|
-
lines.push("## Verification");
|
|
65305
|
-
lines.push("");
|
|
65306
|
-
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
65307
|
-
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
65308
|
-
lines.push("");
|
|
65309
|
-
return lines.join("\n");
|
|
65310
|
-
}
|
|
65311
|
-
function generateAgentsMdSnippet(rules) {
|
|
65312
|
-
return baseContent(
|
|
65313
|
-
rules,
|
|
65314
|
-
"AGENTS.md",
|
|
65315
|
-
"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."
|
|
65316
|
-
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
|
|
65317
|
-
}
|
|
65318
|
-
function generateClaudeMdSnippet(rules) {
|
|
65319
|
-
return baseContent(
|
|
65320
|
-
rules,
|
|
65321
|
-
"CLAUDE.md",
|
|
65322
|
-
"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."
|
|
65323
|
-
) + "## 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";
|
|
65324
|
-
}
|
|
65325
|
-
function generateAiderSnippet(rules) {
|
|
65326
|
-
return baseContent(
|
|
65327
|
-
rules,
|
|
65328
|
-
"CONVENTIONS.md",
|
|
65329
|
-
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
65330
|
-
);
|
|
65331
|
-
}
|
|
65332
|
-
function generateWindsurfSnippet(rules) {
|
|
65333
|
-
return baseContent(
|
|
65334
|
-
rules,
|
|
65335
|
-
".windsurfrules",
|
|
65336
|
-
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
65337
|
-
);
|
|
65338
|
-
}
|
|
65339
|
-
function generateClineRules(rules) {
|
|
65340
|
-
return baseContent(
|
|
65341
|
-
rules,
|
|
65342
|
-
"AGENTS.md",
|
|
65343
|
-
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
65344
|
-
);
|
|
65345
|
-
}
|
|
65346
|
-
function generateCursorrulesLegacySnippet(rules) {
|
|
65347
|
-
return baseContent(
|
|
65348
|
-
rules,
|
|
65349
|
-
".cursorrules",
|
|
65350
|
-
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
65351
|
-
);
|
|
65352
|
-
}
|
|
65353
|
-
function generateGeminiSnippet(rules) {
|
|
65354
|
-
return baseContent(
|
|
65355
|
-
rules,
|
|
65356
|
-
"GEMINI.md",
|
|
65357
|
-
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
65358
|
-
);
|
|
65359
|
-
}
|
|
65360
|
-
function generateCopilotSnippet(rules) {
|
|
65361
|
-
return baseContent(
|
|
65362
|
-
rules,
|
|
65363
|
-
"copilot-instructions.md",
|
|
65364
|
-
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
65365
|
-
);
|
|
65366
|
-
}
|
|
65367
|
-
|
|
65368
|
-
// src/snippet/targets.ts
|
|
65369
|
-
var SNIPPET_TARGETS = [
|
|
66345
|
+
init_generators();
|
|
66346
|
+
import { join as join31 } from "path";
|
|
66347
|
+
var SNIPPET_TARGETS2 = [
|
|
65370
66348
|
{
|
|
65371
66349
|
flag: "cursor",
|
|
65372
66350
|
cliName: "--cursor",
|
|
@@ -65459,7 +66437,7 @@ var SNIPPET_TARGETS = [
|
|
|
65459
66437
|
}
|
|
65460
66438
|
];
|
|
65461
66439
|
function resolveTargetPath(target) {
|
|
65462
|
-
return target.isFolder ?
|
|
66440
|
+
return target.isFolder ? join31(target.path, target.filename) : target.path;
|
|
65463
66441
|
}
|
|
65464
66442
|
function renderMatrix() {
|
|
65465
66443
|
const lines = [];
|
|
@@ -65467,7 +66445,7 @@ function renderMatrix() {
|
|
|
65467
66445
|
lines.push("");
|
|
65468
66446
|
lines.push("| Flag | File | Agent |");
|
|
65469
66447
|
lines.push("|------|------|-------|");
|
|
65470
|
-
for (const t of
|
|
66448
|
+
for (const t of SNIPPET_TARGETS2) {
|
|
65471
66449
|
lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
|
|
65472
66450
|
}
|
|
65473
66451
|
return lines.join("\n");
|
|
@@ -65481,13 +66459,13 @@ function registerInit(program) {
|
|
|
65481
66459
|
logger.info(renderMatrix());
|
|
65482
66460
|
process.exit(0);
|
|
65483
66461
|
}
|
|
65484
|
-
const cwd =
|
|
65485
|
-
const configPath =
|
|
66462
|
+
const cwd = resolve40(options.workspace ?? process.cwd());
|
|
66463
|
+
const configPath = join32(cwd, "slopbrick.config.mjs");
|
|
65486
66464
|
const detected = detectStack(cwd);
|
|
65487
66465
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
65488
66466
|
const proposed = serializeConfig(fallbackConfig);
|
|
65489
|
-
if (
|
|
65490
|
-
const current =
|
|
66467
|
+
if (existsSync29(configPath) && !cmdOptions.yes) {
|
|
66468
|
+
const current = readFileSync37(configPath, "utf8");
|
|
65491
66469
|
logger.error(`A config file already exists at ${configPath}.`);
|
|
65492
66470
|
logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
|
|
65493
66471
|
logger.error("");
|
|
@@ -65508,7 +66486,7 @@ function registerInit(program) {
|
|
|
65508
66486
|
config = buildInitConfig(detected, answers);
|
|
65509
66487
|
usedWizard = true;
|
|
65510
66488
|
}
|
|
65511
|
-
|
|
66489
|
+
writeFileSync20(configPath, serializeConfig(config));
|
|
65512
66490
|
appendGitignore(cwd);
|
|
65513
66491
|
const refresh = await refreshRegistrySnapshot(cwd);
|
|
65514
66492
|
if (!refresh.ok) {
|
|
@@ -65532,27 +66510,27 @@ function registerInit(program) {
|
|
|
65532
66510
|
}
|
|
65533
66511
|
logger.info(refresh.message);
|
|
65534
66512
|
}
|
|
65535
|
-
const targetsToWrite =
|
|
66513
|
+
const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
|
|
65536
66514
|
if (cmdOptions.all) return true;
|
|
65537
66515
|
const opts = cmdOptions;
|
|
65538
66516
|
return Boolean(opts[t.flag]);
|
|
65539
66517
|
});
|
|
65540
66518
|
for (const target of targetsToWrite) {
|
|
65541
|
-
const snippetPath =
|
|
65542
|
-
|
|
66519
|
+
const snippetPath = join32(cwd, resolveTargetPath(target));
|
|
66520
|
+
mkdirSync16(dirname21(snippetPath), { recursive: true });
|
|
65543
66521
|
const generated = target.generator(builtinRules);
|
|
65544
|
-
if (!target.isFolder &&
|
|
65545
|
-
const existing =
|
|
66522
|
+
if (!target.isFolder && existsSync29(snippetPath)) {
|
|
66523
|
+
const existing = readFileSync37(snippetPath, "utf8");
|
|
65546
66524
|
if (existing.includes("<!-- slopbrick:begin -->")) {
|
|
65547
66525
|
const updated = existing.replace(
|
|
65548
66526
|
/<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
|
|
65549
66527
|
"<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
|
|
65550
66528
|
);
|
|
65551
|
-
|
|
66529
|
+
writeFileSync20(snippetPath, updated, "utf8");
|
|
65552
66530
|
if (!options.quiet) logger.info(`Updated ${snippetPath}`);
|
|
65553
66531
|
continue;
|
|
65554
66532
|
}
|
|
65555
|
-
|
|
66533
|
+
writeFileSync20(
|
|
65556
66534
|
snippetPath,
|
|
65557
66535
|
existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
|
|
65558
66536
|
"utf8"
|
|
@@ -65560,7 +66538,7 @@ function registerInit(program) {
|
|
|
65560
66538
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65561
66539
|
continue;
|
|
65562
66540
|
}
|
|
65563
|
-
|
|
66541
|
+
writeFileSync20(snippetPath, generated, "utf8");
|
|
65564
66542
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65565
66543
|
}
|
|
65566
66544
|
if (options.baseline) {
|
|
@@ -65580,12 +66558,12 @@ function registerInit(program) {
|
|
|
65580
66558
|
// src/cli/commands/flywheel.ts
|
|
65581
66559
|
init_logger();
|
|
65582
66560
|
init_telemetry();
|
|
65583
|
-
import { mkdirSync as
|
|
65584
|
-
import { dirname as
|
|
66561
|
+
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync21 } from "fs";
|
|
66562
|
+
import { dirname as dirname22, resolve as resolve41 } from "path";
|
|
65585
66563
|
|
|
65586
66564
|
// src/report/flywheel.ts
|
|
65587
|
-
import { existsSync as
|
|
65588
|
-
import { join as
|
|
66565
|
+
import { existsSync as existsSync30, readFileSync as readFileSync38 } from "fs";
|
|
66566
|
+
import { join as join33 } from "path";
|
|
65589
66567
|
function average(values) {
|
|
65590
66568
|
if (values.length === 0) return 0;
|
|
65591
66569
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
@@ -65675,7 +66653,7 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65675
66653
|
function registerFlywheel(program) {
|
|
65676
66654
|
program.command("flywheel").description("summarize aggregated scan telemetry").option("--format <pretty|json>", "output format", "pretty").option("--export <path>", "write summary as JSON to <path>").action(async (cmdOptions, command) => {
|
|
65677
66655
|
const options = command.optsWithGlobals();
|
|
65678
|
-
const cwd =
|
|
66656
|
+
const cwd = resolve41(options.workspace ?? process.cwd());
|
|
65679
66657
|
const payloads = readTelemetry(cwd);
|
|
65680
66658
|
if (payloads.length === 0) {
|
|
65681
66659
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -65683,9 +66661,9 @@ function registerFlywheel(program) {
|
|
|
65683
66661
|
}
|
|
65684
66662
|
const summary = summarizeTelemetry(payloads);
|
|
65685
66663
|
if (cmdOptions.export) {
|
|
65686
|
-
const exportPath =
|
|
65687
|
-
|
|
65688
|
-
|
|
66664
|
+
const exportPath = resolve41(cmdOptions.export);
|
|
66665
|
+
mkdirSync17(dirname22(exportPath), { recursive: true });
|
|
66666
|
+
writeFileSync21(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
65689
66667
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
65690
66668
|
process.exit(0);
|
|
65691
66669
|
}
|
|
@@ -65712,8 +66690,8 @@ init_dist2();
|
|
|
65712
66690
|
init_memory_io();
|
|
65713
66691
|
|
|
65714
66692
|
// src/fix/focus-ring.ts
|
|
65715
|
-
import { existsSync as
|
|
65716
|
-
import { readFileSync as
|
|
66693
|
+
import { existsSync as existsSync31 } from "fs";
|
|
66694
|
+
import { readFileSync as readFileSync39, writeFileSync as writeFileSync22 } from "fs";
|
|
65717
66695
|
var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
|
|
65718
66696
|
var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
65719
66697
|
:focus-visible {
|
|
@@ -65722,15 +66700,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
|
65722
66700
|
}
|
|
65723
66701
|
/* @slopbrick:v1.0.0:fix:focus-ring-end */`;
|
|
65724
66702
|
function applyFocusRingFix(targetFile) {
|
|
65725
|
-
if (!
|
|
66703
|
+
if (!existsSync31(targetFile)) {
|
|
65726
66704
|
return { applied: false, reason: "missing-global-css-target" };
|
|
65727
66705
|
}
|
|
65728
|
-
const content =
|
|
66706
|
+
const content = readFileSync39(targetFile, "utf-8");
|
|
65729
66707
|
if (content.includes(ANCHOR_START)) {
|
|
65730
66708
|
return { applied: false, reason: "already-present" };
|
|
65731
66709
|
}
|
|
65732
66710
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
65733
|
-
|
|
66711
|
+
writeFileSync22(targetFile, `${content}${separator}${CSS_BLOCK}
|
|
65734
66712
|
`);
|
|
65735
66713
|
return { applied: true };
|
|
65736
66714
|
}
|
|
@@ -65739,21 +66717,21 @@ function applyFocusRingFix(targetFile) {
|
|
|
65739
66717
|
init_layout_token();
|
|
65740
66718
|
|
|
65741
66719
|
// src/fix/use-client.ts
|
|
65742
|
-
import { readFileSync as
|
|
66720
|
+
import { readFileSync as readFileSync40, writeFileSync as writeFileSync23 } from "fs";
|
|
65743
66721
|
function applyUseClientFix(filePath) {
|
|
65744
|
-
const content =
|
|
66722
|
+
const content = readFileSync40(filePath, "utf-8");
|
|
65745
66723
|
const trimmed = content.trimStart();
|
|
65746
66724
|
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
|
|
65747
66725
|
return { applied: false, reason: "already-present" };
|
|
65748
66726
|
}
|
|
65749
|
-
|
|
66727
|
+
writeFileSync23(filePath, `"use client";
|
|
65750
66728
|
|
|
65751
66729
|
${content}`);
|
|
65752
66730
|
return { applied: true };
|
|
65753
66731
|
}
|
|
65754
66732
|
|
|
65755
66733
|
// src/fix/visual-codemod.ts
|
|
65756
|
-
import { readFileSync as
|
|
66734
|
+
import { readFileSync as readFileSync41, writeFileSync as writeFileSync24 } from "fs";
|
|
65757
66735
|
|
|
65758
66736
|
// src/fix/visual-codemods/tailwind.ts
|
|
65759
66737
|
var ARBITRARY_ESCAPE_RE = /\b(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|min-w|max-w|min-h|max-h|text-\w+|leading-\w+|rounded|border)-?\[(-?\d+(?:\.\d+)?)(px|rem|em|%|vh|vw)?\]/g;
|
|
@@ -65962,7 +66940,7 @@ var ALL_CODEMODS = [
|
|
|
65962
66940
|
{ name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
|
|
65963
66941
|
];
|
|
65964
66942
|
function applyVisualCodemods(filePath) {
|
|
65965
|
-
const original =
|
|
66943
|
+
const original = readFileSync41(filePath, "utf-8");
|
|
65966
66944
|
let content = original;
|
|
65967
66945
|
const reasons = [];
|
|
65968
66946
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -65978,7 +66956,7 @@ function applyVisualCodemods(filePath) {
|
|
|
65978
66956
|
}
|
|
65979
66957
|
}
|
|
65980
66958
|
if (content !== original) {
|
|
65981
|
-
|
|
66959
|
+
writeFileSync24(filePath, content);
|
|
65982
66960
|
}
|
|
65983
66961
|
return {
|
|
65984
66962
|
filePath,
|
|
@@ -66260,12 +67238,12 @@ import * as http from "http";
|
|
|
66260
67238
|
import * as https from "https";
|
|
66261
67239
|
import { URL as URL2 } from "url";
|
|
66262
67240
|
var sendBeacon = (url, payload, timeoutMs) => {
|
|
66263
|
-
return new Promise((
|
|
67241
|
+
return new Promise((resolve43) => {
|
|
66264
67242
|
let parsed;
|
|
66265
67243
|
try {
|
|
66266
67244
|
parsed = new URL2(url);
|
|
66267
67245
|
} catch {
|
|
66268
|
-
|
|
67246
|
+
resolve43();
|
|
66269
67247
|
return;
|
|
66270
67248
|
}
|
|
66271
67249
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -66286,7 +67264,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
|
|
|
66286
67264
|
const settle = () => {
|
|
66287
67265
|
if (settled) return;
|
|
66288
67266
|
settled = true;
|
|
66289
|
-
|
|
67267
|
+
resolve43();
|
|
66290
67268
|
};
|
|
66291
67269
|
let req;
|
|
66292
67270
|
try {
|
|
@@ -66366,14 +67344,16 @@ process.on("uncaughtException", (err) => {
|
|
|
66366
67344
|
});
|
|
66367
67345
|
async function runCli({ start }) {
|
|
66368
67346
|
try {
|
|
66369
|
-
const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
67347
|
+
const program = new Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
66370
67348
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
67349
|
+
setExitOverride(program);
|
|
66371
67350
|
registerInit(program);
|
|
66372
67351
|
registerInstall(program);
|
|
66373
67352
|
registerUninstall(program);
|
|
66374
67353
|
registerBadge(program);
|
|
66375
67354
|
registerSuggest(program);
|
|
66376
67355
|
registerExplain(program);
|
|
67356
|
+
registerComposite(program);
|
|
66377
67357
|
registerCalibration(program);
|
|
66378
67358
|
registerFlywheel(program);
|
|
66379
67359
|
const scanAction = async (paths, _options, command) => {
|
|
@@ -66392,7 +67372,7 @@ async function runCli({ start }) {
|
|
|
66392
67372
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
66393
67373
|
process.exit(2);
|
|
66394
67374
|
}
|
|
66395
|
-
const cwd =
|
|
67375
|
+
const cwd = resolve42(options.workspace ?? process.cwd());
|
|
66396
67376
|
if (options.trend !== void 0) {
|
|
66397
67377
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
66398
67378
|
if (runs.length === 0) {
|
|
@@ -66440,7 +67420,7 @@ async function runCli({ start }) {
|
|
|
66440
67420
|
});
|
|
66441
67421
|
}
|
|
66442
67422
|
if (options.baseline) {
|
|
66443
|
-
const cwd2 =
|
|
67423
|
+
const cwd2 = resolve42(options.workspace ?? process.cwd());
|
|
66444
67424
|
const configHash = hashConfig(config);
|
|
66445
67425
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
66446
67426
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
@@ -66552,7 +67532,9 @@ async function runCli({ start }) {
|
|
|
66552
67532
|
program.outputHelp();
|
|
66553
67533
|
process.exit(0);
|
|
66554
67534
|
}
|
|
66555
|
-
await program
|
|
67535
|
+
await dispatch(program, async () => {
|
|
67536
|
+
await program.parseAsync(process.argv);
|
|
67537
|
+
});
|
|
66556
67538
|
} catch (err) {
|
|
66557
67539
|
if (err instanceof ConfigValidationError) {
|
|
66558
67540
|
logger.error(err.message);
|