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.cjs
CHANGED
|
@@ -36,7 +36,7 @@ var VERSION;
|
|
|
36
36
|
var init_header = __esm({
|
|
37
37
|
"src/types/_header.ts"() {
|
|
38
38
|
"use strict";
|
|
39
|
-
VERSION = "0.
|
|
39
|
+
VERSION = "0.42.0";
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -45823,6 +45823,15 @@ function formatCompositeScore(report) {
|
|
|
45823
45823
|
)
|
|
45824
45824
|
);
|
|
45825
45825
|
}
|
|
45826
|
+
const composite = report.compositeScore;
|
|
45827
|
+
if (composite !== void 0) {
|
|
45828
|
+
lines.push("");
|
|
45829
|
+
lines.push(
|
|
45830
|
+
import_chalk.default.dim(
|
|
45831
|
+
`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.`
|
|
45832
|
+
)
|
|
45833
|
+
);
|
|
45834
|
+
}
|
|
45826
45835
|
return lines.join("\n");
|
|
45827
45836
|
}
|
|
45828
45837
|
function formatCoherenceScores(report) {
|
|
@@ -46875,11 +46884,11 @@ var init_pool = __esm({
|
|
|
46875
46884
|
for (let i = 0; i < this.threadCount; i++) {
|
|
46876
46885
|
spawnWorker();
|
|
46877
46886
|
}
|
|
46878
|
-
return new Promise((
|
|
46887
|
+
return new Promise((resolve43) => {
|
|
46879
46888
|
const check = setInterval(() => {
|
|
46880
46889
|
if (resolved) {
|
|
46881
46890
|
clearInterval(check);
|
|
46882
|
-
|
|
46891
|
+
resolve43(results);
|
|
46883
46892
|
}
|
|
46884
46893
|
}, 10);
|
|
46885
46894
|
});
|
|
@@ -46896,23 +46905,16 @@ var init_registry = __esm({
|
|
|
46896
46905
|
init_builtins();
|
|
46897
46906
|
RuleRegistry = class {
|
|
46898
46907
|
rules = /* @__PURE__ */ new Map();
|
|
46899
|
-
|
|
46900
|
-
|
|
46901
|
-
|
|
46902
|
-
|
|
46903
|
-
|
|
46904
|
-
|
|
46905
|
-
|
|
46906
|
-
|
|
46907
|
-
|
|
46908
|
-
|
|
46909
|
-
create: factory.create,
|
|
46910
|
-
analyze: factory.analyze
|
|
46911
|
-
};
|
|
46912
|
-
this.rules.set(ruleOrId, rule);
|
|
46913
|
-
} else {
|
|
46914
|
-
this.rules.set(ruleOrId.id, ruleOrId);
|
|
46915
|
-
}
|
|
46908
|
+
/**
|
|
46909
|
+
* Register a rule. The 2-arg overload `register(id, factory)` was
|
|
46910
|
+
* deleted in v0.42.0 (architecture review F6): no callers used it,
|
|
46911
|
+
* every `Rule` carries its own `id`, and the per-id registration
|
|
46912
|
+
* was ergonomically redundant with the 1-arg form (`register(rule)`).
|
|
46913
|
+
* External consumers that needed `register-by-id` semantics can
|
|
46914
|
+
* construct a `Rule` directly and pass it in.
|
|
46915
|
+
*/
|
|
46916
|
+
register(rule) {
|
|
46917
|
+
this.rules.set(rule.id, rule);
|
|
46916
46918
|
}
|
|
46917
46919
|
loadBuiltins(onlyRuleId) {
|
|
46918
46920
|
for (const rule of builtinRules) {
|
|
@@ -46941,6 +46943,15 @@ var init_registry = __esm({
|
|
|
46941
46943
|
all() {
|
|
46942
46944
|
return Array.from(this.rules.values());
|
|
46943
46945
|
}
|
|
46946
|
+
/** v0.42.0 (§3b.5): look up a registered rule by id.
|
|
46947
|
+
* Returns undefined if not registered. */
|
|
46948
|
+
get(id) {
|
|
46949
|
+
return this.rules.get(id);
|
|
46950
|
+
}
|
|
46951
|
+
/** v0.42.0 (§3b.5): check whether a rule id is registered. */
|
|
46952
|
+
has(id) {
|
|
46953
|
+
return this.rules.has(id);
|
|
46954
|
+
}
|
|
46944
46955
|
createContexts(config, filePath, cwd, hotspotIssues = []) {
|
|
46945
46956
|
const context = {
|
|
46946
46957
|
config,
|
|
@@ -50592,6 +50603,10 @@ var init_signal_strength2 = __esm({
|
|
|
50592
50603
|
});
|
|
50593
50604
|
|
|
50594
50605
|
// src/engine/worker.ts
|
|
50606
|
+
var worker_exports = {};
|
|
50607
|
+
__export(worker_exports, {
|
|
50608
|
+
scanFile: () => scanFile
|
|
50609
|
+
});
|
|
50595
50610
|
function buildParserCacheConfig(cwd) {
|
|
50596
50611
|
const envVal = process.env.SLOP_AUDIT_CACHE;
|
|
50597
50612
|
const enabled = envVal === "1" || envVal === "true";
|
|
@@ -50668,6 +50683,36 @@ async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
|
50668
50683
|
const issues = applyRuleOverrides(rawIssues, config.rules);
|
|
50669
50684
|
const triggeredRuleIds = Array.from(new Set(issues.map((i) => i.ruleId)));
|
|
50670
50685
|
const compScore = compositeScore(triggeredRuleIds, loadSignalStrength());
|
|
50686
|
+
const fireSet = new Set(triggeredRuleIds);
|
|
50687
|
+
const factsWithFireSet = Object.assign(facts, {
|
|
50688
|
+
compositeFireSet: fireSet
|
|
50689
|
+
});
|
|
50690
|
+
for (const { rule } of rules) {
|
|
50691
|
+
if (!rule.id.startsWith("composite/")) continue;
|
|
50692
|
+
try {
|
|
50693
|
+
const compositeIssues = rule.analyze(
|
|
50694
|
+
rule.create({
|
|
50695
|
+
config,
|
|
50696
|
+
filePath,
|
|
50697
|
+
cwd,
|
|
50698
|
+
framework: config.framework,
|
|
50699
|
+
uiLibraries: config.uiLibraries,
|
|
50700
|
+
hasTailwind: config.hasTailwind,
|
|
50701
|
+
supportsRsc: config.supportsRsc,
|
|
50702
|
+
hotspotIssues: []
|
|
50703
|
+
}),
|
|
50704
|
+
factsWithFireSet
|
|
50705
|
+
);
|
|
50706
|
+
if (compositeIssues.length > 0) {
|
|
50707
|
+
issues.push(...compositeIssues);
|
|
50708
|
+
for (const ci of compositeIssues) fireSet.add(ci.ruleId);
|
|
50709
|
+
}
|
|
50710
|
+
} catch (err) {
|
|
50711
|
+
if (process.env.SLOP_AUDIT_DEBUG === "1") {
|
|
50712
|
+
console.error(`[worker] composite ${rule.id} threw: ${err.message}`);
|
|
50713
|
+
}
|
|
50714
|
+
}
|
|
50715
|
+
}
|
|
50671
50716
|
const gapValues = collectGapValues(facts);
|
|
50672
50717
|
const styleSources = collectStyleSources(facts);
|
|
50673
50718
|
const elementTags = facts.v2.jsx.elements.map((e) => e.tag);
|
|
@@ -54238,6 +54283,21 @@ function safeRelative(cwd, filePath) {
|
|
|
54238
54283
|
return filePath;
|
|
54239
54284
|
}
|
|
54240
54285
|
}
|
|
54286
|
+
function buildInventorySummary(inventory) {
|
|
54287
|
+
const patternCounts = {};
|
|
54288
|
+
const patternNames = {};
|
|
54289
|
+
for (const [category, matches] of Object.entries(inventory.patterns)) {
|
|
54290
|
+
if (matches.length === 0) continue;
|
|
54291
|
+
const names = Array.from(new Set(matches.map((m) => m.name))).sort();
|
|
54292
|
+
patternCounts[category] = names.length;
|
|
54293
|
+
patternNames[category] = names.slice(0, TELEMETRY_INVENTORY_NAME_CAP);
|
|
54294
|
+
}
|
|
54295
|
+
return {
|
|
54296
|
+
scannedFiles: inventory.scannedFiles,
|
|
54297
|
+
patternCounts,
|
|
54298
|
+
patternNames
|
|
54299
|
+
};
|
|
54300
|
+
}
|
|
54241
54301
|
function aggregateViolations(report) {
|
|
54242
54302
|
const counts = /* @__PURE__ */ new Map();
|
|
54243
54303
|
for (const issue of report.issues) {
|
|
@@ -54317,7 +54377,7 @@ function readTelemetry(cwd) {
|
|
|
54317
54377
|
}
|
|
54318
54378
|
return payloads;
|
|
54319
54379
|
}
|
|
54320
|
-
function recordTelemetry(cwd, report, results, config) {
|
|
54380
|
+
function recordTelemetry(cwd, report, results, config, inventory) {
|
|
54321
54381
|
if (config.telemetry === false) {
|
|
54322
54382
|
return void 0;
|
|
54323
54383
|
}
|
|
@@ -54338,7 +54398,14 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54338
54398
|
framework: config.framework
|
|
54339
54399
|
},
|
|
54340
54400
|
violations: aggregateViolations(report),
|
|
54341
|
-
files: buildFileRecords(cwd, report, results)
|
|
54401
|
+
files: buildFileRecords(cwd, report, results),
|
|
54402
|
+
// v0.41.0 (Sprint 2, task 2a.1): the inventory summary is
|
|
54403
|
+
// additive — omitting it (legacy callers) keeps the JSONL line
|
|
54404
|
+
// shape identical to v0.40.x payloads, so old readers stay
|
|
54405
|
+
// green. New readers (`slopbrick drift --since <date>` in
|
|
54406
|
+
// Sprint 2a.2) treat the field as optional and fall back to
|
|
54407
|
+
// a re-scan when it's missing.
|
|
54408
|
+
...inventory ? { inventory: buildInventorySummary(inventory) } : {}
|
|
54342
54409
|
};
|
|
54343
54410
|
const path = telemetryPath2(cwd);
|
|
54344
54411
|
const dir = (0, import_node_path22.dirname)(path);
|
|
@@ -54349,7 +54416,7 @@ function recordTelemetry(cwd, report, results, config) {
|
|
|
54349
54416
|
(0, import_node_fs20.appendFileSync)(path, JSON.stringify(payload) + "\n", "utf-8");
|
|
54350
54417
|
return payload;
|
|
54351
54418
|
}
|
|
54352
|
-
var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES;
|
|
54419
|
+
var import_node_fs20, import_node_crypto8, import_node_path22, TELEMETRY_DIR, TELEMETRY_FILE2, MAX_TELEMETRY_BYTES, MAX_ROTATED_FILES, TELEMETRY_INVENTORY_NAME_CAP;
|
|
54353
54420
|
var init_telemetry = __esm({
|
|
54354
54421
|
"src/engine/telemetry.ts"() {
|
|
54355
54422
|
"use strict";
|
|
@@ -54360,6 +54427,7 @@ var init_telemetry = __esm({
|
|
|
54360
54427
|
TELEMETRY_FILE2 = "scans.jsonl";
|
|
54361
54428
|
MAX_TELEMETRY_BYTES = 10 * 1024 * 1024;
|
|
54362
54429
|
MAX_ROTATED_FILES = 5;
|
|
54430
|
+
TELEMETRY_INVENTORY_NAME_CAP = 50;
|
|
54363
54431
|
}
|
|
54364
54432
|
});
|
|
54365
54433
|
|
|
@@ -54510,29 +54578,29 @@ function renderStructureMarkdown(inventory, constitution) {
|
|
|
54510
54578
|
return lines.join("\n");
|
|
54511
54579
|
}
|
|
54512
54580
|
async function writeStructureMarkdown(workspaceDir, md) {
|
|
54513
|
-
await new Promise((
|
|
54581
|
+
await new Promise((resolve43, reject) => {
|
|
54514
54582
|
try {
|
|
54515
54583
|
const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
54516
54584
|
(0, import_node_fs21.mkdirSync)((0, import_node_path23.dirname)(path), { recursive: true });
|
|
54517
54585
|
(0, import_node_fs21.writeFileSync)(path, md, "utf-8");
|
|
54518
|
-
|
|
54586
|
+
resolve43();
|
|
54519
54587
|
} catch (err) {
|
|
54520
54588
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
54521
54589
|
}
|
|
54522
54590
|
});
|
|
54523
54591
|
}
|
|
54524
54592
|
async function readStructureMarkdown(workspaceDir) {
|
|
54525
|
-
return new Promise((
|
|
54593
|
+
return new Promise((resolve43) => {
|
|
54526
54594
|
try {
|
|
54527
54595
|
const path = (0, import_node_path23.join)(workspaceDir, STRUCTURE_MD_FILE);
|
|
54528
54596
|
if (!(0, import_node_fs21.existsSync)(path)) {
|
|
54529
|
-
|
|
54597
|
+
resolve43(null);
|
|
54530
54598
|
return;
|
|
54531
54599
|
}
|
|
54532
54600
|
const content = (0, import_node_fs21.readFileSync)(path, "utf-8");
|
|
54533
|
-
|
|
54601
|
+
resolve43(content);
|
|
54534
54602
|
} catch {
|
|
54535
|
-
|
|
54603
|
+
resolve43(null);
|
|
54536
54604
|
}
|
|
54537
54605
|
});
|
|
54538
54606
|
}
|
|
@@ -54582,6 +54650,388 @@ var init_structure_md = __esm({
|
|
|
54582
54650
|
}
|
|
54583
54651
|
});
|
|
54584
54652
|
|
|
54653
|
+
// src/snippet/data.ts
|
|
54654
|
+
var CATEGORY_DIRECTIVES, RULE_HINTS;
|
|
54655
|
+
var init_data = __esm({
|
|
54656
|
+
"src/snippet/data.ts"() {
|
|
54657
|
+
"use strict";
|
|
54658
|
+
CATEGORY_DIRECTIVES = {
|
|
54659
|
+
visual: 'Avoid the saturated "vibe purple" Tailwind palette (violet-400-700, indigo-400-700). Prefer emerald, sky, amber, rose for accents. Never use arbitrary color values like bg-[#7c3aed] when a token exists.',
|
|
54660
|
+
logic: "Never use explicit `any`. Use `unknown` and narrow. Always add an AbortSignal to fetches. Handle errors with try/catch, never swallow with empty catch. Use `as const` instead of `as Type` casts.",
|
|
54661
|
+
wcag: 'All form inputs must have an accessible label (visible <label>, aria-label, or aria-labelledby). Decorative images must have empty alt="". Buttons must be <button>, never <div onClick>. Touch targets must be \u2265 24\xD724 CSS px.',
|
|
54662
|
+
security: 'Never store tokens in localStorage or sessionStorage \u2014 use httpOnly Secure SameSite cookies. Never put secrets in NEXT_PUBLIC_* / REACT_APP_* / VITE_* env vars. Validate e.origin in postMessage handlers. Never dangerouslySetInnerHTML with user input. Never use target="_blank" without rel="noopener".',
|
|
54663
|
+
perf: "Always use AbortController with fetch. Use image width/height attributes to prevent CLS. Use <Suspense> around async client components. Don't load all images eagerly.",
|
|
54664
|
+
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
54665
|
+
layout: "Don't stack badge-above-h1 hero patterns. Don't build 3-stat banner rows without explicit user request. Don't use emoji inside nav items (use SVG icons). Use the project's spacing scale (4px or 8px grid), never arbitrary values like p-[13px].",
|
|
54666
|
+
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
54667
|
+
arch: "For Astro: server-render everything by default; only opt-in to client islands when you need interactivity. Don't put secrets in client-side code.",
|
|
54668
|
+
test: "Use domain-specific fixture data, assert on value shapes not just truthiness, and consolidate repeated setup into helpers. Avoid `expect(x).toBeDefined()` placeholders and textbook fixtures like 'John Doe' or 'test@test.com'."
|
|
54669
|
+
};
|
|
54670
|
+
RULE_HINTS = {
|
|
54671
|
+
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
54672
|
+
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
54673
|
+
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
54674
|
+
// docs/research/backlog-rule-hints.md
|
|
54675
|
+
// for future implementers to paste back when the corresponding rules ship.)
|
|
54676
|
+
"security/hardcoded-secret": "Never inline API keys, JWT secrets, or database passwords in source. Load them from env vars and never commit a .env file. Assume any published secret is compromised and rotate it.",
|
|
54677
|
+
"security/exposed-env-var": "NEVER prefix a secret with NEXT_PUBLIC_, VITE_, REACT_APP_, EXPO_PUBLIC_, GATSBY_, or PUBLIC_ \u2014 those vars are inlined into every browser build.",
|
|
54678
|
+
"security/dangerous-cors": "Don't set Access-Control-Allow-Origin: * on production endpoints. Restrict to an explicit allowlist; never combine wildcard origin with credentials: true.",
|
|
54679
|
+
"security/missing-auth-check": "Every server route handler must perform an authentication + authorization check at the top. Reachability of an endpoint by any user (authenticated or not) is a vulnerability, not a feature.",
|
|
54680
|
+
"security/unsafe-html-render": "Sanitize any non-literal value passed to dangerouslySetInnerHTML with DOMPurify. Better: avoid the prop entirely and let React escape via children.",
|
|
54681
|
+
"security/fail-open-auth": "Don't gate auth bypasses on NODE_ENV. Replace dev-env checks with an explicit AUTH_BYPASS flag that's never set in production.",
|
|
54682
|
+
"security/sql-construction": 'Never build SQL with string concatenation or template-literal interpolation. Use parameterized queries: pg client.query("... WHERE id = $1", [id]) or your ORM query builder.',
|
|
54683
|
+
"security/public-admin-route": "Routes under /admin, /internal, /debug, /staff, /manage, /private need an explicit role check on top of standard auth \u2014 auth alone is not enough for privileged paths.",
|
|
54684
|
+
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
54685
|
+
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
54686
|
+
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
54687
|
+
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
54688
|
+
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
54689
|
+
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
54690
|
+
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
54691
|
+
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
54692
|
+
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
54693
|
+
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
54694
|
+
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
54695
|
+
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
54696
|
+
"component/multiple-components-per-file": "One component per file. Move subcomponents into their own files so the Context Window stays small and boundary tests are easy.",
|
|
54697
|
+
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
54698
|
+
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
54699
|
+
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
54700
|
+
"layout/math-element-uniformity": "Human files have lopsided interactive counts (1 button + 12 inputs). AI tends to balance them. Build forms with many inputs and few buttons.",
|
|
54701
|
+
"layout/math-grid-uniformity": "Vary grid-cols-N (grid-cols-2, grid-cols-3, grid-cols-4, grid-cols-6) across sections instead of repeating grid-cols-3.",
|
|
54702
|
+
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
54703
|
+
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
54704
|
+
"logic/heaps-deviation": "Inspect for LLM-style vocabulary patterns: this file's vocabulary grows faster (high Heaps \u03BB) or slower (low \u03BB) than typical source code. Verify authorship if unexpected. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54705
|
+
"logic/ks-distribution-shift": "Inspect the shifted features. KS detects both AI anomalies and production-rot anomalies (it is symmetric); combine with Heaps/Zipf for AI-specific signal. (v0.12.0 \u2014 arXiv:2510.15996, Oct 2025.)",
|
|
54706
|
+
"logic/zipf-slope-anomaly": "Inspect for LLM-style frequency distribution: this file's identifier usage is more peaked or flatter than typical human code. (v0.12.0 \u2014 Christ et al. EMNLP Findings 2025.)",
|
|
54707
|
+
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
54708
|
+
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
54709
|
+
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
54710
|
+
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
54711
|
+
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
54712
|
+
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
54713
|
+
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
54714
|
+
"dead/unused-import": "Remove the import or use the symbol somewhere. Unused imports are the most common AI-iteration signature \u2014 the model added it for a feature, then rewrote the function without cleaning up.",
|
|
54715
|
+
"dead/unused-local": "Remove the declaration or use the variable. AI-iteration signature: the model declared a binding for a feature, then rewrote the function without cleaning up.",
|
|
54716
|
+
"dead/unused-parameter": "Remove the parameter (and update every call site) or use it in the function body. AI-iteration signature: the model added a parameter for a feature, then rewrote the function without removing parameters the new code does not need.",
|
|
54717
|
+
"dead/dead-branch": "Replace the literal boolean with a real condition, or remove the dead branch. AI-iteration signature: a feature flag toggled to a constant, or a wrapper from a previous refactor that was never cleaned up.",
|
|
54718
|
+
"dead/unreachable": "Remove this statement \u2014 code after a return/throw/break/continue is unreachable. AI-iteration signature: the model added an early return for a new error path, then forgot the rest of the function body was still sitting below it.",
|
|
54719
|
+
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
54720
|
+
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
54721
|
+
"typo/math-button-label-uniformity": 'Mix button lengths deliberately \u2014 pair a short "Save" with a longer "Mark as complete" \u2014 instead of repeating the same template.',
|
|
54722
|
+
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
54723
|
+
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
54724
|
+
"visual/math-font-entropy": "Use a wider range of text sizes (text-xs, text-sm, text-lg, text-xl, text-2xl, text-3xl) for a more deliberate type scale.",
|
|
54725
|
+
"visual/math-rounded-entropy": "Use a wider range of border-radius values (sm, md, 2xl, 3xl) instead of repeating the same lg/xl/full pattern.",
|
|
54726
|
+
"visual/math-spacing-entropy": "Mix more spacing values from the design scale (e.g. 3, 5, 7, 10, 14, 20, 28) instead of repeating the same 4/8 pattern.",
|
|
54727
|
+
"visual/naturalness-anomaly": "Use domain-specific identifier names so the identifier stream reflects the actual problem domain. Hindle 2012 \xA74.3: LLM-generated code reuses a narrow band of training-data identifiers, dropping distinct-token ratio below 30%.",
|
|
54728
|
+
"visual/math-color-cluster": "Use at least 3 distinct hue families (e.g. blue + amber + green) instead of clustering every color in the violet/fuchsia band.",
|
|
54729
|
+
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
54730
|
+
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
54731
|
+
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
54732
|
+
"test/duplicate-setup": "Extract shared `beforeEach` / `setupServer` blocks into a single helper (e.g. `renderWithProviders`) so each describe block calls it instead of repeating setup.",
|
|
54733
|
+
"test/fake-placeholder": "Use domain-specific fixture values (`alice@acme-corp.com`, `Order#48231`) or a factory like @faker-js/faker. Avoid textbook placeholders (`John Doe`, `test@test.com`, `id: 1`).",
|
|
54734
|
+
"product/terminology-drift": "Keep the leading noun consistent across files: `PostList`, `PostDetail`, `PostCard` are one entity, not three. AI agents pick slightly different words each invocation; product copy drifts.",
|
|
54735
|
+
"product/ux-pattern-fragmentation": "Keep the per-category count tight: modal \u22643, toast \u22642, button \u22644, input \u22643, card \u22643. Pick the canonical one and alias the rest. `slopbrick patterns` reports the per-category count.",
|
|
54736
|
+
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
54737
|
+
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
54738
|
+
"ai/comment-ratio": "AI tools either skip comments (reductive models) or over-comment (expansive models). Match the corpus mean \xB1 2\u03C3 (Rahman et al. 2024, Bisztray et al. 2025).",
|
|
54739
|
+
"ai/whitespace-regularity": "Vary inter-token spacing (single spaces mostly, occasional alignment in tables). Uniform runs are an AI tell (Shi et al. DetectCodeGPT 2024).",
|
|
54740
|
+
"ai/text-like-ratio": "Move natural-language explanations to README files or doc comments. Inline prose in source code is hard to maintain (Yotkova 2026).",
|
|
54741
|
+
"ai/errors-near-eof": "Check whether the file was truncated by a token limit. Unbalanced delimiters near EOF suggest the model ran out of output budget (Yotkova 2026).",
|
|
54742
|
+
"ai/any-density": "Replace `any` with `unknown`, `Record<string, unknown>`, or a domain type. The `: any` annotation propagates type-errors and defeats TS safety (Lee, Hassan, Hindle MSR 2026).",
|
|
54743
|
+
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
54744
|
+
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
54745
|
+
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
54746
|
+
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
54747
|
+
// reclassified post-v7 calibration in v0.14.5d)
|
|
54748
|
+
"ai/tailwind-color-overuse": "If most utility classes are bg-violet-500, text-violet-600, ring-violet-400 \u2014 the project is on the AI-default palette. Audit and replace with the project's design tokens.",
|
|
54749
|
+
"ai/default-react-stack": "Every new file is a Vite + React + Tailwind + Zustand + React Hook Form clone. Verify the project actually needs each piece before adding it.",
|
|
54750
|
+
"ai/library-reinvention": "Re-implementing zustand, react-hook-form, or date-fns inline (custom event emitters, useState reducers, manual date math) is a sign of LLM completion-mode code. Use the libraries the project already depends on.",
|
|
54751
|
+
"ai/state-default-overuse": "Wrapping every component in useState + useEffect for transient UI state is the React tutorial default. Real production code uses refs, uncontrolled inputs, or the project's state lib.",
|
|
54752
|
+
"ai/fetch-default-overuse": "Calling fetch() inline in components instead of going through the project's data-fetching layer (react-query, swr, or your own client) bypasses the cache, error boundary, and abort handling.",
|
|
54753
|
+
"ai/console-debug-storm": "5+ console.log calls in a single file is debug-by-print-statement, the LLM training-data default. Remove before commit; use the project's logger or a real debugger.",
|
|
54754
|
+
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
54755
|
+
"db/sql-concat": 'Never build SQL with template-literal interpolation \u2014 `db.query(`SELECT \u2026 WHERE id = ${id}`)` is a SQL injection vector. Use parameterized queries (`db.query("\u2026 WHERE id = $1", [id])`) or your ORM query builder.',
|
|
54756
|
+
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
54757
|
+
"docs/stale-package-reference": "Update the doc to reference an installed package, or add the package to package.json. Copy-pasted install commands from a previous project are the #1 doc-drift failure mode.",
|
|
54758
|
+
"docs/stale-function-reference": "Rename the doc reference to match a current export, or add a wrapper export. Stale function callouts in tutorials cost readers 10+ minutes of debugging.",
|
|
54759
|
+
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
54760
|
+
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
54761
|
+
"rust/unused-pub-fn": "Remove the function or call it. Rust's compiler doesn't warn on `pub fn` references unless `#![warn(dead_code)]` is set at the crate root. AI agents leave these behind during iterative refactors.",
|
|
54762
|
+
"rust/unwrap-in-production": "Replace `.unwrap()` / `.expect()` with `?` (early-return on Err), `.map_err(...)` for error conversion, or an explicit `match`. They panic in production; reserve them for `#[cfg(test)]` or `#[test]` scopes.",
|
|
54763
|
+
"rust/todo-macro": "Implement the function body or remove the stub. `todo!()` / `unimplemented!()` both expand to `panic!()`, so a hit at runtime crashes the program. Move them into a `#[cfg(test)] mod tests` block to suppress.",
|
|
54764
|
+
"rust/stringly-typed": 'Replace the `String` / `&str` parameter with the typed enum that already exists in the file. Stringly-typed APIs lose type information at the boundary \u2014 typos (`"Click"` vs `"click"`) only fail at runtime.',
|
|
54765
|
+
// v0.19.0 — TypeScript-specific rules
|
|
54766
|
+
"ts/enum-vs-as-const": "Replace `enum Foo { A, B }` with `const Foo = { A: 'A', B: 'B' } as const` (or `const Foo = ['A', 'B'] as const`). Modern TS style guides (Google, TS-eslint) prefer `as const` because enums have surprising runtime semantics.",
|
|
54767
|
+
"ts/import-type-misuse": "Use `import type { X } from '...'` instead of `import { type X } from '...'`. The split form is more common in real codebases and makes the type-only intent unambiguous.",
|
|
54768
|
+
"ts/never-vs-unknown": "The `never` return type means 'this function never returns'. Reserve it for functions that always throw, always loop, or always exit. For 'impossible' branches, use a concrete type (`void`, `Error`, `unknown`) and an exhaustive check.",
|
|
54769
|
+
"ts/excessive-type-assertion": "More than 3 `as` assertions in one function is a strong signal that the type is wrong, not the code. Fix the type definition (or use a type guard) instead of bypassing the type system.",
|
|
54770
|
+
// v0.19.0 — Go-specific rules
|
|
54771
|
+
"go/struct-tag-inconsistency": 'Pick one tag style per struct. If most fields are `json:"foo"`, this field should be too. Real Go code maintains consistency within a struct (or within a package).',
|
|
54772
|
+
"go/nil-slice-vs-empty": "Either declare as `var x = []int{}` or assign with `make([]int, 0)`. The nil/empty inconsistency is an AI signal \u2014 real code picks one form and sticks with it.",
|
|
54773
|
+
// v0.19.0 — Duplication detector
|
|
54774
|
+
"dup/identical-block": "Refactor to a shared helper. This is a Type-1 clone (byte-for-byte identical after normalization). Common in AI-generated code that copy-pastes from training data.",
|
|
54775
|
+
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
54776
|
+
"dup/near-duplicate": "Refactor to a shared helper. Type-2 near-duplicate: same identifiers + structure, different whitespace/comments. AI-iterated code regenerates the same function with cosmetic edits. (v0.23.0 \u2014 MinHash + LSH, k=5, thr 0.7.)",
|
|
54777
|
+
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
54778
|
+
"dup/structural-clone": "Refactor to a shared helper. Type-3 clone: same shape as another file after identifier canonicalization, with renames and/or added/removed statements. AI agents and copy-paste both produce this. Extract the shared part. (v0.24.0.)",
|
|
54779
|
+
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
54780
|
+
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
54781
|
+
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
54782
|
+
"java/sql-string-concat": "Use a PreparedStatement (JDBC), setParameter (jOOQ), or an ORM (Hibernate, MyBatis with #{}). String concat into SQL is the canonical SQL-injection pattern. OWASP A03:2021. (v0.30.0 \u2014 DORMANT, ratio 0.59.)",
|
|
54783
|
+
"java/thread-sleep-in-loop": "Use ScheduledExecutorService for periodic work, or BlockingQueue.take() for event-driven work. Thread.sleep in a loop is the polling anti-pattern \u2014 ties up Tomcat/Jetty/Netty threads. (v0.30.0 \u2014 DORMANT, era-confounded ratio 0.97.)",
|
|
54784
|
+
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
54785
|
+
"java/suspicious-implementation": "Function name claims validate/encrypt/hash/sanitize but body is empty, returns null/true, or returns the input. This is a content mismatch \u2014 the function's claimed behavior doesn't match its actual behavior. OWASP A04:2021.",
|
|
54786
|
+
// v0.35.1 — Raidar-inspired content-based detection
|
|
54787
|
+
"java/lost-stack-trace": 'catch block throws a new exception without the original cause \u2014 stack trace is lost. Pass the original exception as the second arg to the new exception\'s constructor: `throw new XxxException("msg", e)`.',
|
|
54788
|
+
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
54789
|
+
"swift/force-unwrap": "Replace with the safe form: `as?` + guard/if let, `try?`, or `guard let x = optional else { return }`. `!` crashes unconditionally in release. (v0.24.0 \u2014 DORMANT.)",
|
|
54790
|
+
"swift/print-debug": "Replace with `Logger(subsystem:..., category:...).info(...)` (os.log). `print` writes to stdout with no level and no redaction. (v0.24.0 \u2014 DORMANT.)",
|
|
54791
|
+
"swift/fatal-error-thrown": "Replace with `return` of a typed default (e.g. `nil`, `[]()`) or throw a typed `enum MyError: Error`. `fatalError` survives release builds. (v0.24.0 \u2014 DORMANT.)",
|
|
54792
|
+
"swift/implicitly-unwrapped-optional": "Declare as `var name: Type?` and unwrap with `guard let`. IUOs exist for Obj-C bridging / IBOutlets but using them everywhere is an AI signal. (v0.24.0 \u2014 DORMANT.)",
|
|
54793
|
+
"swift/strong-self-capture": "Capture with `[weak self]` (or `[unowned self]` if guaranteed non-nil): `[weak self] in self?.foo = bar`. Strong self capture creates a retain cycle. (v0.24.0 \u2014 DORMANT.)",
|
|
54794
|
+
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
54795
|
+
"cpp/raw-new-delete": "Replace with `auto p = std::make_unique<T>(...)`. Smart pointers delete on scope exit, never leak on early return / exception, never double-free. (v0.24.0 \u2014 DORMANT.)",
|
|
54796
|
+
"cpp/c-style-cast": "Use the named cast: `static_cast<int>(x)`, `reinterpret_cast<...>(p)`, `const_cast<...>(ref)`. C-style casts silently pick static / reinterpret / const \u2014 impossible to grep. (v0.24.0 \u2014 DORMANT.)",
|
|
54797
|
+
"cpp/printf-debug": "Replace with `spdlog::info(...)`, `LOG(INFO) << ...` (glog), or `ABSL_LOG(INFO) << ...`. `printf` / `std::cout` have no levels, no redaction, no sink routing. (v0.24.0 \u2014 DORMANT.)",
|
|
54798
|
+
"cpp/magic-numbers": "Replace with `constexpr int MAX = 1024;` (or `static constexpr`). Named constants live next to their value, can be searched, force the reader to mean what they say. (v0.24.0 \u2014 DORMANT.)"
|
|
54799
|
+
};
|
|
54800
|
+
}
|
|
54801
|
+
});
|
|
54802
|
+
|
|
54803
|
+
// src/snippet/generators.ts
|
|
54804
|
+
function wrapWithMarkers(level, body) {
|
|
54805
|
+
const head = level === 1 ? "<!-- slopbrick-managed-section -->\n" : "";
|
|
54806
|
+
return `${head}${MARKER_BEGIN}
|
|
54807
|
+
${body}
|
|
54808
|
+
${MARKER_END}
|
|
54809
|
+
`;
|
|
54810
|
+
}
|
|
54811
|
+
function generateCursorSnippet(rules) {
|
|
54812
|
+
const lines = [];
|
|
54813
|
+
lines.push("# slopbrick cursor rules");
|
|
54814
|
+
lines.push("");
|
|
54815
|
+
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
54816
|
+
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
54817
|
+
lines.push("");
|
|
54818
|
+
lines.push("## Category-level directives");
|
|
54819
|
+
lines.push("");
|
|
54820
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54821
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54822
|
+
}
|
|
54823
|
+
lines.push("");
|
|
54824
|
+
lines.push("## Per-rule directives");
|
|
54825
|
+
lines.push("");
|
|
54826
|
+
const ai = aiSpecificRules(rules);
|
|
54827
|
+
const byCategory = categorizeRules(ai);
|
|
54828
|
+
for (const [category, list] of byCategory) {
|
|
54829
|
+
lines.push("### " + category);
|
|
54830
|
+
lines.push("");
|
|
54831
|
+
for (const rule of list) {
|
|
54832
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54833
|
+
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
54834
|
+
}
|
|
54835
|
+
lines.push("");
|
|
54836
|
+
}
|
|
54837
|
+
lines.push("## Verification");
|
|
54838
|
+
lines.push("");
|
|
54839
|
+
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
54840
|
+
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
54841
|
+
lines.push("");
|
|
54842
|
+
return lines.join("\n");
|
|
54843
|
+
}
|
|
54844
|
+
function generateAgentsMdSnippet(rules) {
|
|
54845
|
+
return wrapWithMarkers(
|
|
54846
|
+
1,
|
|
54847
|
+
baseContent(
|
|
54848
|
+
rules,
|
|
54849
|
+
"AGENTS.md",
|
|
54850
|
+
"Generated by `slopbrick init --agents-md`. Read these directives when writing or modifying code in this project. AGENTS.md is the de facto standard read by OpenAI Codex, opencode, Pi, Cline, Continue, Gemini, Claude Code (fallback), and others."
|
|
54851
|
+
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n"
|
|
54852
|
+
);
|
|
54853
|
+
}
|
|
54854
|
+
function generateClaudeMdSnippet(rules) {
|
|
54855
|
+
return wrapWithMarkers(
|
|
54856
|
+
1,
|
|
54857
|
+
baseContent(
|
|
54858
|
+
rules,
|
|
54859
|
+
"CLAUDE.md",
|
|
54860
|
+
"Generated by `slopbrick init --claude-md`. Read this file at the start of every Claude Code session in this project. CLAUDE.md takes precedence over AGENTS.md for Claude Code."
|
|
54861
|
+
) + "## Verification\n\nRun `slopbrick scan` after edits. If MCP is configured (`slopbrick mcp`), use `slop_scan_file` proactively to check before reporting code as done.\n"
|
|
54862
|
+
);
|
|
54863
|
+
}
|
|
54864
|
+
function generateAiderSnippet(rules) {
|
|
54865
|
+
return baseContent(
|
|
54866
|
+
rules,
|
|
54867
|
+
"CONVENTIONS.md",
|
|
54868
|
+
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
54869
|
+
);
|
|
54870
|
+
}
|
|
54871
|
+
function generateWindsurfSnippet(rules) {
|
|
54872
|
+
return baseContent(
|
|
54873
|
+
rules,
|
|
54874
|
+
".windsurfrules",
|
|
54875
|
+
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
54876
|
+
);
|
|
54877
|
+
}
|
|
54878
|
+
function generateClineRules(rules) {
|
|
54879
|
+
return baseContent(
|
|
54880
|
+
rules,
|
|
54881
|
+
"AGENTS.md",
|
|
54882
|
+
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
54883
|
+
);
|
|
54884
|
+
}
|
|
54885
|
+
function generateCursorrulesLegacySnippet(rules) {
|
|
54886
|
+
return baseContent(
|
|
54887
|
+
rules,
|
|
54888
|
+
".cursorrules",
|
|
54889
|
+
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
54890
|
+
);
|
|
54891
|
+
}
|
|
54892
|
+
function generateGeminiSnippet(rules) {
|
|
54893
|
+
return baseContent(
|
|
54894
|
+
rules,
|
|
54895
|
+
"GEMINI.md",
|
|
54896
|
+
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
54897
|
+
);
|
|
54898
|
+
}
|
|
54899
|
+
function generateCopilotSnippet(rules) {
|
|
54900
|
+
return baseContent(
|
|
54901
|
+
rules,
|
|
54902
|
+
"copilot-instructions.md",
|
|
54903
|
+
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
54904
|
+
);
|
|
54905
|
+
}
|
|
54906
|
+
var MARKER_BEGIN, MARKER_END;
|
|
54907
|
+
var init_generators = __esm({
|
|
54908
|
+
"src/snippet/generators.ts"() {
|
|
54909
|
+
"use strict";
|
|
54910
|
+
init_data();
|
|
54911
|
+
init_render2();
|
|
54912
|
+
MARKER_BEGIN = "<!-- slopbrick:begin:v3 -->";
|
|
54913
|
+
MARKER_END = "<!-- slopbrick:end:v3 -->";
|
|
54914
|
+
}
|
|
54915
|
+
});
|
|
54916
|
+
|
|
54917
|
+
// src/snippet/render.ts
|
|
54918
|
+
function aiSpecificRules(rules) {
|
|
54919
|
+
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
54920
|
+
}
|
|
54921
|
+
function categorizeRules(rules) {
|
|
54922
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
54923
|
+
for (const r of rules) {
|
|
54924
|
+
const list = byCategory.get(r.category) ?? [];
|
|
54925
|
+
list.push(r);
|
|
54926
|
+
byCategory.set(r.category, list);
|
|
54927
|
+
}
|
|
54928
|
+
for (const list of byCategory.values()) {
|
|
54929
|
+
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
54930
|
+
}
|
|
54931
|
+
return byCategory;
|
|
54932
|
+
}
|
|
54933
|
+
function baseContent(rules, toolLabel, intro) {
|
|
54934
|
+
const lines = [];
|
|
54935
|
+
lines.push("# " + toolLabel);
|
|
54936
|
+
lines.push("");
|
|
54937
|
+
lines.push(intro);
|
|
54938
|
+
lines.push("");
|
|
54939
|
+
const ai = aiSpecificRules(rules);
|
|
54940
|
+
const byCategory = categorizeRules(ai);
|
|
54941
|
+
lines.push("## Category-level directives");
|
|
54942
|
+
lines.push("");
|
|
54943
|
+
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
54944
|
+
lines.push("- **" + category + "**: " + directive);
|
|
54945
|
+
}
|
|
54946
|
+
lines.push("");
|
|
54947
|
+
lines.push("## Per-rule directives");
|
|
54948
|
+
lines.push("");
|
|
54949
|
+
for (const [category, list] of byCategory) {
|
|
54950
|
+
lines.push("### " + category);
|
|
54951
|
+
lines.push("");
|
|
54952
|
+
for (const rule of list) {
|
|
54953
|
+
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
54954
|
+
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
54955
|
+
}
|
|
54956
|
+
lines.push("");
|
|
54957
|
+
}
|
|
54958
|
+
return lines.join("\n");
|
|
54959
|
+
}
|
|
54960
|
+
function safeRewrite(existing, freshBlockBody) {
|
|
54961
|
+
const beginIdx = existing.indexOf(MARKER_BEGIN);
|
|
54962
|
+
const endIdx = existing.indexOf(MARKER_END);
|
|
54963
|
+
if (beginIdx === -1 && endIdx === -1) {
|
|
54964
|
+
return { content: existing, rewritten: false, failClosed: true, mismatched: false };
|
|
54965
|
+
}
|
|
54966
|
+
if (beginIdx === -1 || endIdx === -1) {
|
|
54967
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
54968
|
+
}
|
|
54969
|
+
if (endIdx <= beginIdx) {
|
|
54970
|
+
return { content: existing, rewritten: false, failClosed: false, mismatched: true };
|
|
54971
|
+
}
|
|
54972
|
+
const before = existing.slice(0, beginIdx + MARKER_BEGIN.length);
|
|
54973
|
+
const after = existing.slice(endIdx);
|
|
54974
|
+
const next = `${before}
|
|
54975
|
+
${freshBlockBody}
|
|
54976
|
+
${after}`;
|
|
54977
|
+
return { content: next, rewritten: true, failClosed: false, mismatched: false };
|
|
54978
|
+
}
|
|
54979
|
+
var init_render2 = __esm({
|
|
54980
|
+
"src/snippet/render.ts"() {
|
|
54981
|
+
"use strict";
|
|
54982
|
+
init_data();
|
|
54983
|
+
init_generators();
|
|
54984
|
+
}
|
|
54985
|
+
});
|
|
54986
|
+
|
|
54987
|
+
// src/snippet/refresh.ts
|
|
54988
|
+
var refresh_exports = {};
|
|
54989
|
+
__export(refresh_exports, {
|
|
54990
|
+
SNIPPET_TARGETS: () => SNIPPET_TARGETS,
|
|
54991
|
+
refreshSnippets: () => refreshSnippets
|
|
54992
|
+
});
|
|
54993
|
+
function refreshSnippets(cwd, rules, targets = [...SNIPPET_TARGETS]) {
|
|
54994
|
+
const result = { rewritten: [], failClosed: [], mismatched: [], absent: [] };
|
|
54995
|
+
for (const filename of targets) {
|
|
54996
|
+
const path = (0, import_node_path24.join)(cwd, filename);
|
|
54997
|
+
if (!(0, import_node_fs22.existsSync)(path)) {
|
|
54998
|
+
result.absent.push(filename);
|
|
54999
|
+
continue;
|
|
55000
|
+
}
|
|
55001
|
+
const existing = (0, import_node_fs22.readFileSync)(path, "utf-8");
|
|
55002
|
+
const agentName = filename.replace(/\.md$/i, "").toLowerCase();
|
|
55003
|
+
const generator = agentName === "claude" ? generateAgentsMdSnippet : generateAgentsMdSnippet;
|
|
55004
|
+
const newBlock = generator([...rules]).trim();
|
|
55005
|
+
const outcome = safeRewrite(existing, newBlock);
|
|
55006
|
+
if (outcome.rewritten) {
|
|
55007
|
+
const tmp = `${path}.tmp`;
|
|
55008
|
+
(0, import_node_fs22.writeFileSync)(tmp, outcome.content, "utf-8");
|
|
55009
|
+
const fs = require("fs");
|
|
55010
|
+
fs.renameSync(tmp, path);
|
|
55011
|
+
result.rewritten.push(filename);
|
|
55012
|
+
} else if (outcome.failClosed) {
|
|
55013
|
+
result.failClosed.push(filename);
|
|
55014
|
+
} else if (outcome.mismatched) {
|
|
55015
|
+
result.mismatched.push(filename);
|
|
55016
|
+
}
|
|
55017
|
+
}
|
|
55018
|
+
return result;
|
|
55019
|
+
}
|
|
55020
|
+
var import_node_fs22, import_node_path24, SNIPPET_TARGETS;
|
|
55021
|
+
var init_refresh = __esm({
|
|
55022
|
+
"src/snippet/refresh.ts"() {
|
|
55023
|
+
"use strict";
|
|
55024
|
+
import_node_fs22 = require("fs");
|
|
55025
|
+
import_node_path24 = require("path");
|
|
55026
|
+
init_render2();
|
|
55027
|
+
init_generators();
|
|
55028
|
+
SNIPPET_TARGETS = [
|
|
55029
|
+
"AGENTS.md",
|
|
55030
|
+
"CLAUDE.md"
|
|
55031
|
+
];
|
|
55032
|
+
}
|
|
55033
|
+
});
|
|
55034
|
+
|
|
54585
55035
|
// src/cli/report/persistRun.ts
|
|
54586
55036
|
async function persistRun(input) {
|
|
54587
55037
|
const {
|
|
@@ -54644,7 +55094,7 @@ async function persistRun(input) {
|
|
|
54644
55094
|
const recentTopHashes = telemetryPayloads.map(
|
|
54645
55095
|
(payload) => [...payload.files].sort((a, b) => b.score - a.score).slice(0, 10).map((file) => file.hash)
|
|
54646
55096
|
);
|
|
54647
|
-
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0,
|
|
55097
|
+
const currentTopFiles = [...report.components].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, 10).map((c) => ({ filePath: c.filePath, hash: hashFile((0, import_node_path25.relative)(cwd, c.filePath)) }));
|
|
54648
55098
|
const unmatchedStringLiterals = results.flatMap((r) => r.unmatchedStringLiterals ?? []);
|
|
54649
55099
|
const flywheelOutput = computeFlywheelOutput(
|
|
54650
55100
|
runs,
|
|
@@ -54664,20 +55114,20 @@ async function persistRun(input) {
|
|
|
54664
55114
|
report.research = state.research;
|
|
54665
55115
|
}
|
|
54666
55116
|
if (flywheelOutput.suggestions.length > 0) {
|
|
54667
|
-
const suggestionsDir = (0,
|
|
54668
|
-
if (!(0,
|
|
54669
|
-
(0,
|
|
54670
|
-
(0,
|
|
55117
|
+
const suggestionsDir = (0, import_node_path25.join)(cwd, ".slopbrick", "flywheel");
|
|
55118
|
+
if (!(0, import_node_fs23.existsSync)(suggestionsDir)) (0, import_node_fs23.mkdirSync)(suggestionsDir, { recursive: true });
|
|
55119
|
+
(0, import_node_fs23.writeFileSync)(
|
|
55120
|
+
(0, import_node_path25.join)(suggestionsDir, "rule-suggestions.json"),
|
|
54671
55121
|
JSON.stringify(flywheelOutput.suggestions, null, 2)
|
|
54672
55122
|
);
|
|
54673
55123
|
}
|
|
54674
55124
|
report.issues.push(...flywheelOutput.hotspotIssues);
|
|
54675
55125
|
}
|
|
54676
|
-
|
|
55126
|
+
let patternInventory;
|
|
54677
55127
|
if (config.projectMemory !== false) {
|
|
54678
55128
|
try {
|
|
54679
55129
|
const durationMs = Date.now() - startTime;
|
|
54680
|
-
|
|
55130
|
+
patternInventory = await buildPatternInventory(cwd, config);
|
|
54681
55131
|
const inventory = buildInventoryFromScan(
|
|
54682
55132
|
{ cwd, results },
|
|
54683
55133
|
patternInventory,
|
|
@@ -54711,13 +55161,29 @@ async function persistRun(input) {
|
|
|
54711
55161
|
}
|
|
54712
55162
|
}
|
|
54713
55163
|
}
|
|
55164
|
+
recordTelemetry(cwd, report, results, config, patternInventory);
|
|
55165
|
+
if (input.autoRefreshSnippets) {
|
|
55166
|
+
try {
|
|
55167
|
+
const { refreshSnippets: refreshSnippets2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
|
|
55168
|
+
const outcome = refreshSnippets2(cwd, registry.getRules());
|
|
55169
|
+
if (outcome.rewritten.length > 0 && !options.quiet && !machineReadableStdout) {
|
|
55170
|
+
logger.info(
|
|
55171
|
+
`Snippet refresh: rewrote ${outcome.rewritten.length} file(s) (${outcome.rewritten.join(", ")}).`
|
|
55172
|
+
);
|
|
55173
|
+
}
|
|
55174
|
+
} catch (err) {
|
|
55175
|
+
if (options.quiet !== true && !machineReadableStdout) {
|
|
55176
|
+
logger.warn(`snippet refresh failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
55177
|
+
}
|
|
55178
|
+
}
|
|
55179
|
+
}
|
|
54714
55180
|
}
|
|
54715
|
-
var
|
|
55181
|
+
var import_node_fs23, import_node_path25;
|
|
54716
55182
|
var init_persistRun = __esm({
|
|
54717
55183
|
"src/cli/report/persistRun.ts"() {
|
|
54718
55184
|
"use strict";
|
|
54719
|
-
|
|
54720
|
-
|
|
55185
|
+
import_node_fs23 = require("fs");
|
|
55186
|
+
import_node_path25 = require("path");
|
|
54721
55187
|
init_logger();
|
|
54722
55188
|
init_cache_incremental();
|
|
54723
55189
|
init_flywheel();
|
|
@@ -54856,7 +55322,7 @@ var init_finalizeReport = __esm({
|
|
|
54856
55322
|
function buildBaselineCache(report, configHash, gitHead, cwd) {
|
|
54857
55323
|
const scores = {};
|
|
54858
55324
|
for (const component of report.components) {
|
|
54859
|
-
scores[(0,
|
|
55325
|
+
scores[(0, import_node_path26.relative)(cwd, component.filePath)] = {
|
|
54860
55326
|
baselineScore: component.componentScore,
|
|
54861
55327
|
componentCount: component.componentCount
|
|
54862
55328
|
};
|
|
@@ -54871,11 +55337,11 @@ function buildBaselineCache(report, configHash, gitHead, cwd) {
|
|
|
54871
55337
|
scores
|
|
54872
55338
|
};
|
|
54873
55339
|
}
|
|
54874
|
-
var
|
|
55340
|
+
var import_node_path26;
|
|
54875
55341
|
var init_baseline_cache = __esm({
|
|
54876
55342
|
"src/cli/report/baseline-cache.ts"() {
|
|
54877
55343
|
"use strict";
|
|
54878
|
-
|
|
55344
|
+
import_node_path26 = require("path");
|
|
54879
55345
|
init_types();
|
|
54880
55346
|
}
|
|
54881
55347
|
});
|
|
@@ -54939,24 +55405,24 @@ function buildArtifactUri(filePath, cwd) {
|
|
|
54939
55405
|
return ".";
|
|
54940
55406
|
}
|
|
54941
55407
|
if (cwd) {
|
|
54942
|
-
const absoluteFilePath = (0,
|
|
54943
|
-
const rel = (0,
|
|
55408
|
+
const absoluteFilePath = (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
|
|
55409
|
+
const rel = (0, import_node_path27.relative)(cwd, absoluteFilePath);
|
|
54944
55410
|
if (rel.startsWith("..")) {
|
|
54945
|
-
return (0,
|
|
55411
|
+
return (0, import_node_path27.basename)(filePath);
|
|
54946
55412
|
}
|
|
54947
55413
|
return rel;
|
|
54948
55414
|
}
|
|
54949
|
-
if ((0,
|
|
54950
|
-
return (0,
|
|
55415
|
+
if ((0, import_node_path27.isAbsolute)(filePath)) {
|
|
55416
|
+
return (0, import_node_path27.basename)(filePath);
|
|
54951
55417
|
}
|
|
54952
55418
|
return filePath;
|
|
54953
55419
|
}
|
|
54954
55420
|
function resolveSourcePath(filePath, cwd) {
|
|
54955
55421
|
if (!filePath) return null;
|
|
54956
55422
|
if (cwd) {
|
|
54957
|
-
return (0,
|
|
55423
|
+
return (0, import_node_path27.isAbsolute)(filePath) ? filePath : (0, import_node_path27.resolve)(cwd, filePath);
|
|
54958
55424
|
}
|
|
54959
|
-
return (0,
|
|
55425
|
+
return (0, import_node_path27.isAbsolute)(filePath) ? filePath : null;
|
|
54960
55426
|
}
|
|
54961
55427
|
function severityToSarifLevel(severity) {
|
|
54962
55428
|
switch (severity) {
|
|
@@ -55039,7 +55505,7 @@ function buildResultFromIssue(issue, cwd, fileContentCache) {
|
|
|
55039
55505
|
let content = fileContentCache.get(sourcePath);
|
|
55040
55506
|
if (content === void 0) {
|
|
55041
55507
|
try {
|
|
55042
|
-
content = (0,
|
|
55508
|
+
content = (0, import_node_fs24.readFileSync)(sourcePath, "utf-8");
|
|
55043
55509
|
fileContentCache.set(sourcePath, content);
|
|
55044
55510
|
} catch {
|
|
55045
55511
|
fileContentCache.set(sourcePath, "");
|
|
@@ -55092,6 +55558,7 @@ function formatSarif(report, options) {
|
|
|
55092
55558
|
const results = report.issues.map(
|
|
55093
55559
|
(issue) => buildResultFromIssue(issue, options?.cwd, fileContentCache)
|
|
55094
55560
|
);
|
|
55561
|
+
const driverProperties = report.compositeScore ? { compositeScore: report.compositeScore } : void 0;
|
|
55095
55562
|
const log = {
|
|
55096
55563
|
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
55097
55564
|
version: "2.1.0",
|
|
@@ -55102,7 +55569,8 @@ function formatSarif(report, options) {
|
|
|
55102
55569
|
name: "slopbrick",
|
|
55103
55570
|
version: report.version,
|
|
55104
55571
|
informationUri: REPO_INFORMATION_URI,
|
|
55105
|
-
rules
|
|
55572
|
+
rules,
|
|
55573
|
+
...driverProperties ? { properties: driverProperties } : {}
|
|
55106
55574
|
}
|
|
55107
55575
|
},
|
|
55108
55576
|
results
|
|
@@ -55111,13 +55579,13 @@ function formatSarif(report, options) {
|
|
|
55111
55579
|
};
|
|
55112
55580
|
return JSON.stringify(log, null, 2);
|
|
55113
55581
|
}
|
|
55114
|
-
var import_node_crypto9,
|
|
55582
|
+
var import_node_crypto9, import_node_fs24, import_node_path27, REPO_INFORMATION_URI, RULES_BASE_URL;
|
|
55115
55583
|
var init_sarif = __esm({
|
|
55116
55584
|
"src/report/sarif.ts"() {
|
|
55117
55585
|
"use strict";
|
|
55118
55586
|
import_node_crypto9 = require("crypto");
|
|
55119
|
-
|
|
55120
|
-
|
|
55587
|
+
import_node_fs24 = require("fs");
|
|
55588
|
+
import_node_path27 = require("path");
|
|
55121
55589
|
REPO_INFORMATION_URI = "https://github.com/usebrick/platform";
|
|
55122
55590
|
RULES_BASE_URL = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
55123
55591
|
}
|
|
@@ -56302,18 +56770,18 @@ function applyReplaceFixes(content, fixes) {
|
|
|
56302
56770
|
return { content, applied, skipped, reasons };
|
|
56303
56771
|
}
|
|
56304
56772
|
function applyLayoutTokenFix(filePath, fixes) {
|
|
56305
|
-
let content = (0,
|
|
56773
|
+
let content = (0, import_node_fs25.readFileSync)(filePath, "utf-8");
|
|
56306
56774
|
const { content: patched, applied, skipped, reasons } = applyReplaceFixes(content, fixes);
|
|
56307
56775
|
if (patched !== content) {
|
|
56308
|
-
(0,
|
|
56776
|
+
(0, import_node_fs25.writeFileSync)(filePath, patched);
|
|
56309
56777
|
}
|
|
56310
56778
|
return { applied, skipped, reasons };
|
|
56311
56779
|
}
|
|
56312
|
-
var
|
|
56780
|
+
var import_node_fs25;
|
|
56313
56781
|
var init_layout_token = __esm({
|
|
56314
56782
|
"src/fix/layout-token.ts"() {
|
|
56315
56783
|
"use strict";
|
|
56316
|
-
|
|
56784
|
+
import_node_fs25 = require("fs");
|
|
56317
56785
|
}
|
|
56318
56786
|
});
|
|
56319
56787
|
|
|
@@ -56393,8 +56861,8 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56393
56861
|
const parts = [];
|
|
56394
56862
|
let hasHunk = false;
|
|
56395
56863
|
for (const [filePath, fixes] of byFile) {
|
|
56396
|
-
if (!(0,
|
|
56397
|
-
const original = (0,
|
|
56864
|
+
if (!(0, import_node_fs26.existsSync)(filePath)) continue;
|
|
56865
|
+
const original = (0, import_node_fs26.readFileSync)(filePath, "utf-8");
|
|
56398
56866
|
const patched = applyFixesToString(original, fixes);
|
|
56399
56867
|
if (patched === original) continue;
|
|
56400
56868
|
if (!hasHunk) {
|
|
@@ -56403,7 +56871,7 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56403
56871
|
parts.push("");
|
|
56404
56872
|
hasHunk = true;
|
|
56405
56873
|
}
|
|
56406
|
-
const rel = (0,
|
|
56874
|
+
const rel = (0, import_node_path28.relative)(cwd, filePath);
|
|
56407
56875
|
parts.push(`--- a/${rel}`);
|
|
56408
56876
|
parts.push(`+++ b/${rel}`);
|
|
56409
56877
|
parts.push(...formatHunk(original, patched));
|
|
@@ -56414,12 +56882,12 @@ function formatUnifiedDiff(report, cwd) {
|
|
|
56414
56882
|
}
|
|
56415
56883
|
return parts.join("\n");
|
|
56416
56884
|
}
|
|
56417
|
-
var
|
|
56885
|
+
var import_node_fs26, import_node_path28;
|
|
56418
56886
|
var init_unified_diff = __esm({
|
|
56419
56887
|
"src/report/unified-diff.ts"() {
|
|
56420
56888
|
"use strict";
|
|
56421
|
-
|
|
56422
|
-
|
|
56889
|
+
import_node_fs26 = require("fs");
|
|
56890
|
+
import_node_path28 = require("path");
|
|
56423
56891
|
init_layout_token();
|
|
56424
56892
|
}
|
|
56425
56893
|
});
|
|
@@ -56445,7 +56913,7 @@ async function buildHeatmap(report, cwd, helpers = { getFileEditCount, getFileLa
|
|
|
56445
56913
|
const reference = new Date(report.generatedAt);
|
|
56446
56914
|
const entries = await Promise.all(
|
|
56447
56915
|
report.components.map(async (component) => {
|
|
56448
|
-
const relPath = (0,
|
|
56916
|
+
const relPath = (0, import_node_path29.relative)(cwd, component.filePath) || component.filePath;
|
|
56449
56917
|
const [edits, lastModified] = await Promise.all([
|
|
56450
56918
|
helpers.getFileEditCount(cwd, relPath, RECENCY_DAYS),
|
|
56451
56919
|
helpers.getFileLastModifiedDate(cwd, relPath)
|
|
@@ -56490,11 +56958,11 @@ function formatHeatmap(entries, options = {}) {
|
|
|
56490
56958
|
);
|
|
56491
56959
|
return [header, ...rows].join("\n");
|
|
56492
56960
|
}
|
|
56493
|
-
var
|
|
56961
|
+
var import_node_path29, RECENCY_DAYS, MAX_EDITS;
|
|
56494
56962
|
var init_heatmap = __esm({
|
|
56495
56963
|
"src/report/heatmap.ts"() {
|
|
56496
56964
|
"use strict";
|
|
56497
|
-
|
|
56965
|
+
import_node_path29 = require("path");
|
|
56498
56966
|
init_git();
|
|
56499
56967
|
RECENCY_DAYS = 30;
|
|
56500
56968
|
MAX_EDITS = 10;
|
|
@@ -56535,7 +57003,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56535
57003
|
if (options.html) {
|
|
56536
57004
|
const html = formatHtml(report);
|
|
56537
57005
|
if (typeof options.html === "string") {
|
|
56538
|
-
(0,
|
|
57006
|
+
(0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.html), html);
|
|
56539
57007
|
if (!options.quiet) {
|
|
56540
57008
|
logger.info(`Wrote HTML report to ${options.html}`);
|
|
56541
57009
|
}
|
|
@@ -56547,7 +57015,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56547
57015
|
if (options.json) {
|
|
56548
57016
|
const json = formatJson(report);
|
|
56549
57017
|
if (typeof options.json === "string") {
|
|
56550
|
-
(0,
|
|
57018
|
+
(0, import_node_fs27.writeFileSync)((0, import_node_path30.resolve)(options.json), json);
|
|
56551
57019
|
if (!options.quiet) {
|
|
56552
57020
|
logger.info(`Wrote JSON report to ${options.json}`);
|
|
56553
57021
|
}
|
|
@@ -56561,7 +57029,7 @@ function renderOutput(report, options, cwd) {
|
|
|
56561
57029
|
return;
|
|
56562
57030
|
}
|
|
56563
57031
|
if (options.format === "sarif") {
|
|
56564
|
-
const cwdSarif = (0,
|
|
57032
|
+
const cwdSarif = (0, import_node_path30.resolve)(options.workspace ?? process.cwd());
|
|
56565
57033
|
logger.info(formatSarif(report, { cwd: cwdSarif }));
|
|
56566
57034
|
return;
|
|
56567
57035
|
}
|
|
@@ -56582,12 +57050,12 @@ async function outputScanResults(report, options, cwd) {
|
|
|
56582
57050
|
}
|
|
56583
57051
|
renderOutput(report, options, cwd);
|
|
56584
57052
|
}
|
|
56585
|
-
var
|
|
57053
|
+
var import_node_path30, import_node_fs27, VALID_FORMATS;
|
|
56586
57054
|
var init_renderOutput = __esm({
|
|
56587
57055
|
"src/cli/report/renderOutput.ts"() {
|
|
56588
57056
|
"use strict";
|
|
56589
|
-
|
|
56590
|
-
|
|
57057
|
+
import_node_path30 = require("path");
|
|
57058
|
+
import_node_fs27 = require("fs");
|
|
56591
57059
|
init_logger();
|
|
56592
57060
|
init_json();
|
|
56593
57061
|
init_pretty();
|
|
@@ -56612,7 +57080,7 @@ async function watchProject(options, cwd, paths) {
|
|
|
56612
57080
|
let currentBaseline;
|
|
56613
57081
|
function getBaselineMtime() {
|
|
56614
57082
|
try {
|
|
56615
|
-
return (0,
|
|
57083
|
+
return (0, import_node_fs28.statSync)(baselinePath(cwd)).mtimeMs;
|
|
56616
57084
|
} catch {
|
|
56617
57085
|
return void 0;
|
|
56618
57086
|
}
|
|
@@ -56720,12 +57188,12 @@ async function watchProject(options, cwd, paths) {
|
|
|
56720
57188
|
await doScan(false);
|
|
56721
57189
|
if (closed) return;
|
|
56722
57190
|
const currentFiles = new Set(issueGroupsMap.keys());
|
|
56723
|
-
watcher = (0,
|
|
57191
|
+
watcher = (0, import_node_fs28.watch)(
|
|
56724
57192
|
cwd,
|
|
56725
57193
|
{ recursive: true },
|
|
56726
57194
|
(_eventType, filename) => {
|
|
56727
57195
|
if (closed || !filename) return;
|
|
56728
|
-
const changedPath = (0,
|
|
57196
|
+
const changedPath = (0, import_node_path31.resolve)(cwd, filename.toString());
|
|
56729
57197
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
56730
57198
|
debounceTimer = setTimeout(() => {
|
|
56731
57199
|
debounceTimer = void 0;
|
|
@@ -56760,12 +57228,12 @@ async function watchProject(options, cwd, paths) {
|
|
|
56760
57228
|
}
|
|
56761
57229
|
);
|
|
56762
57230
|
}
|
|
56763
|
-
var
|
|
57231
|
+
var import_node_fs28, import_node_path31;
|
|
56764
57232
|
var init_watch = __esm({
|
|
56765
57233
|
"src/cli/watch.ts"() {
|
|
56766
57234
|
"use strict";
|
|
56767
|
-
|
|
56768
|
-
|
|
57235
|
+
import_node_fs28 = require("fs");
|
|
57236
|
+
import_node_path31 = require("path");
|
|
56769
57237
|
init_metrics();
|
|
56770
57238
|
init_worker();
|
|
56771
57239
|
init_threshold();
|
|
@@ -56785,11 +57253,11 @@ async function runScan(options, explicitPaths) {
|
|
|
56785
57253
|
setLoggerQuiet(!!options.quiet);
|
|
56786
57254
|
const startTime = Date.now();
|
|
56787
57255
|
const scanId = (0, import_node_crypto10.randomUUID)();
|
|
56788
|
-
const cwd = (0,
|
|
56789
|
-
if (!(0,
|
|
57256
|
+
const cwd = (0, import_node_path32.resolve)(options.workspace ?? process.cwd());
|
|
57257
|
+
if (!(0, import_node_fs29.existsSync)(cwd)) {
|
|
56790
57258
|
throw new Error(`Workspace not found: ${cwd}`);
|
|
56791
57259
|
}
|
|
56792
|
-
const cwdStat = (0,
|
|
57260
|
+
const cwdStat = (0, import_node_fs29.statSync)(cwd);
|
|
56793
57261
|
if (!cwdStat.isDirectory()) {
|
|
56794
57262
|
throw new Error(`Workspace is not a directory: ${cwd}`);
|
|
56795
57263
|
}
|
|
@@ -56816,7 +57284,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56816
57284
|
process.env.SLOP_AUDIT_CACHE = "1";
|
|
56817
57285
|
}
|
|
56818
57286
|
if (options.tokens) {
|
|
56819
|
-
const tokenResult = readDtcgTokensFile((0,
|
|
57287
|
+
const tokenResult = readDtcgTokensFile((0, import_node_path32.resolve)(cwd, options.tokens));
|
|
56820
57288
|
if (tokenResult.ok) {
|
|
56821
57289
|
const extra = tokensToAllowlist(tokenResult.tree);
|
|
56822
57290
|
config.arbitraryValueAllowlist = [...config.arbitraryValueAllowlist, ...extra];
|
|
@@ -56828,14 +57296,14 @@ async function runScan(options, explicitPaths) {
|
|
|
56828
57296
|
if (explicitPaths && explicitPaths.length > 0) {
|
|
56829
57297
|
const { globby: globby4 } = await import("globby");
|
|
56830
57298
|
const { minimatch: minimatch4 } = await import("minimatch");
|
|
56831
|
-
const resolved = explicitPaths.map((p) => (0,
|
|
57299
|
+
const resolved = explicitPaths.map((p) => (0, import_node_path32.resolve)(cwd, p));
|
|
56832
57300
|
const expanded = [];
|
|
56833
57301
|
for (const p of resolved) {
|
|
56834
|
-
if ((0,
|
|
57302
|
+
if ((0, import_node_fs29.existsSync)(p) && (0, import_node_fs29.statSync)(p).isDirectory()) {
|
|
56835
57303
|
const found = await globby4(`${p}/**/*`, { absolute: true, onlyFiles: true });
|
|
56836
57304
|
for (const f of found) {
|
|
56837
|
-
if (!ALL_SOURCE_EXTENSIONS.has((0,
|
|
56838
|
-
const rel = (0,
|
|
57305
|
+
if (!ALL_SOURCE_EXTENSIONS.has((0, import_node_path32.extname)(f).toLowerCase())) continue;
|
|
57306
|
+
const rel = (0, import_node_path32.relative)(cwd, f).split(import_node_path32.sep).join("/");
|
|
56839
57307
|
if (config.include.length > 0 && !config.include.some((pattern) => minimatch4(rel, pattern))) {
|
|
56840
57308
|
continue;
|
|
56841
57309
|
}
|
|
@@ -56985,7 +57453,7 @@ async function runScan(options, explicitPaths) {
|
|
|
56985
57453
|
const scannedPaths = new Set(results.map((result) => result.filePath));
|
|
56986
57454
|
for (const [filePath, cached] of Object.entries(baseline.scores)) {
|
|
56987
57455
|
if (scannedPaths.has(filePath)) continue;
|
|
56988
|
-
if (!(0,
|
|
57456
|
+
if (!(0, import_node_fs29.existsSync)(filePath)) continue;
|
|
56989
57457
|
scores.push({
|
|
56990
57458
|
filePath,
|
|
56991
57459
|
rawScore: 0,
|
|
@@ -57024,7 +57492,12 @@ async function runScan(options, explicitPaths) {
|
|
|
57024
57492
|
const { report, noIncreaseFailure } = await finalizeReport({
|
|
57025
57493
|
cwd,
|
|
57026
57494
|
config,
|
|
57027
|
-
options
|
|
57495
|
+
options: {
|
|
57496
|
+
...options,
|
|
57497
|
+
// v0.42.0 (§3a.4): forward the opt-in flag through to
|
|
57498
|
+
// `persistRun`, which calls `refreshSnippets` post-scan.
|
|
57499
|
+
autoRefreshSnippets: Boolean(options.autoRefreshSnippets) || Boolean(config.autoRefreshSnippets)
|
|
57500
|
+
},
|
|
57028
57501
|
results,
|
|
57029
57502
|
aggregated,
|
|
57030
57503
|
allIssues,
|
|
@@ -57063,12 +57536,12 @@ async function scanProject(options) {
|
|
|
57063
57536
|
const { report } = await runScan({ ...options, workspace: options.cwd });
|
|
57064
57537
|
return report;
|
|
57065
57538
|
}
|
|
57066
|
-
var
|
|
57539
|
+
var import_node_fs29, import_node_path32, import_node_crypto10;
|
|
57067
57540
|
var init_scan2 = __esm({
|
|
57068
57541
|
"src/cli/scan.ts"() {
|
|
57069
57542
|
"use strict";
|
|
57070
|
-
|
|
57071
|
-
|
|
57543
|
+
import_node_fs29 = require("fs");
|
|
57544
|
+
import_node_path32 = require("path");
|
|
57072
57545
|
import_node_crypto10 = require("crypto");
|
|
57073
57546
|
init_render();
|
|
57074
57547
|
init_threshold();
|
|
@@ -57096,396 +57569,44 @@ var init_scan2 = __esm({
|
|
|
57096
57569
|
}
|
|
57097
57570
|
});
|
|
57098
57571
|
|
|
57099
|
-
// src/
|
|
57100
|
-
|
|
57101
|
-
|
|
57102
|
-
|
|
57103
|
-
|
|
57104
|
-
content: [{ type: "text", text: cached }]
|
|
57105
|
-
};
|
|
57106
|
-
}
|
|
57107
|
-
const { handleToolCall: handleToolCall2 } = await Promise.resolve().then(() => (init_tools(), tools_exports));
|
|
57108
|
-
const result = await handleToolCall2("slop_suggest", args, ctx);
|
|
57109
|
-
if (result.isError) return result;
|
|
57110
|
-
try {
|
|
57111
|
-
const parsed = JSON.parse(result.content[0].text);
|
|
57112
|
-
if (parsed !== null && typeof parsed === "object") {
|
|
57113
|
-
parsed.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
57114
|
-
return {
|
|
57115
|
-
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }]
|
|
57116
|
-
};
|
|
57117
|
-
}
|
|
57118
|
-
} catch {
|
|
57119
|
-
}
|
|
57120
|
-
return result;
|
|
57121
|
-
}
|
|
57122
|
-
var STRUCTURE_NOT_FOUND_HINT;
|
|
57123
|
-
var init_slop_suggest_structure = __esm({
|
|
57124
|
-
"src/mcp/slop-suggest-structure.ts"() {
|
|
57125
|
-
"use strict";
|
|
57126
|
-
init_structure_md();
|
|
57127
|
-
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.";
|
|
57128
|
-
}
|
|
57572
|
+
// src/rules/composite-seed.ts
|
|
57573
|
+
var composite_seed_exports = {};
|
|
57574
|
+
__export(composite_seed_exports, {
|
|
57575
|
+
HAND_CURATED_SEED: () => HAND_CURATED_SEED,
|
|
57576
|
+
maybeAppendSeed: () => maybeAppendSeed
|
|
57129
57577
|
});
|
|
57130
|
-
|
|
57131
|
-
|
|
57132
|
-
|
|
57133
|
-
__export(tools_exports, {
|
|
57134
|
-
TOOL_DEFINITIONS: () => TOOL_DEFINITIONS,
|
|
57135
|
-
canonicalToolNames: () => canonicalToolNames,
|
|
57136
|
-
getDeprecation: () => getDeprecation,
|
|
57137
|
-
handleToolCall: () => handleToolCall
|
|
57138
|
-
});
|
|
57139
|
-
function toolError(message) {
|
|
57140
|
-
return {
|
|
57141
|
-
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
57142
|
-
isError: true
|
|
57143
|
-
};
|
|
57144
|
-
}
|
|
57145
|
-
async function runScanFile(args, ctx) {
|
|
57146
|
-
const path = args.path;
|
|
57147
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57148
|
-
const result = await scanFile(path, ctx.config);
|
|
57149
|
-
const simplified = {
|
|
57150
|
-
filePath: result.filePath,
|
|
57151
|
-
componentCount: result.componentCount,
|
|
57152
|
-
parseError: result.parseError,
|
|
57153
|
-
issues: result.issues.map((i) => ({
|
|
57154
|
-
ruleId: i.ruleId,
|
|
57155
|
-
category: i.category,
|
|
57156
|
-
severity: i.severity,
|
|
57157
|
-
line: i.line,
|
|
57158
|
-
column: i.column,
|
|
57159
|
-
message: i.message,
|
|
57160
|
-
advice: i.advice
|
|
57161
|
-
}))
|
|
57162
|
-
};
|
|
57163
|
-
return {
|
|
57164
|
-
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
57165
|
-
};
|
|
57166
|
-
}
|
|
57167
|
-
function explainRule2(args, ctx) {
|
|
57168
|
-
const ruleId = args.ruleId;
|
|
57169
|
-
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
57170
|
-
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
57171
|
-
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
57172
|
-
const explanation = {
|
|
57173
|
-
ruleId: rule.id,
|
|
57174
|
-
category: rule.category,
|
|
57175
|
-
severity: rule.severity,
|
|
57176
|
-
aiSpecific: rule.aiSpecific,
|
|
57177
|
-
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 + ".",
|
|
57178
|
-
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
57179
|
-
};
|
|
57180
|
-
return {
|
|
57181
|
-
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
57182
|
-
};
|
|
57183
|
-
}
|
|
57184
|
-
function listRules(args, ctx) {
|
|
57185
|
-
const category = args.category;
|
|
57186
|
-
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
57187
|
-
const rules = filtered.map((r) => ({
|
|
57188
|
-
id: r.id,
|
|
57189
|
-
category: r.category,
|
|
57190
|
-
severity: r.severity,
|
|
57191
|
-
aiSpecific: r.aiSpecific
|
|
57192
|
-
}));
|
|
57193
|
-
return {
|
|
57194
|
-
content: [
|
|
57195
|
-
{
|
|
57196
|
-
type: "text",
|
|
57197
|
-
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
57198
|
-
}
|
|
57199
|
-
]
|
|
57200
|
-
};
|
|
57201
|
-
}
|
|
57202
|
-
async function runSuggest(args, ctx) {
|
|
57203
|
-
const maxFilesRaw = args.maxFiles;
|
|
57204
|
-
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
57205
|
-
try {
|
|
57206
|
-
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
57207
|
-
const doNotCreate = [
|
|
57208
|
-
...ctx.config.constitution?.forbidden ?? []
|
|
57209
|
-
];
|
|
57210
|
-
const declared = /* @__PURE__ */ new Set();
|
|
57211
|
-
for (const list of [
|
|
57212
|
-
ctx.config.constitution?.stateManagement ?? [],
|
|
57213
|
-
ctx.config.constitution?.dataFetching ?? [],
|
|
57214
|
-
ctx.config.constitution?.uiLibrary ?? [],
|
|
57215
|
-
ctx.config.constitution?.forms ?? [],
|
|
57216
|
-
ctx.config.constitution?.styling ?? [],
|
|
57217
|
-
ctx.config.constitution?.routing ?? []
|
|
57218
|
-
]) {
|
|
57219
|
-
for (const lib of list) declared.add(lib);
|
|
57220
|
-
}
|
|
57221
|
-
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
57222
|
-
return {
|
|
57223
|
-
content: [
|
|
57224
|
-
{
|
|
57225
|
-
type: "text",
|
|
57226
|
-
text: JSON.stringify(
|
|
57227
|
-
{
|
|
57228
|
-
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.",
|
|
57229
|
-
doNotCreate: doNotCreateCapped,
|
|
57230
|
-
declaredStack: Array.from(declared),
|
|
57231
|
-
existingPatterns: inventory
|
|
57232
|
-
},
|
|
57233
|
-
null,
|
|
57234
|
-
2
|
|
57235
|
-
)
|
|
57236
|
-
}
|
|
57237
|
-
]
|
|
57238
|
-
};
|
|
57239
|
-
} catch (err) {
|
|
57240
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57241
|
-
}
|
|
57242
|
-
}
|
|
57243
|
-
function runCheckConstitution(args, ctx) {
|
|
57244
|
-
const path = args.path;
|
|
57245
|
-
if (!path) return toolError("Missing required argument: path");
|
|
57246
|
-
const absPath = (0, import_node_path40.resolve)(ctx.cwd, path);
|
|
57247
|
-
let source;
|
|
57248
|
-
try {
|
|
57249
|
-
source = (0, import_node_fs32.readFileSync)(absPath, "utf-8");
|
|
57250
|
-
} catch (err) {
|
|
57251
|
-
return toolError(
|
|
57252
|
-
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
57253
|
-
);
|
|
57254
|
-
}
|
|
57255
|
-
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
57256
|
-
return {
|
|
57257
|
-
content: [
|
|
57258
|
-
{
|
|
57259
|
-
type: "text",
|
|
57260
|
-
text: JSON.stringify(
|
|
57261
|
-
{
|
|
57262
|
-
file: absPath,
|
|
57263
|
-
importCount: result.imports.length,
|
|
57264
|
-
violationCount: result.violations.length,
|
|
57265
|
-
imports: result.imports,
|
|
57266
|
-
violations: result.violations,
|
|
57267
|
-
// Field name kept stable for backward compatibility with
|
|
57268
|
-
// older consumers; the value reflects whether the merged
|
|
57269
|
-
// `config.constitution` was declared, detected, or absent.
|
|
57270
|
-
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
57271
|
-
},
|
|
57272
|
-
null,
|
|
57273
|
-
2
|
|
57274
|
-
)
|
|
57275
|
-
}
|
|
57276
|
-
]
|
|
57277
|
-
};
|
|
57278
|
-
}
|
|
57279
|
-
async function runFindSimilar(args, ctx) {
|
|
57280
|
-
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
57281
|
-
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
57282
|
-
const props = Array.isArray(args.props) ? args.props : [];
|
|
57283
|
-
const limitRaw = args.limit;
|
|
57284
|
-
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
57285
|
-
try {
|
|
57286
|
-
const matches = await findSimilarFunctions2(
|
|
57287
|
-
{
|
|
57288
|
-
name: typeof args.name === "string" ? args.name : void 0,
|
|
57289
|
-
hooks,
|
|
57290
|
-
props,
|
|
57291
|
-
limit,
|
|
57292
|
-
workspaceDir: ctx.cwd
|
|
57293
|
-
},
|
|
57294
|
-
{ cwd: ctx.cwd }
|
|
57295
|
-
);
|
|
57296
|
-
return {
|
|
57297
|
-
content: [
|
|
57298
|
-
{
|
|
57299
|
-
type: "text",
|
|
57300
|
-
text: JSON.stringify(
|
|
57301
|
-
{
|
|
57302
|
-
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.",
|
|
57303
|
-
count: matches.length,
|
|
57304
|
-
matches: matches.map((m) => ({
|
|
57305
|
-
name: m.signature.name,
|
|
57306
|
-
file: m.signature.fileRel,
|
|
57307
|
-
line: m.signature.line,
|
|
57308
|
-
similarity: Number(m.similarity.toFixed(3)),
|
|
57309
|
-
fingerprint: m.fingerprint,
|
|
57310
|
-
hooks: m.signature.hooks,
|
|
57311
|
-
props: m.signature.props,
|
|
57312
|
-
params: m.signature.params
|
|
57313
|
-
}))
|
|
57314
|
-
},
|
|
57315
|
-
null,
|
|
57316
|
-
2
|
|
57317
|
-
)
|
|
57318
|
-
}
|
|
57319
|
-
]
|
|
57320
|
-
};
|
|
57321
|
-
} catch (err) {
|
|
57322
|
-
return toolError(err instanceof Error ? err.message : String(err));
|
|
57323
|
-
}
|
|
57324
|
-
}
|
|
57325
|
-
async function handleToolCall(toolName, args, ctx) {
|
|
57326
|
-
const deprecation = getDeprecation(toolName);
|
|
57327
|
-
const deprecationNotice = deprecation ? {
|
|
57328
|
-
tool: toolName,
|
|
57329
|
-
replacedBy: deprecation.replacedBy,
|
|
57330
|
-
removedIn: deprecation.removedIn ?? "next major",
|
|
57331
|
-
reason: deprecation.reason
|
|
57332
|
-
} : void 0;
|
|
57333
|
-
switch (toolName) {
|
|
57334
|
-
case "slop_scan_file":
|
|
57335
|
-
return runScanFile(args, ctx);
|
|
57336
|
-
case "slop_explain_rule":
|
|
57337
|
-
return explainRule2(args, ctx);
|
|
57338
|
-
case "slop_list_rules":
|
|
57339
|
-
return listRules(args, ctx);
|
|
57340
|
-
case "slop_suggest":
|
|
57341
|
-
return runSuggest(args, ctx);
|
|
57342
|
-
case "slop_suggest_with_structure":
|
|
57343
|
-
return runSuggestWithStructure(args, ctx);
|
|
57344
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57345
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57346
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57347
|
-
// Their runner functions (runGovernance, runArchitectureScore,
|
|
57348
|
-
// runBusinessLogicScore) are kept in the file for now
|
|
57349
|
-
// (marked @deprecated) to keep the diff small; they can be
|
|
57350
|
-
// deleted in a follow-up. New clients will never see these
|
|
57351
|
-
// tools listed in the MCP tools/list response.
|
|
57352
|
-
case "slop_check_constitution":
|
|
57353
|
-
return runCheckConstitution(args, ctx);
|
|
57354
|
-
case "slop_find_similar":
|
|
57355
|
-
return runFindSimilar(args, ctx);
|
|
57356
|
-
default:
|
|
57357
|
-
return toolError("Unknown tool: " + toolName);
|
|
57578
|
+
function maybeAppendSeed(entries) {
|
|
57579
|
+
if (entries.length === 0) {
|
|
57580
|
+
return [...HAND_CURATED_SEED];
|
|
57358
57581
|
}
|
|
57582
|
+
return entries;
|
|
57359
57583
|
}
|
|
57360
|
-
|
|
57361
|
-
|
|
57362
|
-
|
|
57363
|
-
function getDeprecation(toolName) {
|
|
57364
|
-
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
57365
|
-
}
|
|
57366
|
-
var import_node_fs32, import_node_path40, TOOL_DEFINITIONS;
|
|
57367
|
-
var init_tools = __esm({
|
|
57368
|
-
"src/mcp/tools.ts"() {
|
|
57584
|
+
var HAND_CURATED_SEED;
|
|
57585
|
+
var init_composite_seed = __esm({
|
|
57586
|
+
"src/rules/composite-seed.ts"() {
|
|
57369
57587
|
"use strict";
|
|
57370
|
-
|
|
57371
|
-
import_node_path40 = require("path");
|
|
57372
|
-
init_worker();
|
|
57373
|
-
init_patterns();
|
|
57374
|
-
init_architecture_score();
|
|
57375
|
-
init_business_logic();
|
|
57376
|
-
init_slop_suggest_structure();
|
|
57377
|
-
TOOL_DEFINITIONS = [
|
|
57378
|
-
{
|
|
57379
|
-
name: "slop_scan_file",
|
|
57380
|
-
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.",
|
|
57381
|
-
inputSchema: {
|
|
57382
|
-
type: "object",
|
|
57383
|
-
properties: {
|
|
57384
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
57385
|
-
framework: {
|
|
57386
|
-
type: "string",
|
|
57387
|
-
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
57388
|
-
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
57389
|
-
}
|
|
57390
|
-
},
|
|
57391
|
-
required: ["path"]
|
|
57392
|
-
}
|
|
57393
|
-
},
|
|
57394
|
-
{
|
|
57395
|
-
name: "slop_explain_rule",
|
|
57396
|
-
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.",
|
|
57397
|
-
inputSchema: {
|
|
57398
|
-
type: "object",
|
|
57399
|
-
properties: {
|
|
57400
|
-
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
57401
|
-
},
|
|
57402
|
-
required: ["ruleId"]
|
|
57403
|
-
}
|
|
57404
|
-
},
|
|
57588
|
+
HAND_CURATED_SEED = [
|
|
57405
57589
|
{
|
|
57406
|
-
|
|
57407
|
-
|
|
57408
|
-
|
|
57409
|
-
|
|
57410
|
-
|
|
57411
|
-
|
|
57412
|
-
|
|
57413
|
-
|
|
57414
|
-
|
|
57415
|
-
|
|
57416
|
-
|
|
57417
|
-
|
|
57418
|
-
|
|
57419
|
-
|
|
57420
|
-
|
|
57421
|
-
|
|
57422
|
-
|
|
57423
|
-
|
|
57424
|
-
|
|
57425
|
-
|
|
57426
|
-
}
|
|
57427
|
-
},
|
|
57428
|
-
{
|
|
57429
|
-
name: "slop_suggest_with_structure",
|
|
57430
|
-
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.",
|
|
57431
|
-
inputSchema: {
|
|
57432
|
-
type: "object",
|
|
57433
|
-
properties: {
|
|
57434
|
-
maxFiles: {
|
|
57435
|
-
type: "number",
|
|
57436
|
-
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
57437
|
-
}
|
|
57438
|
-
}
|
|
57439
|
-
}
|
|
57440
|
-
},
|
|
57441
|
-
{
|
|
57442
|
-
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
57443
|
-
// slop_architecture_score, slop_business_logic_score) that
|
|
57444
|
-
// were marked for removal in v0.13.0 but never removed.
|
|
57445
|
-
// They were strict subsets of slop_suggest; users should
|
|
57446
|
-
// call slop_suggest and read repositoryHealth /
|
|
57447
|
-
// architectureConsistency / businessLogicCoherence.
|
|
57448
|
-
name: "slop_check_constitution",
|
|
57449
|
-
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.",
|
|
57450
|
-
inputSchema: {
|
|
57451
|
-
type: "object",
|
|
57452
|
-
properties: {
|
|
57453
|
-
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
57454
|
-
},
|
|
57455
|
-
required: ["path"]
|
|
57456
|
-
}
|
|
57457
|
-
},
|
|
57458
|
-
{
|
|
57459
|
-
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
57460
|
-
// Reference) primitive for slop_suggest. Given a function signature
|
|
57461
|
-
// (name + hooks + props), find the most similar existing
|
|
57462
|
-
// implementations across the codebase. Uses AST fingerprints
|
|
57463
|
-
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
57464
|
-
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
57465
|
-
name: "slop_find_similar",
|
|
57466
|
-
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].",
|
|
57467
|
-
inputSchema: {
|
|
57468
|
-
type: "object",
|
|
57469
|
-
properties: {
|
|
57470
|
-
name: {
|
|
57471
|
-
type: "string",
|
|
57472
|
-
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
57473
|
-
},
|
|
57474
|
-
hooks: {
|
|
57475
|
-
type: "array",
|
|
57476
|
-
items: { type: "string" },
|
|
57477
|
-
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
57478
|
-
},
|
|
57479
|
-
props: {
|
|
57480
|
-
type: "array",
|
|
57481
|
-
items: { type: "string" },
|
|
57482
|
-
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
57483
|
-
},
|
|
57484
|
-
limit: {
|
|
57485
|
-
type: "number",
|
|
57486
|
-
description: "Top-k results to return. Default 10. Capped at 50."
|
|
57487
|
-
}
|
|
57488
|
-
}
|
|
57590
|
+
id: "composite/ai-fingerprint-cluster",
|
|
57591
|
+
ruleIds: ["ai/compression-profile", "ai/comment-ratio", "ai/segment-surprisal-cv"],
|
|
57592
|
+
minMatch: 2,
|
|
57593
|
+
severity: "medium",
|
|
57594
|
+
defaultOff: true,
|
|
57595
|
+
description: "Composite rule: at least 2 of {ai/compression-profile+ai/comment-ratio+...} fire on the same file. Hand-curated seed from the v0.42.0 brief; superseded by `slopbrick composite discover` once empirical clusters dominate the ledger.",
|
|
57596
|
+
calibration: {
|
|
57597
|
+
recall: 0,
|
|
57598
|
+
FP: 0,
|
|
57599
|
+
precision: 0,
|
|
57600
|
+
F1: 0,
|
|
57601
|
+
nFiles: 0
|
|
57602
|
+
},
|
|
57603
|
+
provenance: {
|
|
57604
|
+
seed: "hand-curated-by-brief",
|
|
57605
|
+
discoveredAt: "2026-07-05T00:00:00.000Z",
|
|
57606
|
+
nFiles: 0,
|
|
57607
|
+
members: 3,
|
|
57608
|
+
npmi: 0,
|
|
57609
|
+
fisherP: 1
|
|
57489
57610
|
}
|
|
57490
57611
|
}
|
|
57491
57612
|
];
|
|
@@ -57578,8 +57699,8 @@ init_config2();
|
|
|
57578
57699
|
init_dist2();
|
|
57579
57700
|
|
|
57580
57701
|
// src/cli/program.ts
|
|
57581
|
-
var
|
|
57582
|
-
var
|
|
57702
|
+
var import_node_path75 = require("path");
|
|
57703
|
+
var import_commander3 = require("commander");
|
|
57583
57704
|
|
|
57584
57705
|
// src/cli/options.ts
|
|
57585
57706
|
var import_commander = require("commander");
|
|
@@ -57621,8 +57742,8 @@ init_scan2();
|
|
|
57621
57742
|
init_scan2();
|
|
57622
57743
|
|
|
57623
57744
|
// src/cli/init.ts
|
|
57624
|
-
var
|
|
57625
|
-
var
|
|
57745
|
+
var import_node_fs31 = require("fs");
|
|
57746
|
+
var import_node_path34 = require("path");
|
|
57626
57747
|
var import_node_readline = require("readline");
|
|
57627
57748
|
init_config2();
|
|
57628
57749
|
init_discover();
|
|
@@ -57631,8 +57752,8 @@ init_cache();
|
|
|
57631
57752
|
init_logger();
|
|
57632
57753
|
|
|
57633
57754
|
// src/rules/registry-loader.ts
|
|
57634
|
-
var
|
|
57635
|
-
var
|
|
57755
|
+
var import_node_fs30 = require("fs");
|
|
57756
|
+
var import_node_path33 = require("path");
|
|
57636
57757
|
|
|
57637
57758
|
// src/data/shadcn-registry.json
|
|
57638
57759
|
var shadcn_registry_default = {
|
|
@@ -57766,12 +57887,12 @@ var shadcn_registry_default = {
|
|
|
57766
57887
|
var REGISTRY_URL = "https://ui.shadcn.com/registry.json";
|
|
57767
57888
|
var BUNDLED_REGISTRY_VERSION = shadcn_registry_default.version;
|
|
57768
57889
|
function cachePath2(cwd) {
|
|
57769
|
-
return (0,
|
|
57890
|
+
return (0, import_node_path33.join)(cwd, ".slopbrick", "cache", "registry-snapshot.json");
|
|
57770
57891
|
}
|
|
57771
57892
|
function ensureCacheDir(cwd) {
|
|
57772
|
-
const dir = (0,
|
|
57773
|
-
if (!(0,
|
|
57774
|
-
(0,
|
|
57893
|
+
const dir = (0, import_node_path33.dirname)(cachePath2(cwd));
|
|
57894
|
+
if (!(0, import_node_fs30.existsSync)(dir)) {
|
|
57895
|
+
(0, import_node_fs30.mkdirSync)(dir, { recursive: true });
|
|
57775
57896
|
}
|
|
57776
57897
|
}
|
|
57777
57898
|
function isValidSnapshot(value) {
|
|
@@ -57783,9 +57904,9 @@ function isValidSnapshot(value) {
|
|
|
57783
57904
|
}
|
|
57784
57905
|
function isRegistryFresh(cwd) {
|
|
57785
57906
|
const cached = cachePath2(cwd);
|
|
57786
|
-
if (!(0,
|
|
57907
|
+
if (!(0, import_node_fs30.existsSync)(cached)) return false;
|
|
57787
57908
|
try {
|
|
57788
|
-
const parsed = JSON.parse((0,
|
|
57909
|
+
const parsed = JSON.parse((0, import_node_fs30.readFileSync)(cached, "utf8"));
|
|
57789
57910
|
if (!isValidSnapshot(parsed)) return false;
|
|
57790
57911
|
return parsed.version === BUNDLED_REGISTRY_VERSION;
|
|
57791
57912
|
} catch {
|
|
@@ -57796,7 +57917,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57796
57917
|
try {
|
|
57797
57918
|
const cacheFile = cachePath2(cwd);
|
|
57798
57919
|
ensureCacheDir(cwd);
|
|
57799
|
-
(0,
|
|
57920
|
+
(0, import_node_fs30.writeFileSync)(cacheFile, JSON.stringify(shadcn_registry_default, null, 2), "utf-8");
|
|
57800
57921
|
} catch {
|
|
57801
57922
|
}
|
|
57802
57923
|
return {
|
|
@@ -57808,7 +57929,7 @@ async function refreshRegistrySnapshot(cwd, _url = REGISTRY_URL, _timeoutMs = 5e
|
|
|
57808
57929
|
}
|
|
57809
57930
|
function copyBundledSnapshotToCache(cwd) {
|
|
57810
57931
|
ensureCacheDir(cwd);
|
|
57811
|
-
(0,
|
|
57932
|
+
(0, import_node_fs30.writeFileSync)(cachePath2(cwd), JSON.stringify(shadcn_registry_default, null, 2));
|
|
57812
57933
|
}
|
|
57813
57934
|
|
|
57814
57935
|
// src/cli/init.ts
|
|
@@ -57837,17 +57958,17 @@ var UI_LIBRARY_OPTIONS = ["shadcn/ui", "mui", "chakra", "radix", "tamagui", "nat
|
|
|
57837
57958
|
var STRICTNESS_OPTIONS = ["strict", "balanced", "permissive"];
|
|
57838
57959
|
var STRUCTURE_OPTIONS = ["feature-based", "layer-based", "flat", "monorepo", "other"];
|
|
57839
57960
|
function promptText(rl, question, detected) {
|
|
57840
|
-
return new Promise((
|
|
57961
|
+
return new Promise((resolve43) => {
|
|
57841
57962
|
const lines = [
|
|
57842
57963
|
`? ${question} (detected: ${detected || "none"}) \u2014 npm package name, or Enter to skip:`
|
|
57843
57964
|
];
|
|
57844
57965
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57845
57966
|
const trimmed = answer.trim();
|
|
57846
57967
|
if (trimmed === "") {
|
|
57847
|
-
|
|
57968
|
+
resolve43(void 0);
|
|
57848
57969
|
return;
|
|
57849
57970
|
}
|
|
57850
|
-
|
|
57971
|
+
resolve43(trimmed);
|
|
57851
57972
|
});
|
|
57852
57973
|
});
|
|
57853
57974
|
}
|
|
@@ -57855,7 +57976,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57855
57976
|
const defaultIndex = options.indexOf(defaultValue);
|
|
57856
57977
|
const safeDefaultIndex = defaultIndex >= 0 ? defaultIndex : 0;
|
|
57857
57978
|
const safeDefaultValue = options[safeDefaultIndex];
|
|
57858
|
-
return new Promise((
|
|
57979
|
+
return new Promise((resolve43) => {
|
|
57859
57980
|
function ask() {
|
|
57860
57981
|
const lines = [
|
|
57861
57982
|
`? ${question} (detected: ${safeDefaultValue}):`,
|
|
@@ -57865,7 +57986,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57865
57986
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57866
57987
|
const trimmed = answer.trim();
|
|
57867
57988
|
if (trimmed === "") {
|
|
57868
|
-
|
|
57989
|
+
resolve43(safeDefaultValue);
|
|
57869
57990
|
return;
|
|
57870
57991
|
}
|
|
57871
57992
|
const num = parseInt(trimmed, 10);
|
|
@@ -57874,7 +57995,7 @@ function promptSingleSelect(rl, question, options, defaultValue) {
|
|
|
57874
57995
|
ask();
|
|
57875
57996
|
return;
|
|
57876
57997
|
}
|
|
57877
|
-
|
|
57998
|
+
resolve43(options[num - 1]);
|
|
57878
57999
|
});
|
|
57879
58000
|
}
|
|
57880
58001
|
ask();
|
|
@@ -57884,7 +58005,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57884
58005
|
const defaultIndices = defaultValue.map((v) => options.indexOf(v)).filter((i) => i >= 0);
|
|
57885
58006
|
const defaultDisplay = defaultValue.length > 0 ? defaultValue.join(", ") : "none";
|
|
57886
58007
|
const defaultNumbers = defaultIndices.length > 0 ? defaultIndices.map((i) => i + 2).join(",") : "1";
|
|
57887
|
-
return new Promise((
|
|
58008
|
+
return new Promise((resolve43) => {
|
|
57888
58009
|
function ask() {
|
|
57889
58010
|
const lines = [
|
|
57890
58011
|
`? ${question} (detected: ${defaultDisplay}):`,
|
|
@@ -57895,7 +58016,7 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57895
58016
|
rl.question(lines.join("\n") + "\n", (answer) => {
|
|
57896
58017
|
const trimmed = answer.trim();
|
|
57897
58018
|
if (trimmed === "") {
|
|
57898
|
-
|
|
58019
|
+
resolve43(defaultValue.length > 0 ? defaultValue : []);
|
|
57899
58020
|
return;
|
|
57900
58021
|
}
|
|
57901
58022
|
const numbers = trimmed.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n));
|
|
@@ -57905,11 +58026,11 @@ function promptMultiSelect(rl, question, options, defaultValue) {
|
|
|
57905
58026
|
return;
|
|
57906
58027
|
}
|
|
57907
58028
|
if (numbers.includes(1)) {
|
|
57908
|
-
|
|
58029
|
+
resolve43([]);
|
|
57909
58030
|
return;
|
|
57910
58031
|
}
|
|
57911
58032
|
const selected = [...new Set(numbers.map((n) => options[n - 2]).filter((v) => v !== void 0))];
|
|
57912
|
-
|
|
58033
|
+
resolve43(selected);
|
|
57913
58034
|
});
|
|
57914
58035
|
}
|
|
57915
58036
|
ask();
|
|
@@ -57997,11 +58118,11 @@ async function runDoctor(cwd) {
|
|
|
57997
58118
|
}
|
|
57998
58119
|
try {
|
|
57999
58120
|
const { parseFile: tryParse } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
58000
|
-
const testFile = (0,
|
|
58001
|
-
(0,
|
|
58002
|
-
(0,
|
|
58121
|
+
const testFile = (0, import_node_path34.join)(cwd, ".slopbrick", ".doctor-test.ts");
|
|
58122
|
+
(0, import_node_fs31.mkdirSync)((0, import_node_path34.dirname)(testFile), { recursive: true });
|
|
58123
|
+
(0, import_node_fs31.writeFileSync)(testFile, "export const x = 1;\n");
|
|
58003
58124
|
await tryParse(testFile);
|
|
58004
|
-
(0,
|
|
58125
|
+
(0, import_node_fs31.rmSync)(testFile, { force: true });
|
|
58005
58126
|
ok("Parser is working.");
|
|
58006
58127
|
} catch (err) {
|
|
58007
58128
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -58090,7 +58211,7 @@ async function runDoctor(cwd) {
|
|
|
58090
58211
|
}
|
|
58091
58212
|
|
|
58092
58213
|
// src/cli/commands/badge.ts
|
|
58093
|
-
var
|
|
58214
|
+
var import_node_path35 = require("path");
|
|
58094
58215
|
init_render();
|
|
58095
58216
|
init_logger();
|
|
58096
58217
|
init_scan2();
|
|
@@ -58099,7 +58220,7 @@ function registerBadge(program) {
|
|
|
58099
58220
|
"print a shields.io slop-index badge. Reads .slopbrick/health.json if present (no re-scan); falls back to a fresh scan."
|
|
58100
58221
|
).action(async (_cmdOptions, command) => {
|
|
58101
58222
|
const options = command.optsWithGlobals();
|
|
58102
|
-
const cwd = (0,
|
|
58223
|
+
const cwd = (0, import_node_path35.resolve)(options.workspace ?? process.cwd());
|
|
58103
58224
|
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
58104
58225
|
const health = loadHealth2(cwd);
|
|
58105
58226
|
if (health) {
|
|
@@ -58116,7 +58237,7 @@ function registerBadge(program) {
|
|
|
58116
58237
|
}
|
|
58117
58238
|
|
|
58118
58239
|
// src/cli/commands/suggest.ts
|
|
58119
|
-
var
|
|
58240
|
+
var import_node_path36 = require("path");
|
|
58120
58241
|
init_advice();
|
|
58121
58242
|
init_unified_diff();
|
|
58122
58243
|
init_logger();
|
|
@@ -58125,7 +58246,7 @@ function registerSuggest(program) {
|
|
|
58125
58246
|
program.command("suggest").description("print remediation advice").action(async (_cmdOptions, command) => {
|
|
58126
58247
|
const options = command.optsWithGlobals();
|
|
58127
58248
|
const { report } = await runScan(options);
|
|
58128
|
-
const cwd = (0,
|
|
58249
|
+
const cwd = (0, import_node_path36.resolve)(options.workspace ?? process.cwd());
|
|
58129
58250
|
logger.info(formatAdvice(report));
|
|
58130
58251
|
const diff = formatUnifiedDiff(report, cwd);
|
|
58131
58252
|
if (diff) logger.info(diff);
|
|
@@ -58133,6 +58254,470 @@ function registerSuggest(program) {
|
|
|
58133
58254
|
});
|
|
58134
58255
|
}
|
|
58135
58256
|
|
|
58257
|
+
// src/cli/commands/composite.ts
|
|
58258
|
+
var import_node_path38 = require("path");
|
|
58259
|
+
init_logger();
|
|
58260
|
+
init_telemetry();
|
|
58261
|
+
|
|
58262
|
+
// src/engine/cluster.ts
|
|
58263
|
+
var import_node_crypto11 = require("crypto");
|
|
58264
|
+
var DEFAULT_CLUSTER_PARAMS = {
|
|
58265
|
+
minSupport: 0.05,
|
|
58266
|
+
// 5% of files
|
|
58267
|
+
minNPMI: 0.3,
|
|
58268
|
+
// Cohen's-Landis-and-Koch "fair" agreement
|
|
58269
|
+
fisherAlpha: 0.01,
|
|
58270
|
+
// Bonferroni-style floor for many-pair tests
|
|
58271
|
+
minClusterSize: 2
|
|
58272
|
+
};
|
|
58273
|
+
function adaptiveMinSupport(fileCount) {
|
|
58274
|
+
if (fileCount <= 0) return DEFAULT_CLUSTER_PARAMS.minSupport;
|
|
58275
|
+
return Math.max(DEFAULT_CLUSTER_PARAMS.minSupport, 5 / fileCount);
|
|
58276
|
+
}
|
|
58277
|
+
function computeRuleSupport(fireMatrix) {
|
|
58278
|
+
const support = /* @__PURE__ */ new Map();
|
|
58279
|
+
for (const ruleIds of fireMatrix.values()) {
|
|
58280
|
+
for (const ruleId of ruleIds) {
|
|
58281
|
+
support.set(ruleId, (support.get(ruleId) ?? 0) + 1);
|
|
58282
|
+
}
|
|
58283
|
+
}
|
|
58284
|
+
return support;
|
|
58285
|
+
}
|
|
58286
|
+
function prefilterBySupport(support, totalFiles, minSupport) {
|
|
58287
|
+
const kept = /* @__PURE__ */ new Set();
|
|
58288
|
+
const minCount = Math.max(1, Math.ceil(minSupport * totalFiles));
|
|
58289
|
+
for (const [ruleId, count] of support) {
|
|
58290
|
+
if (count >= minCount) kept.add(ruleId);
|
|
58291
|
+
}
|
|
58292
|
+
return kept;
|
|
58293
|
+
}
|
|
58294
|
+
function buildContingency(rA, rB, totalFiles) {
|
|
58295
|
+
let aAndB = 0;
|
|
58296
|
+
let aOnly = 0;
|
|
58297
|
+
let bOnly = 0;
|
|
58298
|
+
for (const file of rA) {
|
|
58299
|
+
if (rB.has(file)) aAndB++;
|
|
58300
|
+
else aOnly++;
|
|
58301
|
+
}
|
|
58302
|
+
for (const file of rB) {
|
|
58303
|
+
if (!rA.has(file)) bOnly++;
|
|
58304
|
+
}
|
|
58305
|
+
return {
|
|
58306
|
+
aAndB,
|
|
58307
|
+
aOnly,
|
|
58308
|
+
bOnly,
|
|
58309
|
+
neither: Math.max(0, totalFiles - aAndB - aOnly - bOnly)
|
|
58310
|
+
};
|
|
58311
|
+
}
|
|
58312
|
+
function fisherExactTwoSided(c) {
|
|
58313
|
+
const N = c.aAndB + c.aOnly + c.bOnly + c.neither;
|
|
58314
|
+
const K1 = c.aAndB + c.aOnly;
|
|
58315
|
+
const K2 = c.aAndB + c.bOnly;
|
|
58316
|
+
const n = K1;
|
|
58317
|
+
const kObs = c.aAndB;
|
|
58318
|
+
const logPmf = (k) => {
|
|
58319
|
+
if (k < Math.max(0, K1 + K2 - N) || k > Math.min(K1, K2)) return -Infinity;
|
|
58320
|
+
return lnChoose(K2, k) + lnChoose(N - K2, K1 - k) - lnChoose(N, K1);
|
|
58321
|
+
};
|
|
58322
|
+
const pObs = Math.exp(logPmf(kObs));
|
|
58323
|
+
const kMin = Math.max(0, K1 + K2 - N);
|
|
58324
|
+
const kMax = Math.min(K1, K2);
|
|
58325
|
+
let p = 0;
|
|
58326
|
+
for (let k = kMin; k <= kMax; k++) {
|
|
58327
|
+
const pk = Math.exp(logPmf(k));
|
|
58328
|
+
if (pk <= pObs + 1e-12) {
|
|
58329
|
+
p += pk;
|
|
58330
|
+
}
|
|
58331
|
+
}
|
|
58332
|
+
return Math.min(1, p);
|
|
58333
|
+
}
|
|
58334
|
+
function lnChoose(n, k) {
|
|
58335
|
+
if (k < 0 || k > n) return -Infinity;
|
|
58336
|
+
if (k === 0 || k === n) return 0;
|
|
58337
|
+
k = Math.min(k, n - k);
|
|
58338
|
+
let s = 0;
|
|
58339
|
+
for (let i = 1; i <= k; i++) {
|
|
58340
|
+
s += Math.log((n - k + i) / i);
|
|
58341
|
+
}
|
|
58342
|
+
return s;
|
|
58343
|
+
}
|
|
58344
|
+
function npmi(c, total) {
|
|
58345
|
+
if (total === 0 || c.aAndB === 0) return -1;
|
|
58346
|
+
const pXY = c.aAndB / total;
|
|
58347
|
+
const pX = (c.aAndB + c.aOnly) / total;
|
|
58348
|
+
const pY = (c.aAndB + c.bOnly) / total;
|
|
58349
|
+
if (pX === 0 || pY === 0 || pXY === 0) return -1;
|
|
58350
|
+
const pmi = Math.log2(pXY / (pX * pY));
|
|
58351
|
+
const norm = -Math.log2(pXY);
|
|
58352
|
+
return norm === 0 ? 1 : pmi / norm;
|
|
58353
|
+
}
|
|
58354
|
+
function computeEdges(fireMatrix, rules, total, params) {
|
|
58355
|
+
const ruleToFiles = /* @__PURE__ */ new Map();
|
|
58356
|
+
for (const ruleId of rules) ruleToFiles.set(ruleId, /* @__PURE__ */ new Set());
|
|
58357
|
+
for (const [file, fired] of fireMatrix) {
|
|
58358
|
+
for (const ruleId of fired) {
|
|
58359
|
+
if (rules.has(ruleId)) ruleToFiles.get(ruleId).add(file);
|
|
58360
|
+
}
|
|
58361
|
+
}
|
|
58362
|
+
const ruleList = Array.from(rules).sort();
|
|
58363
|
+
const edges = [];
|
|
58364
|
+
for (let i = 0; i < ruleList.length; i++) {
|
|
58365
|
+
for (let j = i + 1; j < ruleList.length; j++) {
|
|
58366
|
+
const a = ruleList[i];
|
|
58367
|
+
const b = ruleList[j];
|
|
58368
|
+
const rA = ruleToFiles.get(a);
|
|
58369
|
+
const rB = ruleToFiles.get(b);
|
|
58370
|
+
const c = buildContingency(rA, rB, total);
|
|
58371
|
+
const n = npmi(c, total);
|
|
58372
|
+
if (n < params.minNPMI) continue;
|
|
58373
|
+
const p = fisherExactTwoSided(c);
|
|
58374
|
+
if (p > params.fisherAlpha) continue;
|
|
58375
|
+
edges.push({ ruleIds: [a, b], weight: n, fisherP: p });
|
|
58376
|
+
}
|
|
58377
|
+
}
|
|
58378
|
+
return edges;
|
|
58379
|
+
}
|
|
58380
|
+
function singleLinkageCluster(edges, minClusterSize) {
|
|
58381
|
+
const parent = /* @__PURE__ */ new Map();
|
|
58382
|
+
const find = (x) => {
|
|
58383
|
+
if (!parent.has(x)) {
|
|
58384
|
+
parent.set(x, x);
|
|
58385
|
+
return x;
|
|
58386
|
+
}
|
|
58387
|
+
if (parent.get(x) === x) return x;
|
|
58388
|
+
const root = find(parent.get(x));
|
|
58389
|
+
parent.set(x, root);
|
|
58390
|
+
return root;
|
|
58391
|
+
};
|
|
58392
|
+
const union = (a, b) => {
|
|
58393
|
+
const ra = find(a);
|
|
58394
|
+
const rb = find(b);
|
|
58395
|
+
if (ra !== rb) parent.set(ra, rb);
|
|
58396
|
+
};
|
|
58397
|
+
for (const e of edges) {
|
|
58398
|
+
union(e.ruleIds[0], e.ruleIds[1]);
|
|
58399
|
+
}
|
|
58400
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58401
|
+
for (const k of parent.keys()) {
|
|
58402
|
+
const root = find(k);
|
|
58403
|
+
if (!groups.has(root)) groups.set(root, []);
|
|
58404
|
+
groups.get(root).push(k);
|
|
58405
|
+
}
|
|
58406
|
+
const clusters = [];
|
|
58407
|
+
for (const members of groups.values()) {
|
|
58408
|
+
if (members.length < minClusterSize) continue;
|
|
58409
|
+
members.sort();
|
|
58410
|
+
clusters.push(members);
|
|
58411
|
+
}
|
|
58412
|
+
clusters.sort((a, b) => b.length - a.length || a[0].localeCompare(b[0]));
|
|
58413
|
+
return clusters;
|
|
58414
|
+
}
|
|
58415
|
+
function filesForComposite(fireMatrix, members, minMatch) {
|
|
58416
|
+
const fired = /* @__PURE__ */ new Set();
|
|
58417
|
+
for (const [file, ids] of fireMatrix) {
|
|
58418
|
+
let n = 0;
|
|
58419
|
+
for (const m of members) if (ids.has(m)) n++;
|
|
58420
|
+
if (n >= minMatch) fired.add(file);
|
|
58421
|
+
}
|
|
58422
|
+
return fired;
|
|
58423
|
+
}
|
|
58424
|
+
var DEFAULT_CALIBRATION = {
|
|
58425
|
+
// 1.5× is the documented floor.
|
|
58426
|
+
recallFpRatio: 1.5,
|
|
58427
|
+
// 0.05 absolute FP — the same `min` value most rules get via the
|
|
58428
|
+
// v0.18.x calibration contract.
|
|
58429
|
+
minFpFloor: 0.05
|
|
58430
|
+
};
|
|
58431
|
+
function calibrateCluster(cluster2, edges, fireMatrix, positiveFiles, totalFiles, thresholds = DEFAULT_CALIBRATION) {
|
|
58432
|
+
if (cluster2.length < 2) return null;
|
|
58433
|
+
const clusterSet = new Set(cluster2);
|
|
58434
|
+
const clusterEdges = edges.filter(
|
|
58435
|
+
(e) => clusterSet.has(e.ruleIds[0]) && clusterSet.has(e.ruleIds[1])
|
|
58436
|
+
);
|
|
58437
|
+
if (clusterEdges.length === 0) return null;
|
|
58438
|
+
const strongest = clusterEdges.reduce((a, b) => b.weight > a.weight ? b : a);
|
|
58439
|
+
let best = null;
|
|
58440
|
+
for (let k = 1; k <= cluster2.length; k++) {
|
|
58441
|
+
const firedFiles = filesForComposite(fireMatrix, cluster2, k);
|
|
58442
|
+
const nFiles = firedFiles.size;
|
|
58443
|
+
if (nFiles === 0) continue;
|
|
58444
|
+
let recall = 0;
|
|
58445
|
+
let fp = 0;
|
|
58446
|
+
let precision = 0;
|
|
58447
|
+
let canCalibrate = false;
|
|
58448
|
+
if (positiveFiles && positiveFiles.size > 0) {
|
|
58449
|
+
let tp = 0;
|
|
58450
|
+
let fpc = 0;
|
|
58451
|
+
for (const file of firedFiles) {
|
|
58452
|
+
if (positiveFiles.has(file)) tp++;
|
|
58453
|
+
else fpc++;
|
|
58454
|
+
}
|
|
58455
|
+
const totalPos = positiveFiles.size;
|
|
58456
|
+
const totalNeg = Math.max(1, totalFiles - totalPos);
|
|
58457
|
+
recall = totalPos > 0 ? tp / totalPos : 0;
|
|
58458
|
+
fp = fpc / totalNeg;
|
|
58459
|
+
precision = tp + fpc > 0 ? tp / (tp + fpc) : 0;
|
|
58460
|
+
canCalibrate = true;
|
|
58461
|
+
}
|
|
58462
|
+
const F1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
|
|
58463
|
+
if (canCalibrate) {
|
|
58464
|
+
if (recall / Math.max(fp, 1e-9) < thresholds.recallFpRatio) continue;
|
|
58465
|
+
if (fp < thresholds.minFpFloor && recall < 0.5) continue;
|
|
58466
|
+
}
|
|
58467
|
+
if (best === null || F1 > best.F1 + 1e-9 || Math.abs(F1 - best.F1) < 1e-9 && k > best.minMatch) {
|
|
58468
|
+
best = {
|
|
58469
|
+
minMatch: k,
|
|
58470
|
+
npmi: strongest.weight,
|
|
58471
|
+
fisherP: strongest.fisherP,
|
|
58472
|
+
recall,
|
|
58473
|
+
FP: fp,
|
|
58474
|
+
precision,
|
|
58475
|
+
F1,
|
|
58476
|
+
nFiles
|
|
58477
|
+
};
|
|
58478
|
+
}
|
|
58479
|
+
}
|
|
58480
|
+
return best;
|
|
58481
|
+
}
|
|
58482
|
+
function compositeId(members) {
|
|
58483
|
+
const sorted = [...members].sort();
|
|
58484
|
+
return "composite/" + (0, import_node_crypto11.createHash)("sha1").update(sorted.join("\n")).digest("hex").slice(0, 12);
|
|
58485
|
+
}
|
|
58486
|
+
function worstSeverity(members, memberSeverities) {
|
|
58487
|
+
let worst = "low";
|
|
58488
|
+
const order = { low: 0, medium: 1, high: 2 };
|
|
58489
|
+
for (const m of members) {
|
|
58490
|
+
const s = memberSeverities?.get(m);
|
|
58491
|
+
if (s && order[s] > order[worst]) worst = s;
|
|
58492
|
+
}
|
|
58493
|
+
return worst;
|
|
58494
|
+
}
|
|
58495
|
+
function describeComposite(members, minMatch) {
|
|
58496
|
+
const clusterPart = members.length <= 4 ? members.join("+") : `${members.slice(0, 3).join("+")}+${members.length - 3} more`;
|
|
58497
|
+
return `Composite rule: at least ${minMatch} of {${clusterPart}} fire on the same file.`;
|
|
58498
|
+
}
|
|
58499
|
+
function runClusterer(input) {
|
|
58500
|
+
const total = input.fireMatrix.size;
|
|
58501
|
+
const minSupport = input.params?.minSupport ?? adaptiveMinSupport(total);
|
|
58502
|
+
const params = {
|
|
58503
|
+
minSupport,
|
|
58504
|
+
minNPMI: input.params?.minNPMI ?? DEFAULT_CLUSTER_PARAMS.minNPMI,
|
|
58505
|
+
fisherAlpha: input.params?.fisherAlpha ?? DEFAULT_CLUSTER_PARAMS.fisherAlpha,
|
|
58506
|
+
minClusterSize: input.params?.minClusterSize ?? DEFAULT_CLUSTER_PARAMS.minClusterSize
|
|
58507
|
+
};
|
|
58508
|
+
const support = computeRuleSupport(input.fireMatrix);
|
|
58509
|
+
const rulesKept = prefilterBySupport(support, total, params.minSupport);
|
|
58510
|
+
const edges = computeEdges(input.fireMatrix, rulesKept, total, params);
|
|
58511
|
+
const clusters = singleLinkageCluster(edges, params.minClusterSize);
|
|
58512
|
+
const entries = [];
|
|
58513
|
+
const clusterMaxNPMI = /* @__PURE__ */ new Map();
|
|
58514
|
+
for (const cluster2 of clusters) {
|
|
58515
|
+
const cal = calibrateCluster(
|
|
58516
|
+
cluster2,
|
|
58517
|
+
edges,
|
|
58518
|
+
input.fireMatrix,
|
|
58519
|
+
input.positiveFiles,
|
|
58520
|
+
total
|
|
58521
|
+
);
|
|
58522
|
+
if (!cal) continue;
|
|
58523
|
+
const id = compositeId(cluster2);
|
|
58524
|
+
const severity = worstSeverity(cluster2, input.memberSeverities);
|
|
58525
|
+
entries.push({
|
|
58526
|
+
id,
|
|
58527
|
+
ruleIds: [...cluster2].sort(),
|
|
58528
|
+
minMatch: cal.minMatch,
|
|
58529
|
+
severity,
|
|
58530
|
+
defaultOff: true,
|
|
58531
|
+
description: describeComposite(cluster2, cal.minMatch),
|
|
58532
|
+
calibration: {
|
|
58533
|
+
recall: round3(cal.recall),
|
|
58534
|
+
FP: round3(cal.FP),
|
|
58535
|
+
precision: round3(cal.precision),
|
|
58536
|
+
F1: round3(cal.F1),
|
|
58537
|
+
nFiles: cal.nFiles
|
|
58538
|
+
},
|
|
58539
|
+
provenance: {
|
|
58540
|
+
seed: "auto-cluster",
|
|
58541
|
+
discoveredAt: input.now ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
58542
|
+
nFiles: total,
|
|
58543
|
+
members: cluster2.length,
|
|
58544
|
+
npmi: round3(cal.npmi),
|
|
58545
|
+
fisherP: round3(cal.fisherP)
|
|
58546
|
+
}
|
|
58547
|
+
});
|
|
58548
|
+
clusterMaxNPMI.set(id, cal.npmi);
|
|
58549
|
+
}
|
|
58550
|
+
entries.sort((a, b) => b.calibration.F1 - a.calibration.F1 || a.id.localeCompare(b.id));
|
|
58551
|
+
return { entries, supportCounts: support, clusterMaxNPMI, filesScanned: total, params };
|
|
58552
|
+
}
|
|
58553
|
+
function round3(n) {
|
|
58554
|
+
return Math.round(n * 1e3) / 1e3;
|
|
58555
|
+
}
|
|
58556
|
+
|
|
58557
|
+
// src/rules/composite-loader.ts
|
|
58558
|
+
var import_node_fs32 = require("fs");
|
|
58559
|
+
var import_node_path37 = require("path");
|
|
58560
|
+
var COMPOSITES_FILE = "composites.json";
|
|
58561
|
+
function compositesPath(cwd) {
|
|
58562
|
+
return (0, import_node_path37.join)(cwd, COMPOSITES_FILE);
|
|
58563
|
+
}
|
|
58564
|
+
function readComposites(cwd) {
|
|
58565
|
+
const path = compositesPath(cwd);
|
|
58566
|
+
if (!(0, import_node_fs32.existsSync)(path)) return [];
|
|
58567
|
+
try {
|
|
58568
|
+
const raw = JSON.parse((0, import_node_fs32.readFileSync)(path, "utf-8"));
|
|
58569
|
+
if (!Array.isArray(raw)) return [];
|
|
58570
|
+
return raw.filter(isCompositeRuleEntry);
|
|
58571
|
+
} catch {
|
|
58572
|
+
return [];
|
|
58573
|
+
}
|
|
58574
|
+
}
|
|
58575
|
+
function isCompositeRuleEntry(value) {
|
|
58576
|
+
if (!value || typeof value !== "object") return false;
|
|
58577
|
+
const v = value;
|
|
58578
|
+
return typeof v.id === "string" && Array.isArray(v.ruleIds) && typeof v.minMatch === "number";
|
|
58579
|
+
}
|
|
58580
|
+
function writeComposites(cwd, entries) {
|
|
58581
|
+
const path = compositesPath(cwd);
|
|
58582
|
+
(0, import_node_fs32.mkdirSync)((0, import_node_path37.dirname)(path), { recursive: true });
|
|
58583
|
+
const tmp = `${path}.tmp`;
|
|
58584
|
+
(0, import_node_fs32.writeFileSync)(tmp, JSON.stringify(entries, null, 2), "utf-8");
|
|
58585
|
+
const fs = require("fs");
|
|
58586
|
+
fs.renameSync(tmp, path);
|
|
58587
|
+
}
|
|
58588
|
+
|
|
58589
|
+
// src/cli/commands/composite.ts
|
|
58590
|
+
function registerComposite(program) {
|
|
58591
|
+
const cmd = program.command("composite").description(
|
|
58592
|
+
"discover and enable composite rules (Sprint 3, \xA73b). Run `slopbrick composite discover` to scan scans.jsonl, audit the candidates, then `slopbrick composite enable <id>` to opt a specific composite into slopbrick.config.mjs."
|
|
58593
|
+
);
|
|
58594
|
+
cmd.command("discover").description(
|
|
58595
|
+
"run the empirical composite clusterer on .slopbrick/flywheel/scans.jsonl; writes composites.json"
|
|
58596
|
+
).action(async (_cmdOptions, command) => {
|
|
58597
|
+
const options = command.optsWithGlobals();
|
|
58598
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58599
|
+
const payloads = readTelemetry(cwd);
|
|
58600
|
+
if (payloads.length === 0) {
|
|
58601
|
+
logger.error(
|
|
58602
|
+
"`composite discover` needs at least 2 scans in .slopbrick/flywheel/scans.jsonl. Run `slopbrick scan` a few times first."
|
|
58603
|
+
);
|
|
58604
|
+
process.exit(1);
|
|
58605
|
+
}
|
|
58606
|
+
const fireMatrix = /* @__PURE__ */ new Map();
|
|
58607
|
+
for (const payload of payloads) {
|
|
58608
|
+
for (const f of payload.files) {
|
|
58609
|
+
const set = fireMatrix.get(f.hash) ?? /* @__PURE__ */ new Set();
|
|
58610
|
+
for (const id of f.ruleIds) set.add(id);
|
|
58611
|
+
if (set.size > 0) fireMatrix.set(f.hash, set);
|
|
58612
|
+
}
|
|
58613
|
+
}
|
|
58614
|
+
const out = runClusterer({
|
|
58615
|
+
fireMatrix,
|
|
58616
|
+
// No labeled positive set; let the clusterer surface for audit.
|
|
58617
|
+
positiveFiles: void 0,
|
|
58618
|
+
now: (/* @__PURE__ */ new Date()).toISOString()
|
|
58619
|
+
});
|
|
58620
|
+
const finalEntries = out.entries.length === 0 ? [...(await Promise.resolve().then(() => (init_composite_seed(), composite_seed_exports))).HAND_CURATED_SEED] : out.entries;
|
|
58621
|
+
writeComposites(cwd, finalEntries);
|
|
58622
|
+
if (out.entries.length === 0) {
|
|
58623
|
+
logger.info("Composite discovery: no clusters passed the empirical gate.");
|
|
58624
|
+
logger.info(
|
|
58625
|
+
" (Try lowering --min-support, or run more scans to densify the ledger.)"
|
|
58626
|
+
);
|
|
58627
|
+
return;
|
|
58628
|
+
}
|
|
58629
|
+
logger.info(
|
|
58630
|
+
`Composite discovery: ${out.entries.length} candidate(s) written to composites.json`
|
|
58631
|
+
);
|
|
58632
|
+
const lines = [];
|
|
58633
|
+
for (const e of out.entries) {
|
|
58634
|
+
lines.push("");
|
|
58635
|
+
lines.push(
|
|
58636
|
+
` ${e.id} minMatch=${e.minMatch} F1=${e.calibration.F1} (${e.ruleIds.length} members)`
|
|
58637
|
+
);
|
|
58638
|
+
lines.push(` ${describeComposite(e.ruleIds, e.minMatch)}`);
|
|
58639
|
+
lines.push(
|
|
58640
|
+
` provenance: ${e.provenance.seed} npmi=${e.provenance.npmi} fisherP=${e.provenance.fisherP}`
|
|
58641
|
+
);
|
|
58642
|
+
}
|
|
58643
|
+
logger.info(lines.join("\n"));
|
|
58644
|
+
logger.info(
|
|
58645
|
+
"\nNext step: review each entry. Opt in with `slopbrick composite enable <id>`; opt out by removing the entry."
|
|
58646
|
+
);
|
|
58647
|
+
});
|
|
58648
|
+
cmd.command("enable <id>").description(
|
|
58649
|
+
"opt a specific composite into slopbrick.config.mjs#compositeRules (one composite per call)"
|
|
58650
|
+
).action(async (id, _cmdOptions, command) => {
|
|
58651
|
+
const options = command.optsWithGlobals();
|
|
58652
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58653
|
+
const composites = readComposites(cwd);
|
|
58654
|
+
const target = composites.find((c) => c.id === id);
|
|
58655
|
+
if (!target) {
|
|
58656
|
+
logger.error(
|
|
58657
|
+
`Composite "${id}" not found in composites.json. Run \`slopbrick composite discover\` first to populate the ledger.`
|
|
58658
|
+
);
|
|
58659
|
+
process.exit(1);
|
|
58660
|
+
}
|
|
58661
|
+
const cfgPath = (0, import_node_path38.resolve)(cwd, "slopbrick.config.mjs");
|
|
58662
|
+
const fs = require("fs");
|
|
58663
|
+
if (!fs.existsSync(cfgPath)) {
|
|
58664
|
+
logger.error(`No slopbrick.config.mjs found at ${cfgPath}.`);
|
|
58665
|
+
process.exit(1);
|
|
58666
|
+
}
|
|
58667
|
+
const src = fs.readFileSync(cfgPath, "utf-8");
|
|
58668
|
+
const compositeSnippet = JSON.stringify(
|
|
58669
|
+
{
|
|
58670
|
+
id: target.id,
|
|
58671
|
+
category: "ai",
|
|
58672
|
+
severity: target.severity,
|
|
58673
|
+
aiSpecific: true,
|
|
58674
|
+
description: target.description,
|
|
58675
|
+
defaultOff: true,
|
|
58676
|
+
ruleIds: target.ruleIds,
|
|
58677
|
+
minMatch: target.minMatch,
|
|
58678
|
+
create: "() => ({})",
|
|
58679
|
+
analyze: "() => []"
|
|
58680
|
+
},
|
|
58681
|
+
null,
|
|
58682
|
+
2
|
|
58683
|
+
);
|
|
58684
|
+
if (src.includes(`id: '${target.id}'`) || src.includes(`id: "${target.id}"`)) {
|
|
58685
|
+
logger.info(`Composite "${id}" is already enabled in slopbrick.config.mjs (idempotent).`);
|
|
58686
|
+
return;
|
|
58687
|
+
}
|
|
58688
|
+
const updated = src.replace(
|
|
58689
|
+
/(export default\s*{[\s\S]*?)(}\s*;?\s*)$/,
|
|
58690
|
+
(_match, head, tail) => `${head} compositeRules: [
|
|
58691
|
+
${compositeSnippet}
|
|
58692
|
+
],
|
|
58693
|
+
${tail}`
|
|
58694
|
+
);
|
|
58695
|
+
fs.writeFileSync(cfgPath, updated, "utf-8");
|
|
58696
|
+
logger.info(`Composite "${id}" enabled.`);
|
|
58697
|
+
logger.info("Next scan will apply it; rerun `slopbrick scan` to see the effect.");
|
|
58698
|
+
});
|
|
58699
|
+
cmd.command("list").description("list composites currently registered (composites.json + config-declared)").action(async (_cmdOptions, command) => {
|
|
58700
|
+
const options = command.optsWithGlobals();
|
|
58701
|
+
const cwd = (0, import_node_path38.resolve)(options.workspace ?? process.cwd());
|
|
58702
|
+
const composites = readComposites(cwd);
|
|
58703
|
+
if (composites.length === 0) {
|
|
58704
|
+
logger.info("No composites registered. Run `slopbrick composite discover` first.");
|
|
58705
|
+
return;
|
|
58706
|
+
}
|
|
58707
|
+
const lines = ["Registered composites:"];
|
|
58708
|
+
for (const e of composites) {
|
|
58709
|
+
lines.push(
|
|
58710
|
+
`
|
|
58711
|
+
${e.id}
|
|
58712
|
+
${describeComposite(e.ruleIds, e.minMatch)}
|
|
58713
|
+
members=[${e.ruleIds.join(", ")}] minMatch=${e.minMatch}
|
|
58714
|
+
F1=${e.calibration.F1} recall=${e.calibration.recall} FP=${e.calibration.FP}`
|
|
58715
|
+
);
|
|
58716
|
+
}
|
|
58717
|
+
logger.info(lines.join("\n"));
|
|
58718
|
+
});
|
|
58719
|
+
}
|
|
58720
|
+
|
|
58136
58721
|
// src/cli/explain.ts
|
|
58137
58722
|
var RULES_BASE_URL2 = "https://github.com/usebrick/platform/blob/main/packages/slopbrick/src/rules";
|
|
58138
58723
|
function ruleIdToFilename2(ruleId) {
|
|
@@ -58182,152 +58767,7 @@ function formatExplain(result) {
|
|
|
58182
58767
|
// src/cli/commands/explain.ts
|
|
58183
58768
|
init_logger();
|
|
58184
58769
|
init_builtins();
|
|
58185
|
-
|
|
58186
|
-
// src/snippet/data.ts
|
|
58187
|
-
var CATEGORY_DIRECTIVES = {
|
|
58188
|
-
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.',
|
|
58189
|
-
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.",
|
|
58190
|
-
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.',
|
|
58191
|
-
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".',
|
|
58192
|
-
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.",
|
|
58193
|
-
typo: `Never leave TODO / placeholder / "change me" copy in shipped code. Use real i18n strings or the project's content map.`,
|
|
58194
|
-
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].",
|
|
58195
|
-
component: "Don't build components > 200 lines. Extract shared subcomponents. Avoid circular prop drilling \u2014 use context.",
|
|
58196
|
-
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.",
|
|
58197
|
-
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'."
|
|
58198
|
-
};
|
|
58199
|
-
var RULE_HINTS = {
|
|
58200
|
-
// v0.38.0 hygiene: 37 v10-DORMANT rule hints were removed. Every
|
|
58201
|
-
// remaining key matches a rule in src/rules/builtins.ts. No orphans.
|
|
58202
|
-
// (v0.16.0 previously moved 35 out-of-scope orphan hints to
|
|
58203
|
-
// docs/research/backlog-rule-hints.md
|
|
58204
|
-
// for future implementers to paste back when the corresponding rules ship.)
|
|
58205
|
-
"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.",
|
|
58206
|
-
"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.",
|
|
58207
|
-
"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.",
|
|
58208
|
-
"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.",
|
|
58209
|
-
"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.",
|
|
58210
|
-
"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.",
|
|
58211
|
-
"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.',
|
|
58212
|
-
"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.",
|
|
58213
|
-
"visual/arbitrary-escape": "Never use bracket-notation values like text-[13px] or bg-[#7c3aed]. Use design tokens instead.",
|
|
58214
|
-
"visual/spacing-scale-violation": "Use spacing scale tokens (p-2, gap-4, etc.) instead of arbitrary values like p-[13px] or gap-[1.75rem].",
|
|
58215
|
-
"visual/radius-scale-violation": "Use radius scale tokens (rounded-md, rounded-lg, etc.) instead of arbitrary values like rounded-[7px].",
|
|
58216
|
-
// v0.16.0 — in-scope orphans kept here (corresponding rule ships in v0.16.0).
|
|
58217
|
-
"typo/placeholder-text": 'Never leave "TODO", "placeholder", "change me", "your text here" in shipped UI.',
|
|
58218
|
-
"logic/key-prop-missing": "Always provide a stable `key` prop when rendering lists.",
|
|
58219
|
-
"logic/boundary-violation": "Don't import data-layer / DB code into UI components. Server-side only.",
|
|
58220
|
-
"wcag/missing-alt": 'Every <img> needs alt text. Decorative: alt="". Informative: describe the image.',
|
|
58221
|
-
"security/localstorage-token": "Never store JWT / access token / refresh token in localStorage or sessionStorage. Issue as httpOnly cookie.",
|
|
58222
|
-
"security/eval": "Never use eval() or new Function(). These are RCE vectors if the input is ever attacker-controlled.",
|
|
58223
|
-
"security/target-blank-no-noopener": 'Always add rel="noopener" (or rel="noreferrer") to target="_blank" links.',
|
|
58224
|
-
"component/giant-component": "Don't build components > 200 lines. Extract shared subcomponents.",
|
|
58225
|
-
"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.",
|
|
58226
|
-
"component/shadcn-prop-mismatch": "Select shadcn variants via the `variant` prop, not long `className` overrides. See the component registry for available variants.",
|
|
58227
|
-
"context/import-path-mismatch": "Use only the canonical import paths declared in brick.config.json (e.g. @/components/ui/, @/lib/).",
|
|
58228
|
-
"layout/gap-monopoly": "Mix gap-2 / gap-4 / gap-6 / gap-12 deliberately. Don't repeat the same gap value across the whole project.",
|
|
58229
|
-
"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.",
|
|
58230
|
-
"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.",
|
|
58231
|
-
"layout/spacing-grid": "Use the configured spacing scale (4px or 8px grid). Avoid arbitrary values like p-[13px] that aren't on the scale.",
|
|
58232
|
-
"logic/ghost-defensive": "Use optional chaining (?.) or early returns instead of deep && guards. If a defensive chain runs 3+ levels deep, refactor.",
|
|
58233
|
-
"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.)",
|
|
58234
|
-
"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.)",
|
|
58235
|
-
"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.)",
|
|
58236
|
-
"logic/math-any-density": "Replace `: any` with proper types. Start with the parameter/return types of the most-used functions.",
|
|
58237
|
-
"logic/math-console-log-storm": "Replace debug logs with a proper debugger or logger.debug(). Remove all console.log before shipping.",
|
|
58238
|
-
"logic/math-gini-class-usage": "Spread usage across more class tokens instead of repeating the same handful (p-4, p-8, rounded-lg, etc.).",
|
|
58239
|
-
"logic/math-variable-name-entropy": "Use domain-specific identifier names (reservations, invoices, customers) instead of generic data/items/value.",
|
|
58240
|
-
"logic/optimistic-no-rollback": "In optimistic updates, revert state in the catch block: `setX(prev => prev)`. Never leave stale UI on error.",
|
|
58241
|
-
"logic/reactive-hook-soup": "Coordinate state via a single derived value (useMemo) or a state machine. Avoid chained useEffects that sync local state.",
|
|
58242
|
-
"logic/zombie-state": "Remove unused useState or wire it into the component. Don't leave declared-but-never-read state bindings.",
|
|
58243
|
-
"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.",
|
|
58244
|
-
"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.",
|
|
58245
|
-
"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.",
|
|
58246
|
-
"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.",
|
|
58247
|
-
"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.",
|
|
58248
|
-
"perf/cls-image": "Add width/height attributes or an aspect-ratio utility to prevent layout shift.",
|
|
58249
|
-
"perf/css-bloat": "Extract to a CSS variable (`--surface-card`) or a component prop when a class string repeats 5+ times.",
|
|
58250
|
-
"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.',
|
|
58251
|
-
"visual/inline-style-dominance": "Replace inline `style={{...}}` with className utilities (e.g. Tailwind `p-4 m-2 gap-3`) or a CSS module class.",
|
|
58252
|
-
"visual/math-default-font": "Import a distinctive font (next/font/google, @font-face, or a CSS variable) instead of relying on the framework default.",
|
|
58253
|
-
"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.",
|
|
58254
|
-
"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.",
|
|
58255
|
-
"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.",
|
|
58256
|
-
"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%.",
|
|
58257
|
-
"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.",
|
|
58258
|
-
"wcag/focus-appearance": "Add a focus-visible:ring-* class, or remove outline-none. Keyboard users need a visible focus indicator.",
|
|
58259
|
-
"wcag/focus-obscured": "Ensure focused elements are not hidden behind fixed or sticky wrappers.",
|
|
58260
|
-
"test/weak-assertion": "Assert on a specific value or shape: `expect(x).toEqual(expectedValue)`. Avoid `.toBeDefined()` / `.toBeTruthy()` placeholders and tautological `expect(x).toBe(x)`.",
|
|
58261
|
-
"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.",
|
|
58262
|
-
"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`).",
|
|
58263
|
-
"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.",
|
|
58264
|
-
"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.",
|
|
58265
|
-
// v0.13.0 — AI-specific rules (peer-reviewed signals).
|
|
58266
|
-
"ai/markdown-leakage": "Delete stray `\\`\\`\\`<lang>\\`\\`\\`` markers; they are Markdown fences, not valid syntax in standalone source files (Yotkova et al. SemEval-2026).",
|
|
58267
|
-
"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).",
|
|
58268
|
-
"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).",
|
|
58269
|
-
"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).",
|
|
58270
|
-
"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).",
|
|
58271
|
-
"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).",
|
|
58272
|
-
"ai/renyi-profile": "The token distribution is mass-concentrated on a few high-frequency tokens. Verify authorship if unexpected (R\xE9nyi 1961, Moslonka 2025).",
|
|
58273
|
-
"ai/segment-surprisal-cv": "The cross-entropy is suspiciously uniform across the file. Real codebases have varied registers (Binoculars, Hans 2024).",
|
|
58274
|
-
"ai/compression-profile": "The file compresses unusually well and lines are highly repetitive \u2014 characteristic of AI-generated boilerplate (Cilibrasi 2005, Mahoney 1999).",
|
|
58275
|
-
// v0.14.5b — 6 new AI tendency detection rules (DORMANT in v0.14.5b;
|
|
58276
|
-
// reclassified post-v7 calibration in v0.14.5d)
|
|
58277
|
-
"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.",
|
|
58278
|
-
"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.",
|
|
58279
|
-
"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.",
|
|
58280
|
-
"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.",
|
|
58281
|
-
"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.",
|
|
58282
|
-
"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.",
|
|
58283
|
-
// v0.17.0 — db/* rules (Postgres static analysis via pgsql-parser)
|
|
58284
|
-
"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.',
|
|
58285
|
-
// v0.17.0 — docs/* rules (markdown drift detection)
|
|
58286
|
-
"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.",
|
|
58287
|
-
"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.",
|
|
58288
|
-
"docs/broken-link": "Create the file or fix the link target. On a public docs site, broken links erode trust more than stale copy.",
|
|
58289
|
-
// v0.18.9 — Rust-specific rules (tree-sitter-backed)
|
|
58290
|
-
"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.",
|
|
58291
|
-
"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.",
|
|
58292
|
-
"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.",
|
|
58293
|
-
"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.',
|
|
58294
|
-
// v0.19.0 — TypeScript-specific rules
|
|
58295
|
-
"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.",
|
|
58296
|
-
"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.",
|
|
58297
|
-
"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.",
|
|
58298
|
-
"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.",
|
|
58299
|
-
// v0.19.0 — Go-specific rules
|
|
58300
|
-
"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).',
|
|
58301
|
-
"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.",
|
|
58302
|
-
// v0.19.0 — Duplication detector
|
|
58303
|
-
"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.",
|
|
58304
|
-
// v0.23.0 — Type-2 near-duplicate detector (MinHash + LSH)
|
|
58305
|
-
"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.)",
|
|
58306
|
-
// v0.24.0 — Type-3 structural clone detector (two-stage MinHash)
|
|
58307
|
-
"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.)",
|
|
58308
|
-
// v0.24.0 — Kotlin rules (DORMANT until v9 Kotlin corpus calibration)
|
|
58309
|
-
// v0.29.0 — non-AI Kotlin rules (DORMANT until larger pos arm)
|
|
58310
|
-
// v0.30.0 — non-AI Java rules (Option C applied to v9 Java corpus, 92k files)
|
|
58311
|
-
"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.)",
|
|
58312
|
-
"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.)",
|
|
58313
|
-
// v0.35.0 — content-based detection (CoCoNUTS-inspired)
|
|
58314
|
-
"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.",
|
|
58315
|
-
// v0.35.1 — Raidar-inspired content-based detection
|
|
58316
|
-
"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)`.',
|
|
58317
|
-
// v0.24.0 — Swift rules (DORMANT until v9 Swift corpus calibration)
|
|
58318
|
-
"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.)",
|
|
58319
|
-
"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.)",
|
|
58320
|
-
"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.)",
|
|
58321
|
-
"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.)",
|
|
58322
|
-
"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.)",
|
|
58323
|
-
// v0.24.0 — C++ rules (DORMANT until v9 C++ corpus calibration)
|
|
58324
|
-
"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.)",
|
|
58325
|
-
"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.)",
|
|
58326
|
-
"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.)",
|
|
58327
|
-
"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.)"
|
|
58328
|
-
};
|
|
58329
|
-
|
|
58330
|
-
// src/cli/commands/explain.ts
|
|
58770
|
+
init_data();
|
|
58331
58771
|
function registerExplain(program) {
|
|
58332
58772
|
program.command("explain <ruleId>").description("Print rationale, pattern, and remediation for a single rule").action((ruleId) => {
|
|
58333
58773
|
const result = explainRule(ruleId, builtinRules, RULE_HINTS);
|
|
@@ -58337,7 +58777,7 @@ function registerExplain(program) {
|
|
|
58337
58777
|
}
|
|
58338
58778
|
|
|
58339
58779
|
// src/cli/commands/calibration.ts
|
|
58340
|
-
var
|
|
58780
|
+
var import_node_path39 = require("path");
|
|
58341
58781
|
var import_node_url = require("url");
|
|
58342
58782
|
|
|
58343
58783
|
// src/rules/signal-strength.json
|
|
@@ -61930,7 +62370,7 @@ var signal_strength_default2 = {
|
|
|
61930
62370
|
};
|
|
61931
62371
|
|
|
61932
62372
|
// src/cli/commands/calibration.ts
|
|
61933
|
-
var __dirname = (0,
|
|
62373
|
+
var __dirname = (0, import_node_path39.dirname)((0, import_node_url.fileURLToPath)(__importMetaUrl));
|
|
61934
62374
|
function loadData() {
|
|
61935
62375
|
return signal_strength_default2;
|
|
61936
62376
|
}
|
|
@@ -62051,13 +62491,13 @@ function registerCalibration(program) {
|
|
|
62051
62491
|
}
|
|
62052
62492
|
|
|
62053
62493
|
// src/cli/commands/install.ts
|
|
62054
|
-
var
|
|
62494
|
+
var import_node_path41 = require("path");
|
|
62055
62495
|
init_logger();
|
|
62056
62496
|
init_git();
|
|
62057
62497
|
|
|
62058
62498
|
// src/cli/installer.ts
|
|
62059
|
-
var
|
|
62060
|
-
var
|
|
62499
|
+
var import_node_fs33 = require("fs");
|
|
62500
|
+
var import_node_path40 = require("path");
|
|
62061
62501
|
var BEGIN_SENTINEL = "# slopbrick-hook-begin";
|
|
62062
62502
|
var END_SENTINEL = "# slopbrick-hook-end";
|
|
62063
62503
|
var SENTINEL_BLOCK = `${BEGIN_SENTINEL}
|
|
@@ -62065,14 +62505,14 @@ npx slopbrick --staged
|
|
|
62065
62505
|
${END_SENTINEL}
|
|
62066
62506
|
`;
|
|
62067
62507
|
function hookPath(gitRoot) {
|
|
62068
|
-
const huskyDir = (0,
|
|
62069
|
-
if ((0,
|
|
62070
|
-
return (0,
|
|
62508
|
+
const huskyDir = (0, import_node_path40.join)(gitRoot, ".husky");
|
|
62509
|
+
if ((0, import_node_fs33.existsSync)(huskyDir)) {
|
|
62510
|
+
return (0, import_node_path40.join)(huskyDir, "pre-commit");
|
|
62071
62511
|
}
|
|
62072
|
-
return (0,
|
|
62512
|
+
return (0, import_node_path40.join)(gitRoot, ".git", "hooks", "pre-commit");
|
|
62073
62513
|
}
|
|
62074
62514
|
function readHookContent(path) {
|
|
62075
|
-
return (0,
|
|
62515
|
+
return (0, import_node_fs33.readFileSync)(path, "utf8");
|
|
62076
62516
|
}
|
|
62077
62517
|
function sentinelsPresent(content) {
|
|
62078
62518
|
const lines = content.split(/\r?\n/);
|
|
@@ -62094,7 +62534,7 @@ function replaceSentinelBlock(content) {
|
|
|
62094
62534
|
}
|
|
62095
62535
|
function installHook(gitRoot) {
|
|
62096
62536
|
const path = hookPath(gitRoot);
|
|
62097
|
-
if ((0,
|
|
62537
|
+
if ((0, import_node_fs33.existsSync)(path)) {
|
|
62098
62538
|
const content = readHookContent(path);
|
|
62099
62539
|
const { begin, end } = sentinelsPresent(content);
|
|
62100
62540
|
if (begin && end) {
|
|
@@ -62106,9 +62546,9 @@ function installHook(gitRoot) {
|
|
|
62106
62546
|
exitCode: 0
|
|
62107
62547
|
};
|
|
62108
62548
|
}
|
|
62109
|
-
(0,
|
|
62549
|
+
(0, import_node_fs33.writeFileSync)(path, replaced.endsWith("\n") ? replaced : `${replaced}
|
|
62110
62550
|
`);
|
|
62111
|
-
(0,
|
|
62551
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
62112
62552
|
return {
|
|
62113
62553
|
ok: true,
|
|
62114
62554
|
message: "Replaced pre-commit hook block",
|
|
@@ -62126,17 +62566,17 @@ function installHook(gitRoot) {
|
|
|
62126
62566
|
}
|
|
62127
62567
|
const normalized = content.length > 0 && !content.endsWith("\n") ? `${content}
|
|
62128
62568
|
` : content;
|
|
62129
|
-
(0,
|
|
62130
|
-
(0,
|
|
62569
|
+
(0, import_node_fs33.writeFileSync)(path, `${normalized}${SENTINEL_BLOCK}`);
|
|
62570
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
62131
62571
|
return {
|
|
62132
62572
|
ok: true,
|
|
62133
62573
|
message: "Installed pre-commit hook",
|
|
62134
62574
|
exitCode: 0
|
|
62135
62575
|
};
|
|
62136
62576
|
}
|
|
62137
|
-
(0,
|
|
62138
|
-
(0,
|
|
62139
|
-
(0,
|
|
62577
|
+
(0, import_node_fs33.mkdirSync)((0, import_node_path40.dirname)(path), { recursive: true });
|
|
62578
|
+
(0, import_node_fs33.writeFileSync)(path, SENTINEL_BLOCK, { mode: 493 });
|
|
62579
|
+
(0, import_node_fs33.chmodSync)(path, 493);
|
|
62140
62580
|
return {
|
|
62141
62581
|
ok: true,
|
|
62142
62582
|
message: "Installed pre-commit hook",
|
|
@@ -62145,7 +62585,7 @@ function installHook(gitRoot) {
|
|
|
62145
62585
|
}
|
|
62146
62586
|
function uninstallHook(gitRoot) {
|
|
62147
62587
|
const path = hookPath(gitRoot);
|
|
62148
|
-
if (!(0,
|
|
62588
|
+
if (!(0, import_node_fs33.existsSync)(path)) {
|
|
62149
62589
|
return {
|
|
62150
62590
|
ok: true,
|
|
62151
62591
|
message: "Hook not installed",
|
|
@@ -62181,7 +62621,7 @@ function uninstallHook(gitRoot) {
|
|
|
62181
62621
|
}
|
|
62182
62622
|
const remaining = [...lines.slice(0, start), ...lines.slice(end + 1)];
|
|
62183
62623
|
const result = remaining.join("\n");
|
|
62184
|
-
(0,
|
|
62624
|
+
(0, import_node_fs33.writeFileSync)(path, result.endsWith("\n") ? result : `${result}
|
|
62185
62625
|
`);
|
|
62186
62626
|
return {
|
|
62187
62627
|
ok: true,
|
|
@@ -62194,7 +62634,7 @@ function uninstallHook(gitRoot) {
|
|
|
62194
62634
|
function registerInstall(program) {
|
|
62195
62635
|
program.command("install").description("install the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
62196
62636
|
const options = command.optsWithGlobals();
|
|
62197
|
-
const cwd = (0,
|
|
62637
|
+
const cwd = (0, import_node_path41.resolve)(options.workspace ?? process.cwd());
|
|
62198
62638
|
const root = getGitRoot(cwd);
|
|
62199
62639
|
if (!root) {
|
|
62200
62640
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -62209,13 +62649,13 @@ function registerInstall(program) {
|
|
|
62209
62649
|
}
|
|
62210
62650
|
|
|
62211
62651
|
// src/cli/commands/uninstall.ts
|
|
62212
|
-
var
|
|
62652
|
+
var import_node_path42 = require("path");
|
|
62213
62653
|
init_logger();
|
|
62214
62654
|
init_git();
|
|
62215
62655
|
function registerUninstall(program) {
|
|
62216
62656
|
program.command("uninstall").description("uninstall the git pre-commit hook").action(async (_cmdOptions, command) => {
|
|
62217
62657
|
const options = command.optsWithGlobals();
|
|
62218
|
-
const cwd = (0,
|
|
62658
|
+
const cwd = (0, import_node_path42.resolve)(options.workspace ?? process.cwd());
|
|
62219
62659
|
const root = getGitRoot(cwd);
|
|
62220
62660
|
if (!root) {
|
|
62221
62661
|
logger.error("Not a Git repository. Run `git init` first, or remove --staged from your command.");
|
|
@@ -62236,7 +62676,374 @@ init_logger();
|
|
|
62236
62676
|
init_builtins();
|
|
62237
62677
|
init_config2();
|
|
62238
62678
|
init_header();
|
|
62239
|
-
|
|
62679
|
+
|
|
62680
|
+
// src/mcp/tools.ts
|
|
62681
|
+
var import_node_fs34 = require("fs");
|
|
62682
|
+
var import_node_path43 = require("path");
|
|
62683
|
+
init_worker();
|
|
62684
|
+
init_patterns();
|
|
62685
|
+
init_architecture_score();
|
|
62686
|
+
init_business_logic();
|
|
62687
|
+
init_structure_md();
|
|
62688
|
+
var TOOL_DEFINITIONS = [
|
|
62689
|
+
{
|
|
62690
|
+
name: "slop_scan_file",
|
|
62691
|
+
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.",
|
|
62692
|
+
inputSchema: {
|
|
62693
|
+
type: "object",
|
|
62694
|
+
properties: {
|
|
62695
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." },
|
|
62696
|
+
framework: {
|
|
62697
|
+
type: "string",
|
|
62698
|
+
enum: ["react", "vue", "svelte", "astro", "html"],
|
|
62699
|
+
description: "Framework multiplier to apply. Defaults to the configured framework."
|
|
62700
|
+
}
|
|
62701
|
+
},
|
|
62702
|
+
required: ["path"]
|
|
62703
|
+
}
|
|
62704
|
+
},
|
|
62705
|
+
{
|
|
62706
|
+
name: "slop_explain_rule",
|
|
62707
|
+
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.",
|
|
62708
|
+
inputSchema: {
|
|
62709
|
+
type: "object",
|
|
62710
|
+
properties: {
|
|
62711
|
+
ruleId: { type: "string", description: 'e.g. "visual/ai-default-palette".' }
|
|
62712
|
+
},
|
|
62713
|
+
required: ["ruleId"]
|
|
62714
|
+
}
|
|
62715
|
+
},
|
|
62716
|
+
{
|
|
62717
|
+
name: "slop_list_rules",
|
|
62718
|
+
description: "List all registered rules with their category, severity, and aiSpecific flag. Optional category filter (visual | logic | wcag | security | perf | typo | layout | component | arch).",
|
|
62719
|
+
inputSchema: {
|
|
62720
|
+
type: "object",
|
|
62721
|
+
properties: {
|
|
62722
|
+
category: { type: "string", description: "Optional category filter." }
|
|
62723
|
+
}
|
|
62724
|
+
}
|
|
62725
|
+
},
|
|
62726
|
+
{
|
|
62727
|
+
name: "slop_suggest",
|
|
62728
|
+
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.",
|
|
62729
|
+
inputSchema: {
|
|
62730
|
+
type: "object",
|
|
62731
|
+
properties: {
|
|
62732
|
+
maxFiles: {
|
|
62733
|
+
type: "number",
|
|
62734
|
+
description: "Cap on files scanned to keep the inventory cheap. Defaults to 200."
|
|
62735
|
+
}
|
|
62736
|
+
}
|
|
62737
|
+
}
|
|
62738
|
+
},
|
|
62739
|
+
{
|
|
62740
|
+
name: "slop_suggest_with_structure",
|
|
62741
|
+
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.",
|
|
62742
|
+
inputSchema: {
|
|
62743
|
+
type: "object",
|
|
62744
|
+
properties: {
|
|
62745
|
+
maxFiles: {
|
|
62746
|
+
type: "number",
|
|
62747
|
+
description: "Cap on files scanned for the slow-path fallback. Defaults to 200."
|
|
62748
|
+
}
|
|
62749
|
+
}
|
|
62750
|
+
}
|
|
62751
|
+
},
|
|
62752
|
+
{
|
|
62753
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
62754
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
62755
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
62756
|
+
// They were strict subsets of slop_suggest; users should
|
|
62757
|
+
// call slop_suggest and read repositoryHealth /
|
|
62758
|
+
// architectureConsistency / businessLogicCoherence.
|
|
62759
|
+
name: "slop_check_constitution",
|
|
62760
|
+
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.",
|
|
62761
|
+
inputSchema: {
|
|
62762
|
+
type: "object",
|
|
62763
|
+
properties: {
|
|
62764
|
+
path: { type: "string", description: "Absolute or cwd-relative path to the source file." }
|
|
62765
|
+
},
|
|
62766
|
+
required: ["path"]
|
|
62767
|
+
}
|
|
62768
|
+
},
|
|
62769
|
+
{
|
|
62770
|
+
// v0.10.1: find_similar_function. The GIR (Give-Implementation-
|
|
62771
|
+
// Reference) primitive for slop_suggest. Given a function signature
|
|
62772
|
+
// (name + hooks + props), find the most similar existing
|
|
62773
|
+
// implementations across the codebase. Uses AST fingerprints
|
|
62774
|
+
// (sha256 over sorted hooks ∪ props ∪ params) + Jaccard similarity
|
|
62775
|
+
// — no LLM, no embeddings, deterministic. Foundation for StackPick.
|
|
62776
|
+
name: "slop_find_similar",
|
|
62777
|
+
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].",
|
|
62778
|
+
inputSchema: {
|
|
62779
|
+
type: "object",
|
|
62780
|
+
properties: {
|
|
62781
|
+
name: {
|
|
62782
|
+
type: "string",
|
|
62783
|
+
description: "Function/component name to match. Omit to match by hooks+props only."
|
|
62784
|
+
},
|
|
62785
|
+
hooks: {
|
|
62786
|
+
type: "array",
|
|
62787
|
+
items: { type: "string" },
|
|
62788
|
+
description: 'React hooks used by the target signature, e.g. ["useState", "useEffect"].'
|
|
62789
|
+
},
|
|
62790
|
+
props: {
|
|
62791
|
+
type: "array",
|
|
62792
|
+
items: { type: "string" },
|
|
62793
|
+
description: 'Component props for the target signature, e.g. ["variant", "size", "children"].'
|
|
62794
|
+
},
|
|
62795
|
+
limit: {
|
|
62796
|
+
type: "number",
|
|
62797
|
+
description: "Top-k results to return. Default 10. Capped at 50."
|
|
62798
|
+
}
|
|
62799
|
+
}
|
|
62800
|
+
}
|
|
62801
|
+
}
|
|
62802
|
+
];
|
|
62803
|
+
function toolError(message) {
|
|
62804
|
+
return {
|
|
62805
|
+
content: [{ type: "text", text: JSON.stringify({ error: message }) }],
|
|
62806
|
+
isError: true
|
|
62807
|
+
};
|
|
62808
|
+
}
|
|
62809
|
+
async function runScanFile(args, ctx) {
|
|
62810
|
+
const path = args.path;
|
|
62811
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62812
|
+
const result = await scanFile(path, ctx.config);
|
|
62813
|
+
const simplified = {
|
|
62814
|
+
filePath: result.filePath,
|
|
62815
|
+
componentCount: result.componentCount,
|
|
62816
|
+
parseError: result.parseError,
|
|
62817
|
+
issues: result.issues.map((i) => ({
|
|
62818
|
+
ruleId: i.ruleId,
|
|
62819
|
+
category: i.category,
|
|
62820
|
+
severity: i.severity,
|
|
62821
|
+
line: i.line,
|
|
62822
|
+
column: i.column,
|
|
62823
|
+
message: i.message,
|
|
62824
|
+
advice: i.advice
|
|
62825
|
+
}))
|
|
62826
|
+
};
|
|
62827
|
+
return {
|
|
62828
|
+
content: [{ type: "text", text: JSON.stringify(simplified, null, 2) }]
|
|
62829
|
+
};
|
|
62830
|
+
}
|
|
62831
|
+
function explainRule2(args, ctx) {
|
|
62832
|
+
const ruleId = args.ruleId;
|
|
62833
|
+
if (!ruleId) return toolError("Missing required argument: ruleId");
|
|
62834
|
+
const rule = ctx.rules.find((r) => r.id === ruleId);
|
|
62835
|
+
if (!rule) return toolError("Unknown rule: " + ruleId);
|
|
62836
|
+
const explanation = {
|
|
62837
|
+
ruleId: rule.id,
|
|
62838
|
+
category: rule.category,
|
|
62839
|
+
severity: rule.severity,
|
|
62840
|
+
aiSpecific: rule.aiSpecific,
|
|
62841
|
+
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 + ".",
|
|
62842
|
+
whereToLook: "src/rules/" + rule.category + "/" + rule.id.replace(/^[^/]+\//, "") + ".ts"
|
|
62843
|
+
};
|
|
62844
|
+
return {
|
|
62845
|
+
content: [{ type: "text", text: JSON.stringify(explanation, null, 2) }]
|
|
62846
|
+
};
|
|
62847
|
+
}
|
|
62848
|
+
function listRules(args, ctx) {
|
|
62849
|
+
const category = args.category;
|
|
62850
|
+
const filtered = category ? ctx.rules.filter((r) => r.category === category) : ctx.rules;
|
|
62851
|
+
const rules = filtered.map((r) => ({
|
|
62852
|
+
id: r.id,
|
|
62853
|
+
category: r.category,
|
|
62854
|
+
severity: r.severity,
|
|
62855
|
+
aiSpecific: r.aiSpecific
|
|
62856
|
+
}));
|
|
62857
|
+
return {
|
|
62858
|
+
content: [
|
|
62859
|
+
{
|
|
62860
|
+
type: "text",
|
|
62861
|
+
text: JSON.stringify({ count: rules.length, rules }, null, 2)
|
|
62862
|
+
}
|
|
62863
|
+
]
|
|
62864
|
+
};
|
|
62865
|
+
}
|
|
62866
|
+
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.";
|
|
62867
|
+
async function runSuggest(args, ctx, options = {}) {
|
|
62868
|
+
const { includeStructure = false } = options;
|
|
62869
|
+
if (includeStructure) {
|
|
62870
|
+
const cached = await readStructureMarkdown(ctx.cwd);
|
|
62871
|
+
if (cached !== null) {
|
|
62872
|
+
return {
|
|
62873
|
+
content: [{ type: "text", text: cached }]
|
|
62874
|
+
};
|
|
62875
|
+
}
|
|
62876
|
+
}
|
|
62877
|
+
const maxFilesRaw = args.maxFiles;
|
|
62878
|
+
const maxFiles = typeof maxFilesRaw === "number" && Number.isFinite(maxFilesRaw) && maxFilesRaw > 0 ? Math.min(2e3, Math.floor(maxFilesRaw)) : 200;
|
|
62879
|
+
try {
|
|
62880
|
+
const inventory = await buildPatternInventory(ctx.cwd, ctx.config, maxFiles);
|
|
62881
|
+
const doNotCreate = [
|
|
62882
|
+
...ctx.config.constitution?.forbidden ?? []
|
|
62883
|
+
];
|
|
62884
|
+
const declared = /* @__PURE__ */ new Set();
|
|
62885
|
+
for (const list of [
|
|
62886
|
+
ctx.config.constitution?.stateManagement ?? [],
|
|
62887
|
+
ctx.config.constitution?.dataFetching ?? [],
|
|
62888
|
+
ctx.config.constitution?.uiLibrary ?? [],
|
|
62889
|
+
ctx.config.constitution?.forms ?? [],
|
|
62890
|
+
ctx.config.constitution?.styling ?? [],
|
|
62891
|
+
ctx.config.constitution?.routing ?? []
|
|
62892
|
+
]) {
|
|
62893
|
+
for (const lib of list) declared.add(lib);
|
|
62894
|
+
}
|
|
62895
|
+
const doNotCreateCapped = doNotCreate.slice(0, 10);
|
|
62896
|
+
const payload = {
|
|
62897
|
+
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.",
|
|
62898
|
+
doNotCreate: doNotCreateCapped,
|
|
62899
|
+
declaredStack: Array.from(declared),
|
|
62900
|
+
existingPatterns: inventory
|
|
62901
|
+
};
|
|
62902
|
+
if (includeStructure) {
|
|
62903
|
+
payload.structureHint = STRUCTURE_NOT_FOUND_HINT;
|
|
62904
|
+
}
|
|
62905
|
+
try {
|
|
62906
|
+
const { loadHealth: loadHealth2 } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62907
|
+
const health = loadHealth2(ctx.cwd);
|
|
62908
|
+
if (health?.compositeScore) {
|
|
62909
|
+
payload.compositeScore = health.compositeScore;
|
|
62910
|
+
}
|
|
62911
|
+
} catch {
|
|
62912
|
+
}
|
|
62913
|
+
return {
|
|
62914
|
+
content: [
|
|
62915
|
+
{
|
|
62916
|
+
type: "text",
|
|
62917
|
+
text: JSON.stringify(payload, null, 2)
|
|
62918
|
+
}
|
|
62919
|
+
]
|
|
62920
|
+
};
|
|
62921
|
+
} catch (err) {
|
|
62922
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
62923
|
+
}
|
|
62924
|
+
}
|
|
62925
|
+
function runCheckConstitution(args, ctx) {
|
|
62926
|
+
const path = args.path;
|
|
62927
|
+
if (!path) return toolError("Missing required argument: path");
|
|
62928
|
+
const absPath = (0, import_node_path43.resolve)(ctx.cwd, path);
|
|
62929
|
+
let source;
|
|
62930
|
+
try {
|
|
62931
|
+
source = (0, import_node_fs34.readFileSync)(absPath, "utf-8");
|
|
62932
|
+
} catch (err) {
|
|
62933
|
+
return toolError(
|
|
62934
|
+
`Cannot read file ${absPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
62935
|
+
);
|
|
62936
|
+
}
|
|
62937
|
+
const result = checkFileConstitution(source, ctx.config.constitution);
|
|
62938
|
+
return {
|
|
62939
|
+
content: [
|
|
62940
|
+
{
|
|
62941
|
+
type: "text",
|
|
62942
|
+
text: JSON.stringify(
|
|
62943
|
+
{
|
|
62944
|
+
file: absPath,
|
|
62945
|
+
importCount: result.imports.length,
|
|
62946
|
+
violationCount: result.violations.length,
|
|
62947
|
+
imports: result.imports,
|
|
62948
|
+
violations: result.violations,
|
|
62949
|
+
// Field name kept stable for backward compatibility with
|
|
62950
|
+
// older consumers; the value reflects whether the merged
|
|
62951
|
+
// `config.constitution` was declared, detected, or absent.
|
|
62952
|
+
conventionSource: ctx.config.constitution ? "declared-or-detected" : "none"
|
|
62953
|
+
},
|
|
62954
|
+
null,
|
|
62955
|
+
2
|
|
62956
|
+
)
|
|
62957
|
+
}
|
|
62958
|
+
]
|
|
62959
|
+
};
|
|
62960
|
+
}
|
|
62961
|
+
async function runFindSimilar(args, ctx) {
|
|
62962
|
+
const { findSimilarFunctions: findSimilarFunctions2 } = await Promise.resolve().then(() => (init_dist2(), dist_exports2));
|
|
62963
|
+
const hooks = Array.isArray(args.hooks) ? args.hooks : [];
|
|
62964
|
+
const props = Array.isArray(args.props) ? args.props : [];
|
|
62965
|
+
const limitRaw = args.limit;
|
|
62966
|
+
const limit = typeof limitRaw === "number" && Number.isFinite(limitRaw) && limitRaw > 0 ? Math.floor(limitRaw) : 10;
|
|
62967
|
+
try {
|
|
62968
|
+
const matches = await findSimilarFunctions2(
|
|
62969
|
+
{
|
|
62970
|
+
name: typeof args.name === "string" ? args.name : void 0,
|
|
62971
|
+
hooks,
|
|
62972
|
+
props,
|
|
62973
|
+
limit,
|
|
62974
|
+
workspaceDir: ctx.cwd
|
|
62975
|
+
},
|
|
62976
|
+
{ cwd: ctx.cwd }
|
|
62977
|
+
);
|
|
62978
|
+
return {
|
|
62979
|
+
content: [
|
|
62980
|
+
{
|
|
62981
|
+
type: "text",
|
|
62982
|
+
text: JSON.stringify(
|
|
62983
|
+
{
|
|
62984
|
+
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.",
|
|
62985
|
+
count: matches.length,
|
|
62986
|
+
matches: matches.map((m) => ({
|
|
62987
|
+
name: m.signature.name,
|
|
62988
|
+
file: m.signature.fileRel,
|
|
62989
|
+
line: m.signature.line,
|
|
62990
|
+
similarity: Number(m.similarity.toFixed(3)),
|
|
62991
|
+
fingerprint: m.fingerprint,
|
|
62992
|
+
hooks: m.signature.hooks,
|
|
62993
|
+
props: m.signature.props,
|
|
62994
|
+
params: m.signature.params
|
|
62995
|
+
}))
|
|
62996
|
+
},
|
|
62997
|
+
null,
|
|
62998
|
+
2
|
|
62999
|
+
)
|
|
63000
|
+
}
|
|
63001
|
+
]
|
|
63002
|
+
};
|
|
63003
|
+
} catch (err) {
|
|
63004
|
+
return toolError(err instanceof Error ? err.message : String(err));
|
|
63005
|
+
}
|
|
63006
|
+
}
|
|
63007
|
+
async function handleToolCall(toolName, args, ctx) {
|
|
63008
|
+
const deprecation = getDeprecation(toolName);
|
|
63009
|
+
const deprecationNotice = deprecation ? {
|
|
63010
|
+
tool: toolName,
|
|
63011
|
+
replacedBy: deprecation.replacedBy,
|
|
63012
|
+
removedIn: deprecation.removedIn ?? "next major",
|
|
63013
|
+
reason: deprecation.reason
|
|
63014
|
+
} : void 0;
|
|
63015
|
+
switch (toolName) {
|
|
63016
|
+
case "slop_scan_file":
|
|
63017
|
+
return runScanFile(args, ctx);
|
|
63018
|
+
case "slop_explain_rule":
|
|
63019
|
+
return explainRule2(args, ctx);
|
|
63020
|
+
case "slop_list_rules":
|
|
63021
|
+
return listRules(args, ctx);
|
|
63022
|
+
case "slop_suggest":
|
|
63023
|
+
return runSuggest(args, ctx);
|
|
63024
|
+
case "slop_suggest_with_structure":
|
|
63025
|
+
return runSuggest(args, ctx, { includeStructure: true });
|
|
63026
|
+
// v0.39.0: removed 3 deprecated tools (slop_governance,
|
|
63027
|
+
// slop_architecture_score, slop_business_logic_score) that
|
|
63028
|
+
// were marked for removal in v0.13.0 but never removed.
|
|
63029
|
+
// Their runner functions (runGovernance, runArchitectureScore,
|
|
63030
|
+
// runBusinessLogicScore) are kept in the file for now
|
|
63031
|
+
// (marked @deprecated) to keep the diff small; they can be
|
|
63032
|
+
// deleted in a follow-up. New clients will never see these
|
|
63033
|
+
// tools listed in the MCP tools/list response.
|
|
63034
|
+
case "slop_check_constitution":
|
|
63035
|
+
return runCheckConstitution(args, ctx);
|
|
63036
|
+
case "slop_find_similar":
|
|
63037
|
+
return runFindSimilar(args, ctx);
|
|
63038
|
+
default:
|
|
63039
|
+
return toolError("Unknown tool: " + toolName);
|
|
63040
|
+
}
|
|
63041
|
+
}
|
|
63042
|
+
function getDeprecation(toolName) {
|
|
63043
|
+
return TOOL_DEFINITIONS.find((t) => t.name === toolName)?.deprecated;
|
|
63044
|
+
}
|
|
63045
|
+
|
|
63046
|
+
// src/mcp/server.ts
|
|
62240
63047
|
var SERVER_INFO = {
|
|
62241
63048
|
name: "slopbrick",
|
|
62242
63049
|
// v0.39.0: use the VERSION constant (same source as the CLI)
|
|
@@ -62304,7 +63111,7 @@ function errorResponse(id, code, message, data2) {
|
|
|
62304
63111
|
return { jsonrpc: "2.0", id, error: { code, message, ...data2 !== void 0 ? { data: data2 } : {} } };
|
|
62305
63112
|
}
|
|
62306
63113
|
async function runMcpServer(input, output, cwd) {
|
|
62307
|
-
return new Promise((
|
|
63114
|
+
return new Promise((resolve43) => {
|
|
62308
63115
|
let buffer = "";
|
|
62309
63116
|
input.setEncoding("utf-8");
|
|
62310
63117
|
input.on("data", (chunk) => {
|
|
@@ -62342,8 +63149,8 @@ async function runMcpServer(input, output, cwd) {
|
|
|
62342
63149
|
nlIdx = buffer.indexOf("\n");
|
|
62343
63150
|
}
|
|
62344
63151
|
});
|
|
62345
|
-
input.on("end", () =>
|
|
62346
|
-
input.on("close", () =>
|
|
63152
|
+
input.on("end", () => resolve43());
|
|
63153
|
+
input.on("close", () => resolve43());
|
|
62347
63154
|
});
|
|
62348
63155
|
}
|
|
62349
63156
|
|
|
@@ -62366,7 +63173,7 @@ function registerDoctor(program) {
|
|
|
62366
63173
|
}
|
|
62367
63174
|
|
|
62368
63175
|
// src/cli/commands/watch.ts
|
|
62369
|
-
var
|
|
63176
|
+
var import_node_path44 = require("path");
|
|
62370
63177
|
init_watch();
|
|
62371
63178
|
function registerWatch(program, scanAction) {
|
|
62372
63179
|
program.command("watch").description("re-run scan on every file change. Flags new violations as you write. The LockBrick prevention loop entry.").action(async (_cmdOptions, command) => {
|
|
@@ -62375,19 +63182,19 @@ function registerWatch(program, scanAction) {
|
|
|
62375
63182
|
...rawGlobals,
|
|
62376
63183
|
noIncrease: rawGlobals.increase === false
|
|
62377
63184
|
};
|
|
62378
|
-
const cwd = (0,
|
|
63185
|
+
const cwd = (0, import_node_path44.resolve)(options.workspace ?? process.cwd());
|
|
62379
63186
|
await scanAction([], options, command);
|
|
62380
63187
|
await watchProject(options, cwd, []);
|
|
62381
63188
|
});
|
|
62382
63189
|
}
|
|
62383
63190
|
|
|
62384
63191
|
// src/cli/commands/lock.ts
|
|
62385
|
-
var
|
|
63192
|
+
var import_node_path45 = require("path");
|
|
62386
63193
|
init_logger();
|
|
62387
63194
|
function registerLock(program) {
|
|
62388
63195
|
program.command("lock").description("install a Git pre-commit hook that runs `slopbrick scan --staged` on every commit. The LockBrick prevention loop: block AI-introduced slop from ever reaching the repo.").option("--uninstall", "remove the pre-commit hook instead of installing it").option("--husky", "force-install under .husky/pre-commit (Husky v9). Default auto-detects via .husky/ dir.").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62389
63196
|
(cmdOptions) => {
|
|
62390
|
-
const cwd = (0,
|
|
63197
|
+
const cwd = (0, import_node_path45.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62391
63198
|
if (cmdOptions.uninstall) {
|
|
62392
63199
|
const result2 = uninstallHook(cwd);
|
|
62393
63200
|
logger.info(result2.message);
|
|
@@ -62408,7 +63215,7 @@ function registerLock(program) {
|
|
|
62408
63215
|
}
|
|
62409
63216
|
|
|
62410
63217
|
// src/cli/commands/ci.ts
|
|
62411
|
-
var
|
|
63218
|
+
var import_node_path46 = require("path");
|
|
62412
63219
|
init_logger();
|
|
62413
63220
|
init_dist();
|
|
62414
63221
|
function registerCi(program, scanAction) {
|
|
@@ -62423,7 +63230,7 @@ function registerCi(program, scanAction) {
|
|
|
62423
63230
|
// scan only changed files
|
|
62424
63231
|
format: cmdOptions.format ?? "json"
|
|
62425
63232
|
};
|
|
62426
|
-
const cwd = (0,
|
|
63233
|
+
const cwd = (0, import_node_path46.resolve)(options.workspace ?? process.cwd());
|
|
62427
63234
|
await scanAction([], options, command);
|
|
62428
63235
|
const health = loadHealth(cwd);
|
|
62429
63236
|
if (!health) {
|
|
@@ -62451,12 +63258,12 @@ function registerCi(program, scanAction) {
|
|
|
62451
63258
|
}
|
|
62452
63259
|
|
|
62453
63260
|
// src/cli/commands/memory.ts
|
|
62454
|
-
var
|
|
63261
|
+
var import_node_path47 = require("path");
|
|
62455
63262
|
init_logger();
|
|
62456
63263
|
function registerMemory(program) {
|
|
62457
63264
|
program.command("memory").description("show or regenerate .slopbrick/structure.md (the agent-readable repository summary) without re-scanning").option("--show", "print the current .slopbrick/structure.md to stdout (default if no flag is passed)").option("--regenerate", "re-render structure.md from the existing inventory.json + constitution.json (no scan)").option("--workspace <path>", "workspace directory", process.cwd()).action(
|
|
62458
63265
|
async (cmdOptions) => {
|
|
62459
|
-
const cwd = (0,
|
|
63266
|
+
const cwd = (0, import_node_path47.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62460
63267
|
const { renderStructureMarkdown: renderStructureMarkdown2, readStructureMarkdown: readStructureMarkdown2, writeStructureMarkdown: writeStructureMarkdown2 } = await Promise.resolve().then(() => (init_structure_md(), structure_md_exports));
|
|
62461
63268
|
const { loadInventory: loadInventory2, loadConstitution: loadConstitution2, inventoryPath: invPath, constitutionPath: conPath } = await Promise.resolve().then(() => (init_dist(), dist_exports));
|
|
62462
63269
|
if (cmdOptions.regenerate) {
|
|
@@ -62486,49 +63293,49 @@ function registerMemory(program) {
|
|
|
62486
63293
|
}
|
|
62487
63294
|
|
|
62488
63295
|
// src/cli/commands/migrate.ts
|
|
62489
|
-
var
|
|
63296
|
+
var import_node_path49 = require("path");
|
|
62490
63297
|
init_logger();
|
|
62491
63298
|
|
|
62492
63299
|
// src/cli/migrate.ts
|
|
62493
|
-
var
|
|
62494
|
-
var
|
|
63300
|
+
var import_node_fs35 = require("fs");
|
|
63301
|
+
var import_node_path48 = require("path");
|
|
62495
63302
|
init_logger();
|
|
62496
63303
|
function planMigration(workspaceDir) {
|
|
62497
63304
|
const moves = [];
|
|
62498
63305
|
const rewrites = [];
|
|
62499
63306
|
const gitignoreEdits = [];
|
|
62500
|
-
const oldDir = (0,
|
|
62501
|
-
const newDir = (0,
|
|
62502
|
-
if ((0,
|
|
63307
|
+
const oldDir = (0, import_node_path48.join)(workspaceDir, ".slop-audit");
|
|
63308
|
+
const newDir = (0, import_node_path48.join)(workspaceDir, ".slopbrick");
|
|
63309
|
+
if ((0, import_node_fs35.existsSync)(oldDir)) {
|
|
62503
63310
|
moves.push({ from: oldDir, to: newDir, kind: "dir" });
|
|
62504
63311
|
rewrites.push({
|
|
62505
|
-
path: (0,
|
|
63312
|
+
path: (0, import_node_path48.join)(newDir, "inventory.json"),
|
|
62506
63313
|
field: "version",
|
|
62507
63314
|
from: '"1"',
|
|
62508
63315
|
to: '"2"'
|
|
62509
63316
|
});
|
|
62510
63317
|
rewrites.push({
|
|
62511
|
-
path: (0,
|
|
63318
|
+
path: (0, import_node_path48.join)(newDir, "constitution.json"),
|
|
62512
63319
|
field: "version",
|
|
62513
63320
|
from: '"1"',
|
|
62514
63321
|
to: '"2"'
|
|
62515
63322
|
});
|
|
62516
63323
|
}
|
|
62517
|
-
const oldCache = (0,
|
|
62518
|
-
const newCache = (0,
|
|
62519
|
-
if ((0,
|
|
63324
|
+
const oldCache = (0, import_node_path48.join)(workspaceDir, ".slop-audit-cache.json");
|
|
63325
|
+
const newCache = (0, import_node_path48.join)(workspaceDir, ".slopbrick-cache.json");
|
|
63326
|
+
if ((0, import_node_fs35.existsSync)(oldCache)) {
|
|
62520
63327
|
moves.push({ from: oldCache, to: newCache, kind: "file" });
|
|
62521
63328
|
}
|
|
62522
63329
|
for (const ext of ["mjs", "cjs", "js"]) {
|
|
62523
|
-
const oldCfg = (0,
|
|
62524
|
-
const newCfg = (0,
|
|
62525
|
-
if ((0,
|
|
63330
|
+
const oldCfg = (0, import_node_path48.join)(workspaceDir, `slop-audit.config.${ext}`);
|
|
63331
|
+
const newCfg = (0, import_node_path48.join)(workspaceDir, `slopbrick.config.${ext}`);
|
|
63332
|
+
if ((0, import_node_fs35.existsSync)(oldCfg)) {
|
|
62526
63333
|
moves.push({ from: oldCfg, to: newCfg, kind: "config" });
|
|
62527
63334
|
}
|
|
62528
63335
|
}
|
|
62529
|
-
const gi = (0,
|
|
62530
|
-
if ((0,
|
|
62531
|
-
const content = (0,
|
|
63336
|
+
const gi = (0, import_node_path48.join)(workspaceDir, ".gitignore");
|
|
63337
|
+
if ((0, import_node_fs35.existsSync)(gi)) {
|
|
63338
|
+
const content = (0, import_node_fs35.readFileSync)(gi, "utf-8");
|
|
62532
63339
|
if (content.includes(".slop-audit/")) {
|
|
62533
63340
|
gitignoreEdits.push({
|
|
62534
63341
|
path: gi,
|
|
@@ -62547,28 +63354,28 @@ function planMigration(workspaceDir) {
|
|
|
62547
63354
|
return { moves, rewrites, gitignoreEdits };
|
|
62548
63355
|
}
|
|
62549
63356
|
function isAlreadyMigrated(workspaceDir) {
|
|
62550
|
-
return (0,
|
|
63357
|
+
return (0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slopbrick")) && !(0, import_node_fs35.existsSync)((0, import_node_path48.join)(workspaceDir, ".slop-audit"));
|
|
62551
63358
|
}
|
|
62552
63359
|
function applyMigration(plan, options = {}) {
|
|
62553
63360
|
if (options.dryRun) return;
|
|
62554
63361
|
for (const m of plan.moves) {
|
|
62555
|
-
(0,
|
|
63362
|
+
(0, import_node_fs35.renameSync)(m.from, m.to);
|
|
62556
63363
|
}
|
|
62557
63364
|
for (const r of plan.rewrites) {
|
|
62558
|
-
if (!(0,
|
|
62559
|
-
const content = (0,
|
|
63365
|
+
if (!(0, import_node_fs35.existsSync)(r.path)) continue;
|
|
63366
|
+
const content = (0, import_node_fs35.readFileSync)(r.path, "utf-8");
|
|
62560
63367
|
const next = content.replace(`"version": ${r.from}`, `"version": ${r.to}`);
|
|
62561
|
-
(0,
|
|
63368
|
+
(0, import_node_fs35.writeFileSync)(r.path, next);
|
|
62562
63369
|
}
|
|
62563
63370
|
for (const g of plan.gitignoreEdits) {
|
|
62564
|
-
const content = (0,
|
|
63371
|
+
const content = (0, import_node_fs35.readFileSync)(g.path, "utf-8");
|
|
62565
63372
|
const next = content.replaceAll(g.from, g.to);
|
|
62566
|
-
(0,
|
|
63373
|
+
(0, import_node_fs35.writeFileSync)(g.path, next);
|
|
62567
63374
|
}
|
|
62568
63375
|
}
|
|
62569
63376
|
function runMigrate(options) {
|
|
62570
63377
|
const { workspace, dryRun = false, force = false } = options;
|
|
62571
|
-
if (!(0,
|
|
63378
|
+
if (!(0, import_node_fs35.existsSync)(workspace)) {
|
|
62572
63379
|
return {
|
|
62573
63380
|
ok: false,
|
|
62574
63381
|
alreadyMigrated: false,
|
|
@@ -62578,9 +63385,9 @@ function runMigrate(options) {
|
|
|
62578
63385
|
};
|
|
62579
63386
|
}
|
|
62580
63387
|
const alreadyMigrated = isAlreadyMigrated(workspace);
|
|
62581
|
-
const newDir = (0,
|
|
62582
|
-
const oldDir = (0,
|
|
62583
|
-
if ((0,
|
|
63388
|
+
const newDir = (0, import_node_path48.join)(workspace, ".slopbrick");
|
|
63389
|
+
const oldDir = (0, import_node_path48.join)(workspace, ".slop-audit");
|
|
63390
|
+
if ((0, import_node_fs35.existsSync)(newDir) && (0, import_node_fs35.existsSync)(oldDir) && !force) {
|
|
62584
63391
|
return {
|
|
62585
63392
|
ok: false,
|
|
62586
63393
|
alreadyMigrated: false,
|
|
@@ -62656,7 +63463,7 @@ function registerMigrate(program) {
|
|
|
62656
63463
|
(cmdOptions, command) => {
|
|
62657
63464
|
const globals = command.optsWithGlobals();
|
|
62658
63465
|
const format = (cmdOptions.format ?? globals.format) === "json" ? "json" : "pretty";
|
|
62659
|
-
const cwd = (0,
|
|
63466
|
+
const cwd = (0, import_node_path49.resolve)(cmdOptions.workspace ?? process.cwd());
|
|
62660
63467
|
const result = runMigrate({
|
|
62661
63468
|
workspace: cwd,
|
|
62662
63469
|
dryRun: cmdOptions.dryRun,
|
|
@@ -62760,19 +63567,19 @@ function registerRules(program) {
|
|
|
62760
63567
|
}
|
|
62761
63568
|
|
|
62762
63569
|
// src/cli/commands/validate-config.ts
|
|
62763
|
-
var
|
|
62764
|
-
var
|
|
63570
|
+
var import_node_fs36 = require("fs");
|
|
63571
|
+
var import_node_path50 = require("path");
|
|
62765
63572
|
init_logger();
|
|
62766
63573
|
init_validation();
|
|
62767
63574
|
function registerValidateConfig(program) {
|
|
62768
63575
|
program.command("validate-config [path]").description("Statically validate a slopbrick.config.mjs without scanning").action(async (configPath) => {
|
|
62769
|
-
const path = configPath ? (0,
|
|
62770
|
-
if (!(0,
|
|
63576
|
+
const path = configPath ? (0, import_node_path50.resolve)(configPath) : (0, import_node_path50.resolve)(process.cwd(), "slopbrick.config.mjs");
|
|
63577
|
+
if (!(0, import_node_fs36.existsSync)(path)) {
|
|
62771
63578
|
logger.error(`Error: config file not found: ${path}`);
|
|
62772
63579
|
process.exit(2);
|
|
62773
63580
|
}
|
|
62774
63581
|
try {
|
|
62775
|
-
const mod = (0,
|
|
63582
|
+
const mod = (0, import_node_path50.extname)(path) === ".cjs" ? require(path) : await import(path);
|
|
62776
63583
|
const userConfig = mod.default ?? mod;
|
|
62777
63584
|
const result = validateConfig(userConfig);
|
|
62778
63585
|
if (result.errors.length === 0) {
|
|
@@ -62992,8 +63799,8 @@ ${formatMarkdown(result.report)}`);
|
|
|
62992
63799
|
}
|
|
62993
63800
|
|
|
62994
63801
|
// src/cli/commands/calibrate.ts
|
|
62995
|
-
var
|
|
62996
|
-
var
|
|
63802
|
+
var import_node_fs39 = require("fs");
|
|
63803
|
+
var import_node_path54 = require("path");
|
|
62997
63804
|
init_logger();
|
|
62998
63805
|
|
|
62999
63806
|
// src/research/providers/openai.ts
|
|
@@ -63049,8 +63856,8 @@ function createProvider(config) {
|
|
|
63049
63856
|
}
|
|
63050
63857
|
|
|
63051
63858
|
// src/research/generator.ts
|
|
63052
|
-
var
|
|
63053
|
-
var
|
|
63859
|
+
var import_node_fs37 = require("fs");
|
|
63860
|
+
var import_node_path51 = require("path");
|
|
63054
63861
|
|
|
63055
63862
|
// src/research/prompts.ts
|
|
63056
63863
|
var DEFAULT_PROMPT_TEMPLATES = [
|
|
@@ -63113,14 +63920,14 @@ async function generateSamples(options) {
|
|
|
63113
63920
|
}
|
|
63114
63921
|
const samples = [];
|
|
63115
63922
|
const ext = extForFramework(framework);
|
|
63116
|
-
const dir = (0,
|
|
63117
|
-
(0,
|
|
63923
|
+
const dir = (0, import_node_path51.join)(outputDir, framework, componentType);
|
|
63924
|
+
(0, import_node_fs37.mkdirSync)(dir, { recursive: true });
|
|
63118
63925
|
for (let i = 1; i <= count; i += 1) {
|
|
63119
63926
|
const raw = await provider.generateSample(renderPrompt(template), { temperature });
|
|
63120
63927
|
const code = extractCodeFromMarkdown(raw);
|
|
63121
63928
|
const fileName = `sample-${i}${ext}`;
|
|
63122
|
-
const filePath = (0,
|
|
63123
|
-
(0,
|
|
63929
|
+
const filePath = (0, import_node_path51.join)(dir, fileName);
|
|
63930
|
+
(0, import_node_fs37.writeFileSync)(filePath, code, "utf8");
|
|
63124
63931
|
const sample = {
|
|
63125
63932
|
filePath,
|
|
63126
63933
|
framework,
|
|
@@ -63131,8 +63938,8 @@ async function generateSamples(options) {
|
|
|
63131
63938
|
};
|
|
63132
63939
|
samples.push(sample);
|
|
63133
63940
|
}
|
|
63134
|
-
const metadataPath = (0,
|
|
63135
|
-
(0,
|
|
63941
|
+
const metadataPath = (0, import_node_path51.join)(dir, "metadata.json");
|
|
63942
|
+
(0, import_node_fs37.writeFileSync)(metadataPath, JSON.stringify(samples, null, 2), "utf8");
|
|
63136
63943
|
return samples;
|
|
63137
63944
|
}
|
|
63138
63945
|
|
|
@@ -63338,46 +64145,46 @@ function slugify(value) {
|
|
|
63338
64145
|
|
|
63339
64146
|
// src/research/calibrator.ts
|
|
63340
64147
|
var import_node_child_process2 = require("child_process");
|
|
63341
|
-
var
|
|
63342
|
-
var
|
|
64148
|
+
var import_node_fs38 = require("fs");
|
|
64149
|
+
var import_node_path53 = require("path");
|
|
63343
64150
|
|
|
63344
64151
|
// src/corpus-paths.ts
|
|
63345
|
-
var
|
|
64152
|
+
var import_node_path52 = require("path");
|
|
63346
64153
|
var CORPUS_ROOT = process.env["SLOPBRICK_CORPUS_DIR"] ?? "/Users/cheng/corpus-expansion";
|
|
63347
|
-
var POSITIVE_DIR = (0,
|
|
63348
|
-
var NEGATIVE_DIR = (0,
|
|
63349
|
-
var FILELISTS_DIR = (0,
|
|
64154
|
+
var POSITIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "positive");
|
|
64155
|
+
var NEGATIVE_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "negative");
|
|
64156
|
+
var FILELISTS_DIR = (0, import_node_path52.join)(CORPUS_ROOT, "filelists");
|
|
63350
64157
|
|
|
63351
64158
|
// src/research/calibrator.ts
|
|
63352
64159
|
var DEFAULT_POSITIVE = POSITIVE_DIR;
|
|
63353
64160
|
var DEFAULT_NEGATIVE = NEGATIVE_DIR;
|
|
63354
64161
|
function buildFileList(dir, extensions) {
|
|
63355
|
-
const tmpList = (0,
|
|
64162
|
+
const tmpList = (0, import_node_path53.join)("/tmp", `cal-build-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`);
|
|
63356
64163
|
const expr = extensions.map((e) => `-name '*.${e}'`).join(" -o ");
|
|
63357
64164
|
(0, import_node_child_process2.execFileSync)("bash", ["-c", `find ${dir} -maxdepth 8 -type f \\( ${expr} \\) -print0 | xargs -0 realpath > ${tmpList}`]);
|
|
63358
|
-
const out = (0,
|
|
64165
|
+
const out = (0, import_node_fs38.readFileSync)(tmpList, "utf8");
|
|
63359
64166
|
(0, import_node_child_process2.execFileSync)("rm", ["-f", tmpList]);
|
|
63360
64167
|
return out.trim().split("\n").filter(Boolean);
|
|
63361
64168
|
}
|
|
63362
64169
|
function runScan2(fileListPath) {
|
|
63363
|
-
const files = (0,
|
|
64170
|
+
const files = (0, import_node_fs38.readFileSync)(fileListPath, "utf8").trim().split("\n").filter(Boolean);
|
|
63364
64171
|
const CHUNK = 600;
|
|
63365
64172
|
const ruleFires = /* @__PURE__ */ new Map();
|
|
63366
64173
|
const uniqueFilesPerRule = /* @__PURE__ */ new Map();
|
|
63367
64174
|
let fileCount = 0;
|
|
63368
|
-
const tmpOut = (0,
|
|
64175
|
+
const tmpOut = (0, import_node_path53.join)("/tmp", `calibrate-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
63369
64176
|
for (let i = 0; i < files.length; i += CHUNK) {
|
|
63370
64177
|
const chunk = files.slice(i, i + CHUNK);
|
|
63371
64178
|
try {
|
|
63372
64179
|
(0, import_node_child_process2.execFileSync)(
|
|
63373
64180
|
"node",
|
|
63374
|
-
[(0,
|
|
64181
|
+
[(0, import_node_path53.join)(process.cwd(), "bin", "slopbrick.js"), "scan", ...chunk, "--json", tmpOut, "--no-telemetry", "--quiet"],
|
|
63375
64182
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
|
|
63376
64183
|
);
|
|
63377
64184
|
} catch {
|
|
63378
64185
|
}
|
|
63379
|
-
if (!(0,
|
|
63380
|
-
const report = JSON.parse((0,
|
|
64186
|
+
if (!(0, import_node_fs38.existsSync)(tmpOut)) continue;
|
|
64187
|
+
const report = JSON.parse((0, import_node_fs38.readFileSync)(tmpOut, "utf8"));
|
|
63381
64188
|
fileCount += report.fileCount;
|
|
63382
64189
|
for (const issue of report.issues) {
|
|
63383
64190
|
ruleFires.set(issue.ruleId, (ruleFires.get(issue.ruleId) ?? 0) + 1);
|
|
@@ -63405,16 +64212,16 @@ function classify(posFiles, negFiles) {
|
|
|
63405
64212
|
async function calibrate(cwd, options = {}) {
|
|
63406
64213
|
const positiveDir = options.positiveDir ?? DEFAULT_POSITIVE;
|
|
63407
64214
|
const negativeDir = options.negativeDir ?? DEFAULT_NEGATIVE;
|
|
63408
|
-
if (!(0,
|
|
63409
|
-
if (!(0,
|
|
64215
|
+
if (!(0, import_node_fs38.existsSync)(positiveDir)) throw new Error(`Positive corpus not found: ${positiveDir}`);
|
|
64216
|
+
if (!(0, import_node_fs38.existsSync)(negativeDir)) throw new Error(`Negative corpus not found: ${negativeDir}`);
|
|
63410
64217
|
const positiveFiles = buildFileList(positiveDir, ["tsx", "ts", "jsx", "js"]);
|
|
63411
64218
|
const negativeFiles = buildFileList(negativeDir, ["tsx", "ts"]);
|
|
63412
64219
|
const posSample = options.positiveLimit ? positiveFiles.slice(0, options.positiveLimit) : positiveFiles;
|
|
63413
64220
|
const negSample = options.negativeLimit ? negativeFiles.slice(0, options.negativeLimit) : negativeFiles;
|
|
63414
|
-
const posListPath = (0,
|
|
63415
|
-
const negListPath = (0,
|
|
63416
|
-
(0,
|
|
63417
|
-
(0,
|
|
64221
|
+
const posListPath = (0, import_node_path53.join)("/tmp", `cal-pos-${Date.now()}.txt`);
|
|
64222
|
+
const negListPath = (0, import_node_path53.join)("/tmp", `cal-neg-${Date.now()}.txt`);
|
|
64223
|
+
(0, import_node_fs38.writeFileSync)(posListPath, posSample.join("\n"));
|
|
64224
|
+
(0, import_node_fs38.writeFileSync)(negListPath, negSample.join("\n"));
|
|
63418
64225
|
const builtins = await Promise.resolve().then(() => (init_builtins(), builtins_exports));
|
|
63419
64226
|
const builtinRules2 = builtins.builtinRules ?? [];
|
|
63420
64227
|
const metaById = /* @__PURE__ */ new Map();
|
|
@@ -63528,9 +64335,9 @@ function registerCalibrate(program) {
|
|
|
63528
64335
|
positiveLimit: cmdOptions.positiveLimit,
|
|
63529
64336
|
negativeLimit: cmdOptions.negativeLimit
|
|
63530
64337
|
});
|
|
63531
|
-
const outputPath = cmdOptions.output ? (0,
|
|
63532
|
-
(0,
|
|
63533
|
-
(0,
|
|
64338
|
+
const outputPath = cmdOptions.output ? (0, import_node_path54.resolve)(cwd, cmdOptions.output) : (0, import_node_path54.resolve)(cwd, "corpus", "calibration-empirical.md");
|
|
64339
|
+
(0, import_node_fs39.mkdirSync)((0, import_node_path54.dirname)(outputPath), { recursive: true });
|
|
64340
|
+
(0, import_node_fs39.writeFileSync)(outputPath, reportToMarkdown(report), "utf8");
|
|
63534
64341
|
logger.info(
|
|
63535
64342
|
"Calibrated " + report.rules.length + " rules across " + report.positiveFileCount + " positive + " + report.negativeFileCount + " negative files."
|
|
63536
64343
|
);
|
|
@@ -63667,15 +64474,16 @@ function registerTrend(program) {
|
|
|
63667
64474
|
}
|
|
63668
64475
|
|
|
63669
64476
|
// src/cli/commands/drift.ts
|
|
63670
|
-
var
|
|
64477
|
+
var import_node_path56 = require("path");
|
|
63671
64478
|
init_logger();
|
|
63672
64479
|
init_scan2();
|
|
63673
64480
|
|
|
63674
64481
|
// src/cli/drift.ts
|
|
63675
|
-
var
|
|
63676
|
-
var
|
|
64482
|
+
var import_node_fs40 = require("fs");
|
|
64483
|
+
var import_node_path55 = require("path");
|
|
63677
64484
|
init_discover();
|
|
63678
64485
|
init_patterns();
|
|
64486
|
+
init_telemetry();
|
|
63679
64487
|
async function runDrift(cwd, config, options = {}) {
|
|
63680
64488
|
const maxFiles = options.maxFiles ?? 1e3;
|
|
63681
64489
|
const allFiles = await discoverFiles(cwd, config);
|
|
@@ -63686,7 +64494,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63686
64494
|
for (const absPath of limited) {
|
|
63687
64495
|
let source;
|
|
63688
64496
|
try {
|
|
63689
|
-
source = (0,
|
|
64497
|
+
source = (0, import_node_fs40.readFileSync)(absPath, "utf-8");
|
|
63690
64498
|
} catch {
|
|
63691
64499
|
continue;
|
|
63692
64500
|
}
|
|
@@ -63697,7 +64505,7 @@ async function runDrift(cwd, config, options = {}) {
|
|
|
63697
64505
|
byCategory[v.category] = (byCategory[v.category] ?? 0) + 1;
|
|
63698
64506
|
byFile.push({
|
|
63699
64507
|
file: absPath,
|
|
63700
|
-
relPath: (0,
|
|
64508
|
+
relPath: (0, import_node_path55.relative)(cwd, absPath),
|
|
63701
64509
|
category: v.category,
|
|
63702
64510
|
import: v.import,
|
|
63703
64511
|
declared: v.declared,
|
|
@@ -63783,38 +64591,229 @@ function formatDrift(result, opts = {}) {
|
|
|
63783
64591
|
function driftExitCode(result) {
|
|
63784
64592
|
return result.totalViolations > 0 ? 1 : 0;
|
|
63785
64593
|
}
|
|
64594
|
+
async function runDriftOverTime(cwd, config, options) {
|
|
64595
|
+
const payloads = readTelemetry(cwd);
|
|
64596
|
+
const sortedAsc = [...payloads].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
64597
|
+
const withInventory = sortedAsc.filter((p) => p.inventory !== void 0);
|
|
64598
|
+
const baselineSource = options.since === "baseline" ? "baseline" : "since";
|
|
64599
|
+
let baseline;
|
|
64600
|
+
if (baselineSource === "baseline") {
|
|
64601
|
+
baseline = withInventory[0];
|
|
64602
|
+
} else {
|
|
64603
|
+
baseline = withInventory.find((p) => p.timestamp >= options.since);
|
|
64604
|
+
}
|
|
64605
|
+
const current = withInventory.at(-1);
|
|
64606
|
+
if (!baseline || !current) {
|
|
64607
|
+
return {
|
|
64608
|
+
scannedFiles: 0,
|
|
64609
|
+
filesWithViolations: 0,
|
|
64610
|
+
totalViolations: 0,
|
|
64611
|
+
byCategory: {},
|
|
64612
|
+
byFile: [],
|
|
64613
|
+
conventionSource: deriveSource(config.constitution),
|
|
64614
|
+
constitution: config.constitution,
|
|
64615
|
+
introduced: [],
|
|
64616
|
+
removed: [],
|
|
64617
|
+
introducedUndeclared: [],
|
|
64618
|
+
snapshotsConsidered: withInventory.length,
|
|
64619
|
+
driftScore: 0,
|
|
64620
|
+
baselineAt: baseline?.timestamp ?? "",
|
|
64621
|
+
currentAt: current?.timestamp ?? "",
|
|
64622
|
+
baselineSource
|
|
64623
|
+
};
|
|
64624
|
+
}
|
|
64625
|
+
const baselineNames = flattenPatternNames(baseline.inventory.patternNames);
|
|
64626
|
+
const currentNames = flattenPatternNames(current.inventory.patternNames);
|
|
64627
|
+
const baselineSet = new Set(baselineNames.map((p) => `${p.category}\0${p.name}`));
|
|
64628
|
+
const currentSet = new Set(currentNames.map((p) => `${p.category}\0${p.name}`));
|
|
64629
|
+
const introduced = [];
|
|
64630
|
+
const removed = [];
|
|
64631
|
+
for (const p of currentNames) {
|
|
64632
|
+
const key = `${p.category}\0${p.name}`;
|
|
64633
|
+
if (!baselineSet.has(key)) introduced.push(p);
|
|
64634
|
+
}
|
|
64635
|
+
for (const p of baselineNames) {
|
|
64636
|
+
const key = `${p.category}\0${p.name}`;
|
|
64637
|
+
if (!currentSet.has(key)) removed.push(p);
|
|
64638
|
+
}
|
|
64639
|
+
introduced.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64640
|
+
removed.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64641
|
+
const declared = collectDeclaredNames(config.constitution);
|
|
64642
|
+
const introducedUndeclared = introduced.filter((p) => !declared.has(p.name.toLowerCase()));
|
|
64643
|
+
const baselineTotal = Math.max(baselineSet.size, 1);
|
|
64644
|
+
const driftScore = Math.min(
|
|
64645
|
+
100,
|
|
64646
|
+
Math.round((introduced.length + removed.length) / baselineTotal * 100)
|
|
64647
|
+
);
|
|
64648
|
+
return {
|
|
64649
|
+
scannedFiles: current.inventory.scannedFiles,
|
|
64650
|
+
filesWithViolations: 0,
|
|
64651
|
+
totalViolations: 0,
|
|
64652
|
+
byCategory: {},
|
|
64653
|
+
byFile: [],
|
|
64654
|
+
conventionSource: deriveSource(config.constitution),
|
|
64655
|
+
constitution: config.constitution,
|
|
64656
|
+
introduced,
|
|
64657
|
+
removed,
|
|
64658
|
+
introducedUndeclared,
|
|
64659
|
+
snapshotsConsidered: withInventory.length,
|
|
64660
|
+
driftScore,
|
|
64661
|
+
baselineAt: baseline.timestamp,
|
|
64662
|
+
currentAt: current.timestamp,
|
|
64663
|
+
baselineSource
|
|
64664
|
+
};
|
|
64665
|
+
}
|
|
64666
|
+
function flattenPatternNames(patternNames) {
|
|
64667
|
+
const out = [];
|
|
64668
|
+
for (const [category, names] of Object.entries(patternNames)) {
|
|
64669
|
+
for (const name of names) {
|
|
64670
|
+
out.push({ category, name });
|
|
64671
|
+
}
|
|
64672
|
+
}
|
|
64673
|
+
out.sort((a, b) => a.category.localeCompare(b.category) || a.name.localeCompare(b.name));
|
|
64674
|
+
return out;
|
|
64675
|
+
}
|
|
64676
|
+
function collectDeclaredNames(constitution) {
|
|
64677
|
+
const out = /* @__PURE__ */ new Set();
|
|
64678
|
+
if (!constitution) return out;
|
|
64679
|
+
const fields = ["stateManagement", "dataFetching", "uiLibrary", "forms", "styling", "routing"];
|
|
64680
|
+
for (const f of fields) {
|
|
64681
|
+
for (const v of constitution[f] ?? []) {
|
|
64682
|
+
out.add(v.toLowerCase());
|
|
64683
|
+
}
|
|
64684
|
+
}
|
|
64685
|
+
for (const list of Object.values(constitution.custom ?? {})) {
|
|
64686
|
+
for (const v of list) {
|
|
64687
|
+
out.add(v.toLowerCase());
|
|
64688
|
+
}
|
|
64689
|
+
}
|
|
64690
|
+
for (const v of constitution.forbidden ?? []) {
|
|
64691
|
+
out.add(v.toLowerCase());
|
|
64692
|
+
}
|
|
64693
|
+
return out;
|
|
64694
|
+
}
|
|
64695
|
+
function formatDriftOverTime(result) {
|
|
64696
|
+
const lines = [];
|
|
64697
|
+
lines.push("Temporal drift report");
|
|
64698
|
+
lines.push("");
|
|
64699
|
+
if (result.snapshotsConsidered === 0 || !result.baselineAt || !result.currentAt) {
|
|
64700
|
+
lines.push(" No historical telemetry found at .slopbrick/flywheel/scans.jsonl.");
|
|
64701
|
+
lines.push(" Run a few scans with `slopbrick scan` first; the temporal drift");
|
|
64702
|
+
lines.push(" detector needs \u2265 2 scan payloads to compute a baseline window.");
|
|
64703
|
+
return lines.join("\n");
|
|
64704
|
+
}
|
|
64705
|
+
const sinceLabel = result.baselineSource === "baseline" ? `baseline (oldest scan)` : `since ${result.baselineAt}`;
|
|
64706
|
+
lines.push(` Window: ${sinceLabel} \u2192 ${result.currentAt}`);
|
|
64707
|
+
lines.push(` Snapshots considered: ${result.snapshotsConsidered}`);
|
|
64708
|
+
lines.push(` Drift score: ${result.driftScore} / 100 (informational)`);
|
|
64709
|
+
lines.push("");
|
|
64710
|
+
lines.push(` Patterns introduced: ${result.introduced.length}`);
|
|
64711
|
+
for (const p of result.introduced) {
|
|
64712
|
+
lines.push(` + ${p.category}/${p.name}`);
|
|
64713
|
+
}
|
|
64714
|
+
if (result.introducedUndeclared.length > 0) {
|
|
64715
|
+
lines.push("");
|
|
64716
|
+
lines.push(
|
|
64717
|
+
` Patterns introduced but not in declared constitution: ${result.introducedUndeclared.length}`
|
|
64718
|
+
);
|
|
64719
|
+
for (const p of result.introducedUndeclared) {
|
|
64720
|
+
lines.push(` ! ${p.category}/${p.name}`);
|
|
64721
|
+
}
|
|
64722
|
+
}
|
|
64723
|
+
lines.push("");
|
|
64724
|
+
lines.push(` Patterns removed: ${result.removed.length}`);
|
|
64725
|
+
for (const p of result.removed) {
|
|
64726
|
+
lines.push(` - ${p.category}/${p.name}`);
|
|
64727
|
+
}
|
|
64728
|
+
lines.push("");
|
|
64729
|
+
if (result.introducedUndeclared.length > 0) {
|
|
64730
|
+
lines.push(
|
|
64731
|
+
" Tip: add undeclared patterns to slopbrick.config.mjs#constitution to"
|
|
64732
|
+
);
|
|
64733
|
+
lines.push(" promote them into the declared set on the next scan.");
|
|
64734
|
+
} else if (result.introduced.length === 0 && result.removed.length === 0) {
|
|
64735
|
+
lines.push(" \u2713 No pattern churn since the baseline window.");
|
|
64736
|
+
}
|
|
64737
|
+
return lines.join("\n");
|
|
64738
|
+
}
|
|
64739
|
+
function driftOverTimeExitCode(result) {
|
|
64740
|
+
return result.introducedUndeclared.length > 0 ? 1 : 0;
|
|
64741
|
+
}
|
|
64742
|
+
|
|
64743
|
+
// src/cli/commands/drift.ts
|
|
64744
|
+
init_load();
|
|
64745
|
+
|
|
64746
|
+
// src/cli/commands/_shared.ts
|
|
64747
|
+
var import_commander2 = require("commander");
|
|
64748
|
+
init_logger();
|
|
64749
|
+
function setExitOverride(program) {
|
|
64750
|
+
program.exitOverride();
|
|
64751
|
+
}
|
|
64752
|
+
async function dispatch(program, runFn) {
|
|
64753
|
+
try {
|
|
64754
|
+
await runFn();
|
|
64755
|
+
} catch (err) {
|
|
64756
|
+
if (err instanceof import_commander2.CommanderError) {
|
|
64757
|
+
if (err.code !== "commander.helpDisplayed" && err.code !== "commander.help") {
|
|
64758
|
+
logger.error(err.message);
|
|
64759
|
+
}
|
|
64760
|
+
process.exit(err.exitCode);
|
|
64761
|
+
return;
|
|
64762
|
+
}
|
|
64763
|
+
throw err;
|
|
64764
|
+
}
|
|
64765
|
+
}
|
|
64766
|
+
function withExitCode(result, compute, message) {
|
|
64767
|
+
const code = compute(result);
|
|
64768
|
+
if (code === 0) return;
|
|
64769
|
+
throw new import_commander2.CommanderError(code, "slopbrick.exit", message);
|
|
64770
|
+
}
|
|
63786
64771
|
|
|
63787
64772
|
// src/cli/commands/drift.ts
|
|
63788
64773
|
function registerDrift(program) {
|
|
63789
64774
|
program.command("drift").description(
|
|
63790
64775
|
"detect imports that violate declared constitution (state, data-fetching, UI, forms, styling, routing) or import forbidden packages"
|
|
63791
|
-
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).
|
|
64776
|
+
).option("--format <pretty|json>", "output format", "pretty").option("--max-files <n>", "cap on files scanned", parseCount, 1e3).option(
|
|
64777
|
+
"--temporal-since <date>",
|
|
64778
|
+
`temporal drift since the given date (ISO-8601, or the literal string \`baseline\` to use the oldest scan in scans.jsonl)`
|
|
64779
|
+
).action(
|
|
63792
64780
|
async (cmdOptions, command) => {
|
|
63793
|
-
|
|
63794
|
-
|
|
63795
|
-
|
|
63796
|
-
|
|
63797
|
-
|
|
63798
|
-
const
|
|
63799
|
-
|
|
63800
|
-
|
|
63801
|
-
|
|
63802
|
-
|
|
63803
|
-
|
|
63804
|
-
|
|
64781
|
+
const options = command.optsWithGlobals();
|
|
64782
|
+
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64783
|
+
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64784
|
+
const cwd = (0, import_node_path56.resolve)(options.workspace ?? process.cwd());
|
|
64785
|
+
if (cmdOptions.temporalSince !== void 0) {
|
|
64786
|
+
const temporalSince = cmdOptions.temporalSince.trim();
|
|
64787
|
+
if (temporalSince.length === 0) {
|
|
64788
|
+
throw new Error("--temporal-since expects a non-empty ISO date or `baseline`");
|
|
64789
|
+
}
|
|
64790
|
+
const sinceArg = temporalSince === "baseline" ? "baseline" : temporalSince;
|
|
64791
|
+
const config2 = await loadConfig(cwd);
|
|
64792
|
+
const result2 = await runDriftOverTime(cwd, config2, { since: sinceArg });
|
|
64793
|
+
logger.info(formatDriftOverTime(result2));
|
|
64794
|
+
withExitCode(
|
|
64795
|
+
result2,
|
|
64796
|
+
driftOverTimeExitCode,
|
|
64797
|
+
`drift --temporal-since: ${result2.introducedUndeclared.length} undeclared patterns introduced`
|
|
64798
|
+
);
|
|
64799
|
+
return;
|
|
63805
64800
|
}
|
|
64801
|
+
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64802
|
+
const result = await runDrift(cwd, config, { maxFiles: cmdOptions.maxFiles });
|
|
64803
|
+
logger.info(formatDrift(result, { json: format === "json" }));
|
|
64804
|
+
withExitCode(result, driftExitCode, `drift: ${result.totalViolations} violations`);
|
|
63806
64805
|
}
|
|
63807
64806
|
);
|
|
63808
64807
|
}
|
|
63809
64808
|
|
|
63810
64809
|
// src/cli/commands/pr.ts
|
|
63811
|
-
var
|
|
64810
|
+
var import_node_path58 = require("path");
|
|
63812
64811
|
init_logger();
|
|
63813
64812
|
init_scan2();
|
|
63814
64813
|
|
|
63815
64814
|
// src/cli/pr.ts
|
|
63816
|
-
var
|
|
63817
|
-
var
|
|
64815
|
+
var import_node_fs41 = require("fs");
|
|
64816
|
+
var import_node_path57 = require("path");
|
|
63818
64817
|
var import_node_child_process3 = require("child_process");
|
|
63819
64818
|
var import_node_util2 = require("util");
|
|
63820
64819
|
var import_minimatch3 = require("minimatch");
|
|
@@ -63860,10 +64859,10 @@ async function discoverPrFiles(cwd, config, base, head, maxFiles) {
|
|
|
63860
64859
|
if (gitFiles.length === 0) return [];
|
|
63861
64860
|
const sourceFiles = [];
|
|
63862
64861
|
for (const relOrAbs of gitFiles) {
|
|
63863
|
-
const abs = (0,
|
|
63864
|
-
const ext = (0,
|
|
64862
|
+
const abs = (0, import_node_path57.resolve)(cwd, relOrAbs);
|
|
64863
|
+
const ext = (0, import_node_path57.extname)(abs).toLowerCase();
|
|
63865
64864
|
if (!PR_EXTENSIONS.has(ext)) continue;
|
|
63866
|
-
const rel = (0,
|
|
64865
|
+
const rel = (0, import_node_path57.relative)(cwd, abs).split("\\").join("/");
|
|
63867
64866
|
if (config.include.length > 0 && !config.include.some((pattern) => (0, import_minimatch3.minimatch)(rel, pattern))) {
|
|
63868
64867
|
continue;
|
|
63869
64868
|
}
|
|
@@ -63891,7 +64890,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
63891
64890
|
for (const absPath of candidates) {
|
|
63892
64891
|
let source;
|
|
63893
64892
|
try {
|
|
63894
|
-
source = (0,
|
|
64893
|
+
source = (0, import_node_fs41.readFileSync)(absPath, "utf-8");
|
|
63895
64894
|
} catch {
|
|
63896
64895
|
continue;
|
|
63897
64896
|
}
|
|
@@ -63934,7 +64933,7 @@ async function runPrScan(cwd, config, options = {}) {
|
|
|
63934
64933
|
totalScore += fileScore;
|
|
63935
64934
|
files.push({
|
|
63936
64935
|
file: absPath,
|
|
63937
|
-
relPath: (0,
|
|
64936
|
+
relPath: (0, import_node_path57.relative)(cwd, absPath).split("\\").join("/"),
|
|
63938
64937
|
score: fileScore,
|
|
63939
64938
|
issueCount: issues.length,
|
|
63940
64939
|
constitutionViolationCount: constitutionViolations.length,
|
|
@@ -64066,20 +65065,135 @@ function formatPrMarkdown(result) {
|
|
|
64066
65065
|
function prExitCode(result) {
|
|
64067
65066
|
return result.totalScore > result.threshold ? 1 : 0;
|
|
64068
65067
|
}
|
|
65068
|
+
function parseUnifiedDiffPaths(diff) {
|
|
65069
|
+
const paths = [];
|
|
65070
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65071
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
65072
|
+
const m = /^\+\+\+\s+(?:b\/)?(\S.*)$/.exec(line);
|
|
65073
|
+
if (!m) continue;
|
|
65074
|
+
const path = m[1];
|
|
65075
|
+
if (path === "/dev/null" || path === "dev/null") continue;
|
|
65076
|
+
if (seen.has(path)) continue;
|
|
65077
|
+
seen.add(path);
|
|
65078
|
+
paths.push(path);
|
|
65079
|
+
}
|
|
65080
|
+
return paths;
|
|
65081
|
+
}
|
|
65082
|
+
async function runPrScanFromDiffFile(cwd, config, diffPath, options = {}) {
|
|
65083
|
+
const threshold = options.threshold ?? config.prScoreThreshold ?? 20;
|
|
65084
|
+
const maxFiles = options.maxFiles ?? 500;
|
|
65085
|
+
const { existsSync: existsSync32, readFileSync: readFileSync42 } = await import("fs");
|
|
65086
|
+
const { isAbsolute: isAbsolute4, resolve: resolvePath } = await import("path");
|
|
65087
|
+
if (!existsSync32(diffPath)) {
|
|
65088
|
+
throw new Error(`Diff file not found: ${diffPath}`);
|
|
65089
|
+
}
|
|
65090
|
+
const diffAbs = isAbsolute4(diffPath) ? diffPath : resolvePath(cwd, diffPath);
|
|
65091
|
+
const diff = readFileSync42(diffAbs, "utf-8");
|
|
65092
|
+
const changedPaths = parseUnifiedDiffPaths(diff);
|
|
65093
|
+
if (changedPaths.length === 0) {
|
|
65094
|
+
return emptyPrResult(threshold);
|
|
65095
|
+
}
|
|
65096
|
+
return scoreChangedPaths(cwd, config, changedPaths, maxFiles, threshold);
|
|
65097
|
+
}
|
|
65098
|
+
async function scoreChangedPaths(cwd, config, paths, maxFiles, threshold) {
|
|
65099
|
+
const { existsSync: existsSync32, statSync: statSync8 } = await import("fs");
|
|
65100
|
+
const { resolve: resolvePath } = await import("path");
|
|
65101
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
65102
|
+
const { scanFile: scanFile2 } = await Promise.resolve().then(() => (init_worker(), worker_exports));
|
|
65103
|
+
const limited = paths.slice(0, maxFiles);
|
|
65104
|
+
const files = [];
|
|
65105
|
+
let totalScore = 0;
|
|
65106
|
+
const byCategory = {};
|
|
65107
|
+
const bySeverity = { low: 0, medium: 0, high: 0 };
|
|
65108
|
+
let constitutionViolations = 0;
|
|
65109
|
+
for (const rel of limited) {
|
|
65110
|
+
const abs = resolvePath(cwd, rel);
|
|
65111
|
+
if (!existsSync32(abs) || !statSync8(abs).isFile()) continue;
|
|
65112
|
+
let source;
|
|
65113
|
+
try {
|
|
65114
|
+
source = await readFile4(abs, "utf-8");
|
|
65115
|
+
} catch {
|
|
65116
|
+
continue;
|
|
65117
|
+
}
|
|
65118
|
+
let result;
|
|
65119
|
+
try {
|
|
65120
|
+
result = await scanFile2(abs, config, void 0, cwd);
|
|
65121
|
+
} catch {
|
|
65122
|
+
continue;
|
|
65123
|
+
}
|
|
65124
|
+
if (result.parseError) continue;
|
|
65125
|
+
const issues = result.issues;
|
|
65126
|
+
let slopPoints = 0;
|
|
65127
|
+
const issueEntries = [];
|
|
65128
|
+
for (const issue of issues) {
|
|
65129
|
+
slopPoints += SEVERITY_WEIGHTS[issue.severity] ?? 0;
|
|
65130
|
+
byCategory[issue.category] = (byCategory[issue.category] ?? 0) + 1;
|
|
65131
|
+
bySeverity[issue.severity] = (bySeverity[issue.severity] ?? 0) + 1;
|
|
65132
|
+
issueEntries.push({
|
|
65133
|
+
ruleId: issue.ruleId,
|
|
65134
|
+
severity: issue.severity,
|
|
65135
|
+
line: issue.line,
|
|
65136
|
+
category: issue.category,
|
|
65137
|
+
message: issue.message
|
|
65138
|
+
});
|
|
65139
|
+
}
|
|
65140
|
+
totalScore += slopPoints;
|
|
65141
|
+
files.push({
|
|
65142
|
+
file: abs,
|
|
65143
|
+
relPath: rel,
|
|
65144
|
+
score: slopPoints,
|
|
65145
|
+
issueCount: issueEntries.length,
|
|
65146
|
+
constitutionViolationCount: 0,
|
|
65147
|
+
slopPoints,
|
|
65148
|
+
issues: issueEntries,
|
|
65149
|
+
constitutionViolations: []
|
|
65150
|
+
});
|
|
65151
|
+
}
|
|
65152
|
+
return {
|
|
65153
|
+
base: "diff",
|
|
65154
|
+
head: "diff",
|
|
65155
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65156
|
+
passed: totalScore <= threshold,
|
|
65157
|
+
threshold,
|
|
65158
|
+
totalScore,
|
|
65159
|
+
filesChanged: files.length,
|
|
65160
|
+
byCategory,
|
|
65161
|
+
bySeverity,
|
|
65162
|
+
files
|
|
65163
|
+
};
|
|
65164
|
+
}
|
|
65165
|
+
function emptyPrResult(threshold) {
|
|
65166
|
+
return {
|
|
65167
|
+
base: "diff",
|
|
65168
|
+
head: "diff",
|
|
65169
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65170
|
+
passed: true,
|
|
65171
|
+
threshold,
|
|
65172
|
+
totalScore: 0,
|
|
65173
|
+
filesChanged: 0,
|
|
65174
|
+
byCategory: {},
|
|
65175
|
+
bySeverity: { low: 0, medium: 0, high: 0 },
|
|
65176
|
+
files: []
|
|
65177
|
+
};
|
|
65178
|
+
}
|
|
64069
65179
|
|
|
64070
65180
|
// src/cli/commands/pr.ts
|
|
64071
65181
|
function registerPr(program) {
|
|
64072
65182
|
program.command("pr").description(
|
|
64073
65183
|
"Score the slop cost of the current PR. Compares --base (default: main) to --head (default: HEAD), scans only changed source files, returns a single weighted score. Exits 1 when score > --threshold (default 20)."
|
|
64074
|
-
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).action(
|
|
65184
|
+
).option("--base <ref>", "base git ref to diff from", "main").option("--head <ref>", "head git ref to diff to", "HEAD").option("--format <text|json|markdown>", "output format", "text").option("--threshold <n>", "score threshold (overrides config.prScoreThreshold)", parseThreshold).option("--max-files <n>", "cap on files scanned", parseCount, 500).option("--diff <file>", "unified-diff file (overrides --base/--head; meant for CI/Action use)").action(
|
|
64075
65185
|
async (cmdOptions, command) => {
|
|
64076
65186
|
try {
|
|
64077
65187
|
const options = command.optsWithGlobals();
|
|
64078
|
-
const cwd = (0,
|
|
65188
|
+
const cwd = (0, import_node_path58.resolve)(options.workspace ?? process.cwd());
|
|
64079
65189
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64080
65190
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64081
65191
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64082
|
-
const result = await
|
|
65192
|
+
const result = cmdOptions.diff ? await runPrScanFromDiffFile(cwd, config, cmdOptions.diff, {
|
|
65193
|
+
format,
|
|
65194
|
+
threshold: cmdOptions.threshold,
|
|
65195
|
+
maxFiles: cmdOptions.maxFiles
|
|
65196
|
+
}) : await runPrScan(cwd, config, {
|
|
64083
65197
|
base: cmdOptions.base,
|
|
64084
65198
|
head: cmdOptions.head,
|
|
64085
65199
|
format,
|
|
@@ -64097,7 +65211,7 @@ function registerPr(program) {
|
|
|
64097
65211
|
}
|
|
64098
65212
|
|
|
64099
65213
|
// src/cli/commands/security.ts
|
|
64100
|
-
var
|
|
65214
|
+
var import_node_path59 = require("path");
|
|
64101
65215
|
init_logger();
|
|
64102
65216
|
init_scan2();
|
|
64103
65217
|
init_ai_security_risk();
|
|
@@ -64110,7 +65224,7 @@ function registerSecurity(program) {
|
|
|
64110
65224
|
const options = command.optsWithGlobals();
|
|
64111
65225
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64112
65226
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64113
|
-
const cwd = (0,
|
|
65227
|
+
const cwd = (0, import_node_path59.resolve)(options.workspace ?? process.cwd());
|
|
64114
65228
|
const { report } = await runScan({ ...options, workspace: cwd });
|
|
64115
65229
|
const securityIssues = report.issues.filter((i) => i.category === "security");
|
|
64116
65230
|
const { risk, findings } = computeAiSecurityRisk(securityIssues);
|
|
@@ -64156,12 +65270,12 @@ function registerSecurity(program) {
|
|
|
64156
65270
|
}
|
|
64157
65271
|
|
|
64158
65272
|
// src/cli/commands/test.ts
|
|
64159
|
-
var
|
|
65273
|
+
var import_node_path61 = require("path");
|
|
64160
65274
|
init_logger();
|
|
64161
65275
|
init_scan2();
|
|
64162
65276
|
|
|
64163
65277
|
// src/cli/test.ts
|
|
64164
|
-
var
|
|
65278
|
+
var import_node_path60 = require("path");
|
|
64165
65279
|
init_scan2();
|
|
64166
65280
|
init_test_quality();
|
|
64167
65281
|
init_logger();
|
|
@@ -64232,7 +65346,7 @@ function formatTestReport(result, opts = {}) {
|
|
|
64232
65346
|
const sortedFiles = [...byFile.keys()].sort();
|
|
64233
65347
|
for (const file of sortedFiles) {
|
|
64234
65348
|
const issues = byFile.get(file) ?? [];
|
|
64235
|
-
const rel = file.startsWith((0,
|
|
65349
|
+
const rel = file.startsWith((0, import_node_path60.resolve)(process.cwd())) ? file : file;
|
|
64236
65350
|
lines.push("");
|
|
64237
65351
|
lines.push(` ${rel}`);
|
|
64238
65352
|
for (const issue of issues.slice(0, 20)) {
|
|
@@ -64261,7 +65375,7 @@ function registerTest(program) {
|
|
|
64261
65375
|
const options = command.optsWithGlobals();
|
|
64262
65376
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64263
65377
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64264
|
-
const cwd = (0,
|
|
65378
|
+
const cwd = (0, import_node_path61.resolve)(options.workspace ?? process.cwd());
|
|
64265
65379
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64266
65380
|
const { result } = await runTestScan(cwd, config, { strict: options.strict });
|
|
64267
65381
|
logger.info(formatTestReport(result, { json: format === "json" }));
|
|
@@ -64275,7 +65389,7 @@ function registerTest(program) {
|
|
|
64275
65389
|
}
|
|
64276
65390
|
|
|
64277
65391
|
// src/cli/commands/architecture.ts
|
|
64278
|
-
var
|
|
65392
|
+
var import_node_path62 = require("path");
|
|
64279
65393
|
init_logger();
|
|
64280
65394
|
init_scan2();
|
|
64281
65395
|
init_architecture_score();
|
|
@@ -64288,7 +65402,7 @@ function registerArchitecture(program) {
|
|
|
64288
65402
|
const options = command.optsWithGlobals();
|
|
64289
65403
|
const rawFormat = options.format ?? cmdOptions.format ?? "pretty";
|
|
64290
65404
|
const format = rawFormat === "json" || rawFormat === "pretty" ? rawFormat : "pretty";
|
|
64291
|
-
const cwd = (0,
|
|
65405
|
+
const cwd = (0, import_node_path62.resolve)(options.workspace ?? process.cwd());
|
|
64292
65406
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64293
65407
|
const score = await buildArchitectureScore(cwd, config, cmdOptions.maxFiles);
|
|
64294
65408
|
const out = format === "json" ? JSON.stringify(score, null, 2) : formatArchitectureScore(score);
|
|
@@ -64303,13 +65417,13 @@ function registerArchitecture(program) {
|
|
|
64303
65417
|
}
|
|
64304
65418
|
|
|
64305
65419
|
// src/cli/commands/business-logic.ts
|
|
64306
|
-
var
|
|
65420
|
+
var import_node_path64 = require("path");
|
|
64307
65421
|
init_logger();
|
|
64308
65422
|
init_scan2();
|
|
64309
65423
|
|
|
64310
65424
|
// src/cli/business-logic.ts
|
|
64311
|
-
var
|
|
64312
|
-
var
|
|
65425
|
+
var import_node_fs42 = require("fs");
|
|
65426
|
+
var import_node_path63 = require("path");
|
|
64313
65427
|
init_discover();
|
|
64314
65428
|
init_business_logic();
|
|
64315
65429
|
async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
@@ -64321,7 +65435,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64321
65435
|
for (const absPath of limited) {
|
|
64322
65436
|
let source;
|
|
64323
65437
|
try {
|
|
64324
|
-
source = (0,
|
|
65438
|
+
source = (0, import_node_fs42.readFileSync)(absPath, "utf-8");
|
|
64325
65439
|
} catch {
|
|
64326
65440
|
continue;
|
|
64327
65441
|
}
|
|
@@ -64330,7 +65444,7 @@ async function runBusinessLogicScan(cwd, config, options = {}) {
|
|
|
64330
65444
|
for (const issue of fileIssues) {
|
|
64331
65445
|
issues.push({
|
|
64332
65446
|
...issue,
|
|
64333
|
-
filePath: (0,
|
|
65447
|
+
filePath: (0, import_node_path63.relative)(cwd, absPath) || absPath
|
|
64334
65448
|
});
|
|
64335
65449
|
}
|
|
64336
65450
|
}
|
|
@@ -64433,7 +65547,7 @@ function registerBusinessLogic(program) {
|
|
|
64433
65547
|
const options = command.optsWithGlobals();
|
|
64434
65548
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64435
65549
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64436
|
-
const cwd = (0,
|
|
65550
|
+
const cwd = (0, import_node_path64.resolve)(options.workspace ?? process.cwd());
|
|
64437
65551
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64438
65552
|
const result = await runBusinessLogicScan(cwd, config, {
|
|
64439
65553
|
maxFiles: cmdOptions.maxFiles
|
|
@@ -64449,7 +65563,7 @@ function registerBusinessLogic(program) {
|
|
|
64449
65563
|
}
|
|
64450
65564
|
|
|
64451
65565
|
// src/cli/commands/maintenance-cost.ts
|
|
64452
|
-
var
|
|
65566
|
+
var import_node_path65 = require("path");
|
|
64453
65567
|
init_logger();
|
|
64454
65568
|
init_scan2();
|
|
64455
65569
|
|
|
@@ -64575,7 +65689,7 @@ function registerMaintenanceCost(program) {
|
|
|
64575
65689
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64576
65690
|
const format = rawFormat === "json" || rawFormat === "text" ? rawFormat : "text";
|
|
64577
65691
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64578
|
-
const cwd = (0,
|
|
65692
|
+
const cwd = (0, import_node_path65.resolve)(options.workspace ?? process.cwd());
|
|
64579
65693
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64580
65694
|
const result = await runMaintenanceCostScan(cwd, config, {
|
|
64581
65695
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64592,7 +65706,7 @@ function registerMaintenanceCost(program) {
|
|
|
64592
65706
|
}
|
|
64593
65707
|
|
|
64594
65708
|
// src/cli/commands/docs.ts
|
|
64595
|
-
var
|
|
65709
|
+
var import_node_path66 = require("path");
|
|
64596
65710
|
init_logger();
|
|
64597
65711
|
init_scan2();
|
|
64598
65712
|
|
|
@@ -64719,7 +65833,7 @@ function registerDocs(program) {
|
|
|
64719
65833
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64720
65834
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64721
65835
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64722
|
-
const cwd = (0,
|
|
65836
|
+
const cwd = (0, import_node_path66.resolve)(options.workspace ?? process.cwd());
|
|
64723
65837
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64724
65838
|
const result = await runDocsScan(cwd, config, {
|
|
64725
65839
|
maxDocFiles: cmdOptions.maxFiles,
|
|
@@ -64741,7 +65855,7 @@ function registerDocs(program) {
|
|
|
64741
65855
|
}
|
|
64742
65856
|
|
|
64743
65857
|
// src/cli/commands/db.ts
|
|
64744
|
-
var
|
|
65858
|
+
var import_node_path67 = require("path");
|
|
64745
65859
|
init_logger();
|
|
64746
65860
|
init_scan2();
|
|
64747
65861
|
|
|
@@ -64862,7 +65976,7 @@ function registerDb(program) {
|
|
|
64862
65976
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
64863
65977
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
64864
65978
|
const strict = options.strict ?? cmdOptions.strict ?? false;
|
|
64865
|
-
const cwd = (0,
|
|
65979
|
+
const cwd = (0, import_node_path67.resolve)(options.workspace ?? process.cwd());
|
|
64866
65980
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
64867
65981
|
const result = await runDbScan(cwd, config, {
|
|
64868
65982
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -64884,13 +65998,13 @@ function registerDb(program) {
|
|
|
64884
65998
|
}
|
|
64885
65999
|
|
|
64886
66000
|
// src/cli/commands/patterns.ts
|
|
64887
|
-
var
|
|
66001
|
+
var import_node_path69 = require("path");
|
|
64888
66002
|
init_logger();
|
|
64889
66003
|
init_scan2();
|
|
64890
66004
|
|
|
64891
66005
|
// src/engine/patterns.ts
|
|
64892
|
-
var
|
|
64893
|
-
var
|
|
66006
|
+
var import_node_fs43 = require("fs");
|
|
66007
|
+
var import_node_path68 = require("path");
|
|
64894
66008
|
init_patterns();
|
|
64895
66009
|
init_discover();
|
|
64896
66010
|
var PATTERN_CATEGORIES = [
|
|
@@ -64925,7 +66039,7 @@ var TOAST_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Toast|Notification|Snackbar|Alert|B
|
|
|
64925
66039
|
var CARD_NAME_RE = /^(?:[A-Z][a-zA-Z0-9]+)?(Card|Tile|Chip|Badge)$/;
|
|
64926
66040
|
var API_PATH_RE2 = /(?:^|\/)(?:lib\/api|services|api-client|clients)\//;
|
|
64927
66041
|
function baseName(filePath) {
|
|
64928
|
-
return (0,
|
|
66042
|
+
return (0, import_node_path68.basename)(filePath).replace(/\.(tsx|ts|jsx|js|vue|svelte|astro)$/i, "");
|
|
64929
66043
|
}
|
|
64930
66044
|
function makeStats(patterns) {
|
|
64931
66045
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -64984,7 +66098,7 @@ function detectApiFromFiles(files, cwd) {
|
|
|
64984
66098
|
const out = [];
|
|
64985
66099
|
for (const f of files) {
|
|
64986
66100
|
if (API_PATH_RE2.test(f)) {
|
|
64987
|
-
out.push((0,
|
|
66101
|
+
out.push((0, import_node_path68.relative)(cwd, f).split("\\").join("/"));
|
|
64988
66102
|
}
|
|
64989
66103
|
}
|
|
64990
66104
|
return out;
|
|
@@ -64994,7 +66108,7 @@ function detectFormsFromFiles(files) {
|
|
|
64994
66108
|
for (const f of files) {
|
|
64995
66109
|
let source;
|
|
64996
66110
|
try {
|
|
64997
|
-
source = (0,
|
|
66111
|
+
source = (0, import_node_fs43.readFileSync)(f, "utf-8");
|
|
64998
66112
|
} catch {
|
|
64999
66113
|
continue;
|
|
65000
66114
|
}
|
|
@@ -65215,7 +66329,7 @@ function registerPatterns(program) {
|
|
|
65215
66329
|
const options = command.optsWithGlobals();
|
|
65216
66330
|
const rawFormat = options.format ?? cmdOptions.format ?? "text";
|
|
65217
66331
|
const format = rawFormat === "json" || rawFormat === "markdown" || rawFormat === "text" ? rawFormat : "text";
|
|
65218
|
-
const cwd = (0,
|
|
66332
|
+
const cwd = (0, import_node_path69.resolve)(options.workspace ?? process.cwd());
|
|
65219
66333
|
const { config } = await runScan({ ...options, workspace: cwd });
|
|
65220
66334
|
const result = await runPatternsScan(cwd, config, {
|
|
65221
66335
|
maxFiles: cmdOptions.maxFiles,
|
|
@@ -65232,8 +66346,8 @@ function registerPatterns(program) {
|
|
|
65232
66346
|
}
|
|
65233
66347
|
|
|
65234
66348
|
// src/cli/commands/research.ts
|
|
65235
|
-
var
|
|
65236
|
-
var
|
|
66349
|
+
var import_node_fs44 = require("fs");
|
|
66350
|
+
var import_node_path70 = require("path");
|
|
65237
66351
|
init_logger();
|
|
65238
66352
|
init_config2();
|
|
65239
66353
|
function registerResearch(program) {
|
|
@@ -65250,24 +66364,24 @@ function registerResearch(program) {
|
|
|
65250
66364
|
framework: cmdOptions.framework,
|
|
65251
66365
|
componentType: cmdOptions.componentType,
|
|
65252
66366
|
provider,
|
|
65253
|
-
outputDir: (0,
|
|
66367
|
+
outputDir: (0, import_node_path70.resolve)(cmdOptions.outputDir),
|
|
65254
66368
|
temperature: cmdOptions.temperature
|
|
65255
66369
|
});
|
|
65256
66370
|
logger.info(`Generated ${samples.length} samples in ${cmdOptions.outputDir}`);
|
|
65257
66371
|
});
|
|
65258
66372
|
research.command("analyze").description("analyze generated samples and report coverage").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "analysis output path", ".slopbrick/flywheel/analysis.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").action(async (cmdOptions) => {
|
|
65259
66373
|
try {
|
|
65260
|
-
const metadataPath = (0,
|
|
65261
|
-
if (!(0,
|
|
66374
|
+
const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
66375
|
+
if (!(0, import_node_fs44.existsSync)(metadataPath)) {
|
|
65262
66376
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65263
66377
|
process.exit(2);
|
|
65264
66378
|
}
|
|
65265
|
-
const samples = JSON.parse((0,
|
|
66379
|
+
const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
|
|
65266
66380
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65267
66381
|
const analysis = await analyzeSamples(samples, config);
|
|
65268
|
-
const outputPath = (0,
|
|
65269
|
-
(0,
|
|
65270
|
-
(0,
|
|
66382
|
+
const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
|
|
66383
|
+
(0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
|
|
66384
|
+
(0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(analysis, null, 2), "utf8");
|
|
65271
66385
|
logger.info(`Analyzed ${analysis.summary.total} samples; coverage: ${analysis.summary.coverage}%`);
|
|
65272
66386
|
logger.info(`Wrote analysis to ${outputPath}`);
|
|
65273
66387
|
} catch (error) {
|
|
@@ -65277,12 +66391,12 @@ function registerResearch(program) {
|
|
|
65277
66391
|
});
|
|
65278
66392
|
research.command("candidates").description("extract patterns from generated samples and emit candidate rules").requiredOption("--input-dir <path>", "directory with generated samples containing metadata.json").option("--output <path>", "output path", ".slopbrick/flywheel/rule-candidates.json").option("--config <path>", "slopbrick config path").option("--framework <name>", "framework multiplier to apply", "react").option("--min-frequency <n>", "minimum cluster frequency", parseCount, 2).option("--include-covered", "include samples already covered by AI-specific rules").action(async (cmdOptions) => {
|
|
65279
66393
|
try {
|
|
65280
|
-
const metadataPath = (0,
|
|
65281
|
-
if (!(0,
|
|
66394
|
+
const metadataPath = (0, import_node_path70.resolve)(cmdOptions.inputDir, "metadata.json");
|
|
66395
|
+
if (!(0, import_node_fs44.existsSync)(metadataPath)) {
|
|
65282
66396
|
logger.error(`No metadata.json found in ${cmdOptions.inputDir}`);
|
|
65283
66397
|
process.exit(2);
|
|
65284
66398
|
}
|
|
65285
|
-
const samples = JSON.parse((0,
|
|
66399
|
+
const samples = JSON.parse((0, import_node_fs44.readFileSync)(metadataPath, "utf8"));
|
|
65286
66400
|
const config = cmdOptions.config ? await loadConfig(cmdOptions.config) : { ...DEFAULT_CONFIG, framework: cmdOptions.framework };
|
|
65287
66401
|
const analysis = await analyzeSamples(samples, config);
|
|
65288
66402
|
const extraction = extractAndCluster(analysis.samples, {
|
|
@@ -65292,8 +66406,8 @@ function registerResearch(program) {
|
|
|
65292
66406
|
const candidates = clustersToCandidates(extraction.clusters, {
|
|
65293
66407
|
minFrequency: cmdOptions.minFrequency
|
|
65294
66408
|
});
|
|
65295
|
-
const outputPath = (0,
|
|
65296
|
-
(0,
|
|
66409
|
+
const outputPath = (0, import_node_path70.resolve)(cmdOptions.output);
|
|
66410
|
+
(0, import_node_fs44.mkdirSync)((0, import_node_path70.dirname)(outputPath), { recursive: true });
|
|
65297
66411
|
const payload = {
|
|
65298
66412
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65299
66413
|
sampleCount: analysis.summary.total,
|
|
@@ -65301,7 +66415,7 @@ function registerResearch(program) {
|
|
|
65301
66415
|
fingerprintCount: extraction.total,
|
|
65302
66416
|
candidates
|
|
65303
66417
|
};
|
|
65304
|
-
(0,
|
|
66418
|
+
(0, import_node_fs44.writeFileSync)(outputPath, JSON.stringify(payload, null, 2), "utf8");
|
|
65305
66419
|
logger.info(`Extracted ${extraction.total} fingerprints across ${analysis.summary.total} samples`);
|
|
65306
66420
|
logger.info(`Wrote ${candidates.length} candidate rule(s) to ${outputPath}`);
|
|
65307
66421
|
} catch (error) {
|
|
@@ -65312,8 +66426,8 @@ function registerResearch(program) {
|
|
|
65312
66426
|
}
|
|
65313
66427
|
|
|
65314
66428
|
// src/cli/commands/init.ts
|
|
65315
|
-
var
|
|
65316
|
-
var
|
|
66429
|
+
var import_node_fs45 = require("fs");
|
|
66430
|
+
var import_node_path72 = require("path");
|
|
65317
66431
|
init_logger();
|
|
65318
66432
|
init_builtins();
|
|
65319
66433
|
init_scan2();
|
|
@@ -65323,145 +66437,9 @@ init_git();
|
|
|
65323
66437
|
init_cache();
|
|
65324
66438
|
|
|
65325
66439
|
// src/snippet/targets.ts
|
|
65326
|
-
var
|
|
65327
|
-
|
|
65328
|
-
|
|
65329
|
-
function aiSpecificRules(rules) {
|
|
65330
|
-
return rules.filter((r) => r.aiSpecific).sort((a, b) => a.id.localeCompare(b.id));
|
|
65331
|
-
}
|
|
65332
|
-
function categorizeRules(rules) {
|
|
65333
|
-
const byCategory = /* @__PURE__ */ new Map();
|
|
65334
|
-
for (const r of rules) {
|
|
65335
|
-
const list = byCategory.get(r.category) ?? [];
|
|
65336
|
-
list.push(r);
|
|
65337
|
-
byCategory.set(r.category, list);
|
|
65338
|
-
}
|
|
65339
|
-
for (const list of byCategory.values()) {
|
|
65340
|
-
list.sort((a, b) => a.id.localeCompare(b.id));
|
|
65341
|
-
}
|
|
65342
|
-
return byCategory;
|
|
65343
|
-
}
|
|
65344
|
-
function baseContent(rules, toolLabel, intro) {
|
|
65345
|
-
const lines = [];
|
|
65346
|
-
lines.push("# " + toolLabel);
|
|
65347
|
-
lines.push("");
|
|
65348
|
-
lines.push(intro);
|
|
65349
|
-
lines.push("");
|
|
65350
|
-
const ai = aiSpecificRules(rules);
|
|
65351
|
-
const byCategory = categorizeRules(ai);
|
|
65352
|
-
lines.push("## Category-level directives");
|
|
65353
|
-
lines.push("");
|
|
65354
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65355
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65356
|
-
}
|
|
65357
|
-
lines.push("");
|
|
65358
|
-
lines.push("## Per-rule directives");
|
|
65359
|
-
lines.push("");
|
|
65360
|
-
for (const [category, list] of byCategory) {
|
|
65361
|
-
lines.push("### " + category);
|
|
65362
|
-
lines.push("");
|
|
65363
|
-
for (const rule of list) {
|
|
65364
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65365
|
-
lines.push("- `" + rule.id + "` (" + rule.severity + "): " + hint);
|
|
65366
|
-
}
|
|
65367
|
-
lines.push("");
|
|
65368
|
-
}
|
|
65369
|
-
return lines.join("\n");
|
|
65370
|
-
}
|
|
65371
|
-
|
|
65372
|
-
// src/snippet/generators.ts
|
|
65373
|
-
function generateCursorSnippet(rules) {
|
|
65374
|
-
const lines = [];
|
|
65375
|
-
lines.push("# slopbrick cursor rules");
|
|
65376
|
-
lines.push("");
|
|
65377
|
-
lines.push("Generated by `slopbrick init --cursor`. These rules describe patterns that slopbrick");
|
|
65378
|
-
lines.push("flags as AI-generated frontend slop. Avoid them when writing code in this project.");
|
|
65379
|
-
lines.push("");
|
|
65380
|
-
lines.push("## Category-level directives");
|
|
65381
|
-
lines.push("");
|
|
65382
|
-
for (const [category, directive] of Object.entries(CATEGORY_DIRECTIVES)) {
|
|
65383
|
-
lines.push("- **" + category + "**: " + directive);
|
|
65384
|
-
}
|
|
65385
|
-
lines.push("");
|
|
65386
|
-
lines.push("## Per-rule directives");
|
|
65387
|
-
lines.push("");
|
|
65388
|
-
const ai = aiSpecificRules(rules);
|
|
65389
|
-
const byCategory = categorizeRules(ai);
|
|
65390
|
-
for (const [category, list] of byCategory) {
|
|
65391
|
-
lines.push("### " + category);
|
|
65392
|
-
lines.push("");
|
|
65393
|
-
for (const rule of list) {
|
|
65394
|
-
const hint = RULE_HINTS[rule.id] ?? "Avoid patterns that fire rule `" + rule.id + "`.";
|
|
65395
|
-
lines.push("- **`" + rule.id + "`** (" + rule.severity + "): " + hint);
|
|
65396
|
-
}
|
|
65397
|
-
lines.push("");
|
|
65398
|
-
}
|
|
65399
|
-
lines.push("## Verification");
|
|
65400
|
-
lines.push("");
|
|
65401
|
-
lines.push("After writing code, run `slopbrick scan` and check for issues in any of the categories above.");
|
|
65402
|
-
lines.push("Use `slopbrick fix` for auto-fixable rules. Use `slopbrick mcp` from MCP-aware clients to query rules and scan files programmatically.");
|
|
65403
|
-
lines.push("");
|
|
65404
|
-
return lines.join("\n");
|
|
65405
|
-
}
|
|
65406
|
-
function generateAgentsMdSnippet(rules) {
|
|
65407
|
-
return baseContent(
|
|
65408
|
-
rules,
|
|
65409
|
-
"AGENTS.md",
|
|
65410
|
-
"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."
|
|
65411
|
-
) + "## How to verify\n\nAfter editing code, run `slopbrick scan`. Any issue with a category in this file is something to fix.\n";
|
|
65412
|
-
}
|
|
65413
|
-
function generateClaudeMdSnippet(rules) {
|
|
65414
|
-
return baseContent(
|
|
65415
|
-
rules,
|
|
65416
|
-
"CLAUDE.md",
|
|
65417
|
-
"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."
|
|
65418
|
-
) + "## 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";
|
|
65419
|
-
}
|
|
65420
|
-
function generateAiderSnippet(rules) {
|
|
65421
|
-
return baseContent(
|
|
65422
|
-
rules,
|
|
65423
|
-
"CONVENTIONS.md",
|
|
65424
|
-
"Generated by `slopbrick init --aider`. Aider reads CONVENTIONS.md (not AGENTS.md) for project conventions."
|
|
65425
|
-
);
|
|
65426
|
-
}
|
|
65427
|
-
function generateWindsurfSnippet(rules) {
|
|
65428
|
-
return baseContent(
|
|
65429
|
-
rules,
|
|
65430
|
-
".windsurfrules",
|
|
65431
|
-
"Generated by `slopbrick init --windsurf`. Windsurf Cascade reads .windsurfrules at project root."
|
|
65432
|
-
);
|
|
65433
|
-
}
|
|
65434
|
-
function generateClineRules(rules) {
|
|
65435
|
-
return baseContent(
|
|
65436
|
-
rules,
|
|
65437
|
-
"AGENTS.md",
|
|
65438
|
-
"Generated by `slopbrick init --cline`. Cline reads .clinerules/AGENTS.md from the project root."
|
|
65439
|
-
);
|
|
65440
|
-
}
|
|
65441
|
-
function generateCursorrulesLegacySnippet(rules) {
|
|
65442
|
-
return baseContent(
|
|
65443
|
-
rules,
|
|
65444
|
-
".cursorrules",
|
|
65445
|
-
"Generated by `slopbrick init --cursorrules`. This is the legacy Cursor rule format. Prefer `.cursor/rules/slopbrick.mdc` (use --cursor) for new projects."
|
|
65446
|
-
);
|
|
65447
|
-
}
|
|
65448
|
-
function generateGeminiSnippet(rules) {
|
|
65449
|
-
return baseContent(
|
|
65450
|
-
rules,
|
|
65451
|
-
"GEMINI.md",
|
|
65452
|
-
"Generated by `slopbrick init --gemini`. Gemini CLI reads .gemini/GEMINI.md from project root."
|
|
65453
|
-
);
|
|
65454
|
-
}
|
|
65455
|
-
function generateCopilotSnippet(rules) {
|
|
65456
|
-
return baseContent(
|
|
65457
|
-
rules,
|
|
65458
|
-
"copilot-instructions.md",
|
|
65459
|
-
"Generated by `slopbrick init --copilot`. GitHub Copilot reads .github/copilot-instructions.md from project root."
|
|
65460
|
-
);
|
|
65461
|
-
}
|
|
65462
|
-
|
|
65463
|
-
// src/snippet/targets.ts
|
|
65464
|
-
var SNIPPET_TARGETS = [
|
|
66440
|
+
var import_node_path71 = require("path");
|
|
66441
|
+
init_generators();
|
|
66442
|
+
var SNIPPET_TARGETS2 = [
|
|
65465
66443
|
{
|
|
65466
66444
|
flag: "cursor",
|
|
65467
66445
|
cliName: "--cursor",
|
|
@@ -65554,7 +66532,7 @@ var SNIPPET_TARGETS = [
|
|
|
65554
66532
|
}
|
|
65555
66533
|
];
|
|
65556
66534
|
function resolveTargetPath(target) {
|
|
65557
|
-
return target.isFolder ? (0,
|
|
66535
|
+
return target.isFolder ? (0, import_node_path71.join)(target.path, target.filename) : target.path;
|
|
65558
66536
|
}
|
|
65559
66537
|
function renderMatrix() {
|
|
65560
66538
|
const lines = [];
|
|
@@ -65562,7 +66540,7 @@ function renderMatrix() {
|
|
|
65562
66540
|
lines.push("");
|
|
65563
66541
|
lines.push("| Flag | File | Agent |");
|
|
65564
66542
|
lines.push("|------|------|-------|");
|
|
65565
|
-
for (const t of
|
|
66543
|
+
for (const t of SNIPPET_TARGETS2) {
|
|
65566
66544
|
lines.push("| `" + t.cliName + "` | `" + resolveTargetPath(t) + "` | " + t.agent + " |");
|
|
65567
66545
|
}
|
|
65568
66546
|
return lines.join("\n");
|
|
@@ -65576,13 +66554,13 @@ function registerInit(program) {
|
|
|
65576
66554
|
logger.info(renderMatrix());
|
|
65577
66555
|
process.exit(0);
|
|
65578
66556
|
}
|
|
65579
|
-
const cwd = (0,
|
|
65580
|
-
const configPath = (0,
|
|
66557
|
+
const cwd = (0, import_node_path72.resolve)(options.workspace ?? process.cwd());
|
|
66558
|
+
const configPath = (0, import_node_path72.join)(cwd, "slopbrick.config.mjs");
|
|
65581
66559
|
const detected = detectStack(cwd);
|
|
65582
66560
|
const fallbackConfig = { ...DEFAULT_CONFIG, ...detected };
|
|
65583
66561
|
const proposed = serializeConfig(fallbackConfig);
|
|
65584
|
-
if ((0,
|
|
65585
|
-
const current = (0,
|
|
66562
|
+
if ((0, import_node_fs45.existsSync)(configPath) && !cmdOptions.yes) {
|
|
66563
|
+
const current = (0, import_node_fs45.readFileSync)(configPath, "utf8");
|
|
65586
66564
|
logger.error(`A config file already exists at ${configPath}.`);
|
|
65587
66565
|
logger.error("To overwrite it with defaults, run `slopbrick init --yes`.");
|
|
65588
66566
|
logger.error("");
|
|
@@ -65603,7 +66581,7 @@ function registerInit(program) {
|
|
|
65603
66581
|
config = buildInitConfig(detected, answers);
|
|
65604
66582
|
usedWizard = true;
|
|
65605
66583
|
}
|
|
65606
|
-
(0,
|
|
66584
|
+
(0, import_node_fs45.writeFileSync)(configPath, serializeConfig(config));
|
|
65607
66585
|
appendGitignore(cwd);
|
|
65608
66586
|
const refresh = await refreshRegistrySnapshot(cwd);
|
|
65609
66587
|
if (!refresh.ok) {
|
|
@@ -65627,27 +66605,27 @@ function registerInit(program) {
|
|
|
65627
66605
|
}
|
|
65628
66606
|
logger.info(refresh.message);
|
|
65629
66607
|
}
|
|
65630
|
-
const targetsToWrite =
|
|
66608
|
+
const targetsToWrite = SNIPPET_TARGETS2.filter((t) => {
|
|
65631
66609
|
if (cmdOptions.all) return true;
|
|
65632
66610
|
const opts = cmdOptions;
|
|
65633
66611
|
return Boolean(opts[t.flag]);
|
|
65634
66612
|
});
|
|
65635
66613
|
for (const target of targetsToWrite) {
|
|
65636
|
-
const snippetPath = (0,
|
|
65637
|
-
(0,
|
|
66614
|
+
const snippetPath = (0, import_node_path72.join)(cwd, resolveTargetPath(target));
|
|
66615
|
+
(0, import_node_fs45.mkdirSync)((0, import_node_path72.dirname)(snippetPath), { recursive: true });
|
|
65638
66616
|
const generated = target.generator(builtinRules);
|
|
65639
|
-
if (!target.isFolder && (0,
|
|
65640
|
-
const existing = (0,
|
|
66617
|
+
if (!target.isFolder && (0, import_node_fs45.existsSync)(snippetPath)) {
|
|
66618
|
+
const existing = (0, import_node_fs45.readFileSync)(snippetPath, "utf8");
|
|
65641
66619
|
if (existing.includes("<!-- slopbrick:begin -->")) {
|
|
65642
66620
|
const updated = existing.replace(
|
|
65643
66621
|
/<!-- slopbrick:begin -->[\s\S]*?<!-- slopbrick:end -->/,
|
|
65644
66622
|
"<!-- slopbrick:begin -->\n" + generated + "<!-- slopbrick:end -->"
|
|
65645
66623
|
);
|
|
65646
|
-
(0,
|
|
66624
|
+
(0, import_node_fs45.writeFileSync)(snippetPath, updated, "utf8");
|
|
65647
66625
|
if (!options.quiet) logger.info(`Updated ${snippetPath}`);
|
|
65648
66626
|
continue;
|
|
65649
66627
|
}
|
|
65650
|
-
(0,
|
|
66628
|
+
(0, import_node_fs45.writeFileSync)(
|
|
65651
66629
|
snippetPath,
|
|
65652
66630
|
existing + (existing.endsWith("\n") ? "\n" : "\n\n") + generated,
|
|
65653
66631
|
"utf8"
|
|
@@ -65655,7 +66633,7 @@ function registerInit(program) {
|
|
|
65655
66633
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65656
66634
|
continue;
|
|
65657
66635
|
}
|
|
65658
|
-
(0,
|
|
66636
|
+
(0, import_node_fs45.writeFileSync)(snippetPath, generated, "utf8");
|
|
65659
66637
|
if (!options.quiet) logger.info(`Wrote ${snippetPath}`);
|
|
65660
66638
|
}
|
|
65661
66639
|
if (options.baseline) {
|
|
@@ -65673,14 +66651,14 @@ function registerInit(program) {
|
|
|
65673
66651
|
}
|
|
65674
66652
|
|
|
65675
66653
|
// src/cli/commands/flywheel.ts
|
|
65676
|
-
var
|
|
65677
|
-
var
|
|
66654
|
+
var import_node_fs47 = require("fs");
|
|
66655
|
+
var import_node_path74 = require("path");
|
|
65678
66656
|
init_logger();
|
|
65679
66657
|
init_telemetry();
|
|
65680
66658
|
|
|
65681
66659
|
// src/report/flywheel.ts
|
|
65682
|
-
var
|
|
65683
|
-
var
|
|
66660
|
+
var import_node_fs46 = require("fs");
|
|
66661
|
+
var import_node_path73 = require("path");
|
|
65684
66662
|
function average(values) {
|
|
65685
66663
|
if (values.length === 0) return 0;
|
|
65686
66664
|
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
@@ -65770,7 +66748,7 @@ function formatFlywheel(summary, options = {}) {
|
|
|
65770
66748
|
function registerFlywheel(program) {
|
|
65771
66749
|
program.command("flywheel").description("summarize aggregated scan telemetry").option("--format <pretty|json>", "output format", "pretty").option("--export <path>", "write summary as JSON to <path>").action(async (cmdOptions, command) => {
|
|
65772
66750
|
const options = command.optsWithGlobals();
|
|
65773
|
-
const cwd = (0,
|
|
66751
|
+
const cwd = (0, import_node_path74.resolve)(options.workspace ?? process.cwd());
|
|
65774
66752
|
const payloads = readTelemetry(cwd);
|
|
65775
66753
|
if (payloads.length === 0) {
|
|
65776
66754
|
logger.info("No flywheel telemetry found. Run a scan first.");
|
|
@@ -65778,9 +66756,9 @@ function registerFlywheel(program) {
|
|
|
65778
66756
|
}
|
|
65779
66757
|
const summary = summarizeTelemetry(payloads);
|
|
65780
66758
|
if (cmdOptions.export) {
|
|
65781
|
-
const exportPath = (0,
|
|
65782
|
-
(0,
|
|
65783
|
-
(0,
|
|
66759
|
+
const exportPath = (0, import_node_path74.resolve)(cmdOptions.export);
|
|
66760
|
+
(0, import_node_fs47.mkdirSync)((0, import_node_path74.dirname)(exportPath), { recursive: true });
|
|
66761
|
+
(0, import_node_fs47.writeFileSync)(exportPath, JSON.stringify(summary, null, 2), "utf-8");
|
|
65784
66762
|
logger.info(`Wrote flywheel summary to ${exportPath}`);
|
|
65785
66763
|
process.exit(0);
|
|
65786
66764
|
}
|
|
@@ -65807,8 +66785,8 @@ init_dist2();
|
|
|
65807
66785
|
init_memory_io();
|
|
65808
66786
|
|
|
65809
66787
|
// src/fix/focus-ring.ts
|
|
65810
|
-
var
|
|
65811
|
-
var
|
|
66788
|
+
var import_node_fs48 = require("fs");
|
|
66789
|
+
var import_node_fs49 = require("fs");
|
|
65812
66790
|
var ANCHOR_START = "/* @slopbrick:v1.0.0:fix:focus-ring */";
|
|
65813
66791
|
var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
65814
66792
|
:focus-visible {
|
|
@@ -65817,15 +66795,15 @@ var CSS_BLOCK = `/* @slopbrick:v1.0.0:fix:focus-ring */
|
|
|
65817
66795
|
}
|
|
65818
66796
|
/* @slopbrick:v1.0.0:fix:focus-ring-end */`;
|
|
65819
66797
|
function applyFocusRingFix(targetFile) {
|
|
65820
|
-
if (!(0,
|
|
66798
|
+
if (!(0, import_node_fs48.existsSync)(targetFile)) {
|
|
65821
66799
|
return { applied: false, reason: "missing-global-css-target" };
|
|
65822
66800
|
}
|
|
65823
|
-
const content = (0,
|
|
66801
|
+
const content = (0, import_node_fs49.readFileSync)(targetFile, "utf-8");
|
|
65824
66802
|
if (content.includes(ANCHOR_START)) {
|
|
65825
66803
|
return { applied: false, reason: "already-present" };
|
|
65826
66804
|
}
|
|
65827
66805
|
const separator = content.endsWith("\n") ? "" : "\n";
|
|
65828
|
-
(0,
|
|
66806
|
+
(0, import_node_fs49.writeFileSync)(targetFile, `${content}${separator}${CSS_BLOCK}
|
|
65829
66807
|
`);
|
|
65830
66808
|
return { applied: true };
|
|
65831
66809
|
}
|
|
@@ -65834,21 +66812,21 @@ function applyFocusRingFix(targetFile) {
|
|
|
65834
66812
|
init_layout_token();
|
|
65835
66813
|
|
|
65836
66814
|
// src/fix/use-client.ts
|
|
65837
|
-
var
|
|
66815
|
+
var import_node_fs50 = require("fs");
|
|
65838
66816
|
function applyUseClientFix(filePath) {
|
|
65839
|
-
const content = (0,
|
|
66817
|
+
const content = (0, import_node_fs50.readFileSync)(filePath, "utf-8");
|
|
65840
66818
|
const trimmed = content.trimStart();
|
|
65841
66819
|
if (trimmed.startsWith("'use client'") || trimmed.startsWith('"use client"')) {
|
|
65842
66820
|
return { applied: false, reason: "already-present" };
|
|
65843
66821
|
}
|
|
65844
|
-
(0,
|
|
66822
|
+
(0, import_node_fs50.writeFileSync)(filePath, `"use client";
|
|
65845
66823
|
|
|
65846
66824
|
${content}`);
|
|
65847
66825
|
return { applied: true };
|
|
65848
66826
|
}
|
|
65849
66827
|
|
|
65850
66828
|
// src/fix/visual-codemod.ts
|
|
65851
|
-
var
|
|
66829
|
+
var import_node_fs51 = require("fs");
|
|
65852
66830
|
|
|
65853
66831
|
// src/fix/visual-codemods/tailwind.ts
|
|
65854
66832
|
var ARBITRARY_ESCAPE_RE = /\b(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr|gap|space-[xy]|w|h|min-w|max-w|min-h|max-h|text-\w+|leading-\w+|rounded|border)-?\[(-?\d+(?:\.\d+)?)(px|rem|em|%|vh|vw)?\]/g;
|
|
@@ -66057,7 +67035,7 @@ var ALL_CODEMODS = [
|
|
|
66057
67035
|
{ name: "aria-attr-typo", fn: applyAriaAttrTypoCodemod }
|
|
66058
67036
|
];
|
|
66059
67037
|
function applyVisualCodemods(filePath) {
|
|
66060
|
-
const original = (0,
|
|
67038
|
+
const original = (0, import_node_fs51.readFileSync)(filePath, "utf-8");
|
|
66061
67039
|
let content = original;
|
|
66062
67040
|
const reasons = [];
|
|
66063
67041
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -66073,7 +67051,7 @@ function applyVisualCodemods(filePath) {
|
|
|
66073
67051
|
}
|
|
66074
67052
|
}
|
|
66075
67053
|
if (content !== original) {
|
|
66076
|
-
(0,
|
|
67054
|
+
(0, import_node_fs51.writeFileSync)(filePath, content);
|
|
66077
67055
|
}
|
|
66078
67056
|
return {
|
|
66079
67057
|
filePath,
|
|
@@ -66355,12 +67333,12 @@ var http = __toESM(require("http"), 1);
|
|
|
66355
67333
|
var https = __toESM(require("https"), 1);
|
|
66356
67334
|
var import_node_url2 = require("url");
|
|
66357
67335
|
var sendBeacon = (url, payload, timeoutMs) => {
|
|
66358
|
-
return new Promise((
|
|
67336
|
+
return new Promise((resolve43) => {
|
|
66359
67337
|
let parsed;
|
|
66360
67338
|
try {
|
|
66361
67339
|
parsed = new import_node_url2.URL(url);
|
|
66362
67340
|
} catch {
|
|
66363
|
-
|
|
67341
|
+
resolve43();
|
|
66364
67342
|
return;
|
|
66365
67343
|
}
|
|
66366
67344
|
const isHttps = parsed.protocol === "https:";
|
|
@@ -66381,7 +67359,7 @@ var sendBeacon = (url, payload, timeoutMs) => {
|
|
|
66381
67359
|
const settle = () => {
|
|
66382
67360
|
if (settled) return;
|
|
66383
67361
|
settled = true;
|
|
66384
|
-
|
|
67362
|
+
resolve43();
|
|
66385
67363
|
};
|
|
66386
67364
|
let req;
|
|
66387
67365
|
try {
|
|
@@ -66461,14 +67439,16 @@ process.on("uncaughtException", (err) => {
|
|
|
66461
67439
|
});
|
|
66462
67440
|
async function runCli({ start }) {
|
|
66463
67441
|
try {
|
|
66464
|
-
const program = new
|
|
67442
|
+
const program = new import_commander3.Command().name("slopbrick").description("Repository Coherence Scanner \u2014 surface AI-induced pattern drift, secret leaks, and design-token violations").version(VERSION).option("--framework <name>", "framework multiplier to apply").option("--include <glob>", "include pattern (repeatable)", collectGlob, []).option("--exclude <glob>", "exclude pattern (repeatable)", collectGlob, []).option("--ai-only", "only report AI-specific issues").option("--human-only", "only report human-facing issues").option("--ignore-wcag22", "ignore WCAG 2.2 related issues").option("--format <pretty|json|sarif|html>", "output format", "pretty").option("--threads <n>", "number of worker threads", parseThreads).option("--since <ref>", "only scan files changed since git ref").option("--diff <ref>", "alias for --since <ref>; also adds PR Slop Score to the report").option("--workspace <path>", "workspace/project path", process.cwd()).option("--tighten", "tighten baseline allowances").option("--fix", "apply auto-fixes").option("--dry-run", "with --fix: print what would change without writing").option("--show-fixes-diff", "print unified diff of proposed auto-fixes").option("--doctor", "run diagnostics").option("--watch", "watch files and re-run").option("--suggest", "print remediation advice").option("--why-failing", "print the top 5 rules dragging the score down").option("--brief", "terse output (verdict + headline + threshold + delta only)").option("--heatmap", "print migration ROI heatmap").option("--quiet", "suppress non-error output").option("--verbose", "enable debug logging (file paths, timings, rule-fire counts)").option("--strict", "exit 2 if any high-severity issue remains").option("--no-increase", "exit 2 if slop index increased since last run").option("--refresh-snippets", "rewrite the managed slopbrick block in AGENTS.md and CLAUDE.md after a scan").option("--baseline", "save a baseline after this scan").option("--trend [n]", "print a sparkline of the last n runs", parseTrend).option("--json [path]", "write JSON report to path or stdout").option("--html [path]", "write HTML report to path or stdout").option("--staged", "scan only changed files (staged and unstaged)").option("--changed", "scan working-tree changes (staged + unstaged + untracked)").option("--incremental", "skip unchanged files using the persisted hash cache").option("--cache-path <path>", "path to the incremental-scan cache (default: .slopbrick-cache.json)").option("--tokens <path>", "merge tokens.json layout values into the arbitrary-value allowlist").option("--cache", "cache parsed AST results locally").option("--no-color", "suppress ANSI color codes in output").option("--security-only", "run only the security/* rules").option("--full", "show the complete report (all issues, all categories)").option("--report-usage", "opt in to a one-shot usage ping to SLOPBRICK_TELEMETRY_ENDPOINT (no PII)");
|
|
66465
67443
|
program.helpInformation = () => formatGroupedHelp(program);
|
|
67444
|
+
setExitOverride(program);
|
|
66466
67445
|
registerInit(program);
|
|
66467
67446
|
registerInstall(program);
|
|
66468
67447
|
registerUninstall(program);
|
|
66469
67448
|
registerBadge(program);
|
|
66470
67449
|
registerSuggest(program);
|
|
66471
67450
|
registerExplain(program);
|
|
67451
|
+
registerComposite(program);
|
|
66472
67452
|
registerCalibration(program);
|
|
66473
67453
|
registerFlywheel(program);
|
|
66474
67454
|
const scanAction = async (paths, _options, command) => {
|
|
@@ -66487,7 +67467,7 @@ async function runCli({ start }) {
|
|
|
66487
67467
|
logger.error("--heatmap and --suggest can't be used together. Pick one: a heatmap of severity, or text advice.");
|
|
66488
67468
|
process.exit(2);
|
|
66489
67469
|
}
|
|
66490
|
-
const cwd = (0,
|
|
67470
|
+
const cwd = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
|
|
66491
67471
|
if (options.trend !== void 0) {
|
|
66492
67472
|
const runs = await readRuns(cwd, fsMemoryIO);
|
|
66493
67473
|
if (runs.length === 0) {
|
|
@@ -66535,7 +67515,7 @@ async function runCli({ start }) {
|
|
|
66535
67515
|
});
|
|
66536
67516
|
}
|
|
66537
67517
|
if (options.baseline) {
|
|
66538
|
-
const cwd2 = (0,
|
|
67518
|
+
const cwd2 = (0, import_node_path75.resolve)(options.workspace ?? process.cwd());
|
|
66539
67519
|
const configHash = hashConfig(config);
|
|
66540
67520
|
const gitHead = await getGitHead(cwd2) ?? "unknown";
|
|
66541
67521
|
const cache = buildBaselineCache(report, configHash, gitHead, cwd2);
|
|
@@ -66647,7 +67627,9 @@ async function runCli({ start }) {
|
|
|
66647
67627
|
program.outputHelp();
|
|
66648
67628
|
process.exit(0);
|
|
66649
67629
|
}
|
|
66650
|
-
await program
|
|
67630
|
+
await dispatch(program, async () => {
|
|
67631
|
+
await program.parseAsync(process.argv);
|
|
67632
|
+
});
|
|
66651
67633
|
} catch (err) {
|
|
66652
67634
|
if (err instanceof ConfigValidationError) {
|
|
66653
67635
|
logger.error(err.message);
|