slopbrick 0.18.3 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,67 +4,35 @@ import * as _usebrick_core from '@usebrick/core';
4
4
  import { SignalStrengthEntry } from '@usebrick/core';
5
5
 
6
6
  /**
7
- * Constitution declared by the user (or auto-detected from package.json).
8
- * Each allow-list field is a list of canonical names — agents and
9
- * reviewers can match against import strings without needing the
10
- * original package name. For example, `@reduxjs/toolkit` and `redux`
11
- * both surface as `'redux'` under `stateManagement`.
7
+ * v0.18.4 (Phase B R-M2): types split.
8
+ * Module: _header
12
9
  *
13
- * `forbidden` is the deny-list: bare package specifiers and `@scope/`
14
- * prefixes that the project has decided are off-limits. Matched against
15
- * imports during `slop_check_constitution` and the `slopbrick drift`
16
- * command.
10
+ * VERSION constant read from package.json at runtime so a release
11
+ * version bump propagates without a separate code change here. The
12
+ * build's `define` rule inlines this at compile time, so the
13
+ * value is baked in.
14
+ *
15
+ * The literal `process.env.SLOPBRICK_VERSION` is replaced by
16
+ * tsup's esbuild `define` at build time, using the version field
17
+ * from this package's package.json. No runtime file I/O, so the
18
+ * dist/ bundle stays self-contained and the path-relative
19
+ * `require('../package.json')` trick (which breaks after bundling)
20
+ * is no longer needed.
17
21
  */
18
- interface Constitution {
19
- /** State-management libraries, e.g. 'zustand', 'redux', 'jotai'. */
20
- stateManagement?: string[];
21
- /** Data-fetching / server-state libraries, e.g. 'react-query', 'swr'. */
22
- dataFetching?: string[];
23
- /** UI component libraries, e.g. 'shadcn', 'radix', 'mui'. */
24
- uiLibrary?: string[];
25
- /** Form + schema-validation libraries, e.g. 'react-hook-form', 'zod'. */
26
- forms?: string[];
27
- /** Styling solutions, e.g. 'tailwind', 'styled-components', 'emotion'. */
28
- styling?: string[];
29
- /** Routing libraries, e.g. 'next', 'react-router', 'tanstack-router'. */
30
- routing?: string[];
31
- /** Free-form user-declared categories, keyed by category name. */
32
- custom?: Record<string, string[]>;
33
- /**
34
- * Explicit deny-list of bare package specifiers and `@scope/`
35
- * prefixes. Anything in this list is forbidden — even if it would
36
- * otherwise match a canonical category. Matching rules (see
37
- * `matchForbidden`):
38
- * - `forbidden: ['moment']` matches `moment` exactly.
39
- * - `forbidden: ['@types/']` matches any `@types/...` import.
40
- * - `forbidden: ['lodash']` matches `lodash` but not `lodash-es`.
41
- */
42
- forbidden?: string[];
43
- }
44
-
45
- type BusinessLogicCategory = 'pricing' | 'validation' | 'formatting';
46
- interface BusinessLogicIssue {
47
- category: BusinessLogicCategory;
48
- filePath: string;
49
- line: number;
50
- column: number;
51
- ruleId: string;
52
- message: string;
53
- advice?: string;
54
- }
22
+ declare const VERSION: string;
55
23
 
56
- interface CategoryDeduction {
57
- category: string;
58
- count: number;
59
- /** Weight per unit (per-extra or per-N-findings). */
60
- weight: number;
61
- /** Total deduction applied to the score. */
62
- deduction: number;
63
- /** Human-readable summary, e.g. "3 modal systems, 2 beyond baseline". */
64
- summary: string;
65
- /** Concrete findings (filenames, category labels, etc.). */
66
- findings: string[];
67
- }
24
+ /**
25
+ * v0.18.4 (Phase B R-M2): types split.
26
+ * Module: primitives
27
+ */
28
+ type Severity = 'low' | 'medium' | 'high';
29
+ type RuleSeverity = Severity | 'auto';
30
+ type Category = 'visual' | 'typo' | 'wcag' | 'layout' | 'component' | 'logic' | 'arch' | 'perf' | 'security' | 'test' | 'docs' | 'db' | 'ai' | 'context' | 'product' | 'i18n';
31
+ /**
32
+ * `react` covers `.tsx`, `.jsx`, `.ts`, `.js`. Other values are detected
33
+ * by file extension. Unknown extensions fall back to `'react'`.
34
+ */
35
+ type Framework$1 = 'react' | 'vue' | 'svelte' | 'astro' | 'html';
68
36
 
69
37
  interface FileMeta {
70
38
  /** Absolute path to the file (or relative path from cwd, whichever the
@@ -232,6 +200,11 @@ interface ScanFactsV2 {
232
200
  optimisticUpdates: OptimisticUpdateFact[];
233
201
  };
234
202
  designTokens: DesignTokens;
203
+ /**
204
+ * dead-code detector rules. Populated by the visitor's identifier
205
+ * walk + import/declaration/branch handlers. See `DeadCodeFacts`
206
+ * above for the shape. */
207
+ deadCode: DeadCodeFacts;
235
208
  /**
236
209
  * templates. Replaces the synthetic `<template>` elements that the
237
210
  * migration injected into `jsx.elements`. */
@@ -248,294 +221,78 @@ interface ScanFactsV2 {
248
221
  * and `--suggest` output). Not all rules need this. */
249
222
  _source?: string;
250
223
  }
251
-
252
- /**
253
- * v0.15.0+: The user-facing 3-bucket taxonomy. Maps engine verdicts
254
- * (6 values, used for LR math and calibration) to UI buckets (3 values,
255
- * used in the report and JSON output).
256
- *
257
- * Per the user's design intent ("INVERTED is basically HYGIENE"), INVERTED
258
- * rules are in the same UI bucket as HYGIENE rules. The engine still
259
- * distinguishes them (INVERTED has LR < 1, HYGIENE has LR ≈ 1).
260
- */
261
- type Bucket = 'ai' | 'hygiene' | 'suppressed';
262
-
263
- declare const VERSION: string;
264
- /**
265
- * Runtime shape of `.slopbrick/health.json`. The on-disk JSON
266
- * matches `packages/core/schemas/v1/health.schema.json` (v3); this
267
- * interface is the slopbrick-side runtime companion. Schema is the
268
- * source of truth — keep this in sync with the codegen output in
269
- * `packages/core/src/generated/health.ts` (RepositoryMemoryHealth).
270
- *
271
- * v3 dropped the single `slopIndex` headline in favor of four named
272
- * scores:
273
- * - `aiQuality` — AI-specific findings (USEFUL + OK verdicts)
274
- * - `engineeringHygiene` — HYGIENE + INVERTED rules
275
- * - `security` — security/* rules
276
- * - `repositoryHealth` — composite (0.5*aiQ + 0.3*eng + 0.2*sec)
224
+ /** What kind of declaration produced a binding. */
225
+ type BindingKind = 'import-specifier' | 'import-default' | 'import-namespace' | 'var' | 'let' | 'const' | 'function' | 'class' | 'type' | 'interface' | 'enum' | 'parameter' | 'catch-clause';
226
+ /** A single binding declaration the visitor found in the file.
277
227
  *
278
- * Plus optional verdict + bucket distributions for agent-readable
279
- * insight.
280
- */
281
- interface HealthFile {
282
- version: typeof _usebrick_core.STRUCTURE_SCHEMA_VERSION;
283
- generatedAt: string;
284
- workspace: string;
285
- aiQuality: number;
286
- engineeringHygiene: number;
287
- security: number;
288
- repositoryHealth: number;
289
- verdictDistribution?: Record<_usebrick_core.Verdict, number>;
290
- bucketDistribution?: Record<Bucket, number>;
291
- categoryScores: Record<string, number>;
292
- issueCounts: {
293
- high: number;
294
- medium: number;
295
- low: number;
296
- };
297
- constitutionDrift?: number;
298
- topOffenseIds?: string[];
299
- scanDurationMs?: number;
228
+ * `isReferenced` is set to true if ANY identifier reference to this
229
+ * name appeared in the file. The visitor uses a single global
230
+ * referenced-name set per file (intra-scope shadowing is rare enough
231
+ * in practice that file-level resolution catches ~95% of dead code
232
+ * with negligible false positives — the remaining 5% is mostly
233
+ * intentional re-exports which are handled separately). */
234
+ interface BindingRecord {
235
+ /** The local name as written (`Button` for `import { Button } from ...`,
236
+ * `setFoo` for a parameter, etc.). */
237
+ name: string;
238
+ kind: BindingKind;
239
+ /** Source line (1-indexed). */
240
+ line: number;
241
+ /** Source column (0-indexed). */
242
+ column: number;
243
+ /** The owning source — used to differentiate top-level imports from
244
+ * inline `import()` expressions, which are dynamic and out of
245
+ * scope for the dead-import rule. */
246
+ source?: string;
247
+ /** Set to true when the visitor saw at least one reference to this
248
+ * name later in the file. */
249
+ isReferenced: boolean;
250
+ }
251
+ /** A literal boolean condition (`if (true)`, `while (false)`,
252
+ * ternary `cond ? a : false`) that makes the branch statically
253
+ * decidable. */
254
+ interface ConstantConditionRecord {
255
+ /** The kind of construct. */
256
+ kind: 'if-true' | 'if-false' | 'while-true' | 'while-false' | 'ternary';
257
+ /** The condition's source text. */
258
+ condition: string;
259
+ line: number;
260
+ column: number;
300
261
  }
301
- /**
302
- * Categorical bucket for the AI Maintenance Cost score.
303
- *
304
- * low — ship as-is, revisit quarterly
305
- * medium schedule cleanup work
306
- * high — block new feature work in the affected subsystem
307
- * critical — dedicated refactor sprint required
308
- *
309
- * Deliberately a categorical label (not numeric). Same reasoning as
310
- * `aiSecurityRisk`: a single bucket is harder to game than a number,
311
- * and a manager can read "AI Maintenance Cost: HIGH" in two seconds.
312
- *
313
- * The numeric `health` (0-100, higher = better) and `monthlyUSD`
314
- * (estimated monthly cost to fix the underlying issues) are exposed
315
- * alongside the label for agents and trend pipelines that need them.
316
- */
317
- type AiMaintenanceCost = 'low' | 'medium' | 'high' | 'critical';
318
- /** Per-axis contribution to the AI Maintenance Cost score. */
319
- interface MaintenanceAxisHealth {
320
- /** Stable axis name (`slopIndex`, `architectureConsistency`, `aiSecurityRisk`, ...). */
321
- axis: string;
322
- /** Display label printed by the pretty reporter. */
323
- label: string;
324
- /** 0-100, higher = better. Derived from the underlying signal. */
325
- health: number;
326
- /** How this was derived, e.g. `100 - slopIndex (inverted)`. */
327
- source: string;
262
+ /** A statement that is unreachable because an earlier statement in
263
+ * the same function body unconditionally exited (`return`, `throw`,
264
+ * `break`, `continue`). */
265
+ interface UnreachableStatementRecord {
266
+ /** Why the earlier statement exited. */
267
+ terminator: 'return' | 'throw' | 'break' | 'continue';
268
+ /** First line of the unreachable statement. */
269
+ line: number;
270
+ column: number;
271
+ /** A short snippet (first 60 chars) for the rule's message. */
272
+ snippet: string;
328
273
  }
329
- /** Inputs to the pure `computeAiMaintenanceCost` function. Every axis is optional. */
330
- interface MaintenanceAxes {
331
- /** v0.15.0 U.4+: 0-100, higher = better. The new headline score
332
- * that replaces slopIndex. Tests and callers should pass this
333
- * going forward. */
334
- aiQuality?: number;
335
- /** v0.15.0 U.4+: 0-100, higher = better. */
336
- engineeringHygiene?: number;
337
- /** v0.15.0 U.4+: 0-100, higher = better. */
338
- security?: number;
339
- /** v0.15.0 U.4+: 0-100, higher = better. */
340
- repositoryHealth?: number;
341
- /** 0-100, lower = better. @deprecated v0.15.0: use aiQuality. Kept
342
- * for backward compat with existing test fixtures and historical
343
- * telemetry. The axis inverts it internally. */
344
- slopIndex?: number;
345
- /** 0-100, higher = better. From `buildArchitectureScore`. */
346
- architectureConsistency?: number;
347
- /** Categorical — mapped to numeric via `MAINTENANCE_SECURITY_NUMERIC`. */
348
- aiSecurityRisk?: 'low' | 'medium' | 'high' | 'critical';
349
- /** Total constitution violations (sum across categories). */
350
- constitutionViolations?: number;
351
- /** Spacing + radius scale violation counts. */
352
- designTokenDrift?: {
353
- spacing: number;
354
- radius: number;
355
- };
356
- /** Total high-severity issues across the scan. Extra penalty. */
357
- highSeverityIssueCount?: number;
358
- /** Approximate LoC; defaults to 0 if unknown (small-project path). */
359
- linesOfCode?: number;
360
- /** False when the user has not declared a constitution / design tokens
361
- * (no drift run). When true, applies the 1.5–2.5× AI multiplier. */
362
- hasAiSignals?: boolean;
274
+ /** The dead-code domain. Empty when the file has nothing the visitor
275
+ * classified as suspicious. */
276
+ interface DeadCodeFacts {
277
+ bindings: BindingRecord[];
278
+ constantConditions: ConstantConditionRecord[];
279
+ unreachableStatements: UnreachableStatementRecord[];
363
280
  }
364
- /** Result returned by `computeAiMaintenanceCost` + `computeAiMaintenanceCostFromReport`. */
365
- interface AiMaintenanceCostResult {
366
- /** Categorical headline. */
367
- cost: AiMaintenanceCost;
368
- /** Weighted health in 0-100 (higher = better). Bucket is derived from this. */
369
- health: number;
370
- /** Estimated monthly USD cost to fix the underlying issues.
371
- * Anchored to Sonar's $306K/yr/MLoC baseline ($25.50/1k LoC/month)
372
- * with the CodeClimate grade→minutes mapping for per-issue cost. */
373
- monthlyUSD: number;
374
- /** Per-axis breakdown, sorted by health ascending (worst first). */
375
- axes: MaintenanceAxisHealth[];
376
- /** Per-bucket advice for managers. */
377
- advice: string;
281
+
282
+ interface FixSuggestion {
283
+ kind: 'insert' | 'replace' | 'css-anchor';
284
+ description: string;
285
+ targetFile?: string;
286
+ anchor?: string;
287
+ oldValue?: string;
288
+ newValue?: string;
378
289
  }
379
- /**
380
- * Categorical drift band on the doc-freshness score.
381
- * low — 80-100
382
- * medium 60-79
383
- * high — 40-59
384
- * critical — 0-39
385
- *
386
- * Drives the `--strict` exit code for `slopbrick docs` (high/critical = 1).
387
- */
388
- type DocDriftLevel = 'low' | 'medium' | 'high' | 'critical';
389
- /** A single doc-drift finding — one rule firing on one line. */
390
- interface DocFinding {
391
- ruleId: 'docs/stale-package-reference' | 'docs/stale-function-reference' | 'docs/expired-code-example' | 'docs/broken-link';
392
- severity: 'low' | 'medium' | 'high';
393
- docFile: string;
394
- line: number;
395
- column: number;
396
- message: string;
397
- advice: string;
398
- /** For `stale-package-reference`: the package the doc references. */
399
- package?: string;
400
- /** For `stale-function-reference`: the identifier. */
401
- identifier?: string;
402
- /** For `broken-link`: the unresolved link target. */
403
- link?: string;
404
- }
405
- /**
406
- * Categorical drift band on the db-health score.
407
- * low — 80-100
408
- * medium — 60-79
409
- * high — 40-59
410
- * critical — 0-39
411
- */
412
- type DbDriftLevel = 'low' | 'medium' | 'high' | 'critical';
413
- /** A single db-health finding — one rule firing on one table/file. */
414
- interface DbFinding {
415
- ruleId: 'db/missing-fk-index' | 'db/duplicate-index' | 'db/missing-not-null' | 'db/enum-sprawl' | 'db/naming-inconsistency' | 'db/sql-concat';
416
- severity: 'low' | 'medium' | 'high';
417
- dbFile: string;
418
- line: number;
419
- column: number;
420
- message: string;
421
- advice: string;
422
- /** For rules that target a specific table. */
423
- table?: string;
424
- /** For column-level rules. */
425
- columnName?: string;
426
- }
427
- /**
428
- * Letter-grade band on the composite Repository Health score.
429
- * Drives the `aiDebt` field — same categorical shape as `aiSecurityRisk`.
430
- *
431
- * low — 80-100 (clean; safe to ship)
432
- * medium — 60-79 (manageable; revisit quarterly)
433
- * high — 40-59 (block new feature work in affected subsystems)
434
- * critical — 0-39 (dedicated refactor sprint required)
435
- */
436
- type AiDebt = 'low' | 'medium' | 'high' | 'critical';
437
- /** Numeric mapping for categorical `aiSecurityRisk` used in the composite. */
438
- declare const AI_SECURITY_NUMERIC: Record<'low' | 'medium' | 'high' | 'critical', number>;
439
- /** Inputs to the pure `buildRepositoryHealth` function. Every input is optional. */
440
- interface RepositoryHealthInputs {
441
- /** v0.15.0 U.4+: 0-100, higher = better. The new headline score
442
- * that replaces slopIndex. Tests and callers should pass this
443
- * going forward. */
444
- aiQuality?: number;
445
- /** v0.15.0 U.4+: 0-100, higher = better. */
446
- engineeringHygiene?: number;
447
- /** v0.15.0 U.4+: 0-100, higher = better. */
448
- security?: number;
449
- /** v0.15.0 U.4+: 0-100, higher = better. */
450
- repositoryHealth?: number;
451
- /** 0-100, lower = better. Inverted to 100 - x for the composite.
452
- * @deprecated v0.15.0: use aiQuality. Kept for backward compat. */
453
- slopIndex?: number;
454
- /** 0-100, higher = better. */
455
- architectureConsistency?: number;
456
- /** Categorical — mapped via AI_SECURITY_NUMERIC. */
457
- aiSecurityRisk?: 'low' | 'medium' | 'high' | 'critical';
458
- /** Spacing + radius scale violation counts. */
459
- designTokenViolations?: {
460
- spacing: number;
461
- radius: number;
462
- };
463
- /** Total high-severity issue count (extra penalty). */
464
- highSeverityIssueCount?: number;
465
- /** 0-100, higher = better. From `slopbrick test` (Phase 5). */
466
- testQuality?: number;
467
- /** 0-100, higher = better. From `slopbrick business-logic` (Phase 7). */
468
- businessLogicCoherence?: number;
469
- /** 0-100, higher = better. From `slopbrick docs` (Phase 6). */
470
- docFreshness?: number;
471
- /** 0-100, higher = better. From `slopbrick db` (Phase 8). */
472
- dbHealth?: number;
473
- /** v0.10 — MDL log-likelihood ratio from `computeMDLikelihood`.
474
- * Positive = rule-firing evidence favors m_ai; negative favors
475
- * m_human. Surfaced on the result for reporting; NOT folded into
476
- * the weighted-average composite (which is heuristic and remains
477
- * unchanged in this phase). Caller computes the ratio from the
478
- * distinct `ruleId` set across the report's issues. */
479
- mdlLogRatio?: number;
480
- }
481
- /** Result returned by `buildRepositoryHealth` + `buildRepositoryHealthFromReport`. */
482
- interface RepositoryHealth {
483
- /** Composite 0-100 score (higher = better). */
484
- score: number;
485
- /** Letter-grade band derived from `score`. */
486
- aiDebt: AiDebt;
487
- /** Per-axis contribution breakdown (axis → 0-100, higher = better). */
488
- breakdown: Record<string, number>;
489
- /** Per-axis weight actually applied (post-renormalization). */
490
- appliedWeights: Record<string, number>;
491
- /** Warnings for managers (e.g. "Critical security risk overrides everything"). */
492
- warnings: string[];
493
- /** One-line summary. */
494
- headline: string;
495
- /** v0.10 — MDL log-likelihood ratio (positive = m_ai evidence).
496
- * Surfaced when the caller passed `mdlLogRatio` to
497
- * `buildRepositoryHealth`. Not folded into the weighted-average
498
- * composite in this phase (kept separate so the heuristic score
499
- * can be audited independently of the MDL axis). */
500
- mdlLogRatio?: number;
501
- }
502
- /** Default weights — sum to 1.0. Optional axes are skipped, and the
503
- * remaining weights renormalize to 1.0. Tuned to give `aiSecurityRisk`
504
- * the heaviest weight (it is the catastrophic single-event dimension)
505
- * while still letting slopIndex + architecture drive most cases. */
506
- declare const REPOSITORY_HEALTH_WEIGHTS: {
507
- readonly slopIndex: 0.2;
508
- readonly architectureConsistency: 0.2;
509
- readonly aiSecurityRisk: 0.2;
510
- readonly designTokenViolations: 0.1;
511
- readonly testQuality: 0.1;
512
- readonly businessLogicCoherence: 0.1;
513
- readonly docFreshness: 0.05;
514
- readonly dbHealth: 0.05;
515
- };
516
-
517
- type Severity = 'low' | 'medium' | 'high';
518
- type RuleSeverity = Severity | 'auto';
519
- type Category = 'visual' | 'typo' | 'wcag' | 'layout' | 'component' | 'logic' | 'arch' | 'perf' | 'security' | 'test' | 'docs' | 'db' | 'ai' | 'context' | 'product' | 'i18n';
520
- /**
521
- * `react` covers `.tsx`, `.jsx`, `.ts`, `.js`. Other values are detected
522
- * by file extension. Unknown extensions fall back to `'react'`.
523
- */
524
- type Framework$1 = 'react' | 'vue' | 'svelte' | 'astro' | 'html';
525
- interface FixSuggestion {
526
- kind: 'insert' | 'replace' | 'css-anchor';
527
- description: string;
528
- targetFile?: string;
529
- anchor?: string;
530
- oldValue?: string;
531
- newValue?: string;
532
- }
533
- interface Issue {
534
- ruleId: string;
535
- category: Category;
536
- severity: Severity;
537
- aiSpecific: boolean;
538
- filePath?: string;
290
+ interface Issue {
291
+ ruleId: string;
292
+ category: Category;
293
+ severity: Severity;
294
+ aiSpecific: boolean;
295
+ filePath?: string;
539
296
  message: string;
540
297
  line: number;
541
298
  column: number;
@@ -816,19 +573,527 @@ interface FileScanResult {
816
573
  */
817
574
  compositeScore?: _usebrick_engine.CompositeScore;
818
575
  }
819
- interface ComponentScore {
576
+ interface ComponentScore {
577
+ filePath: string;
578
+ rawScore: number;
579
+ componentScore: number;
580
+ adjustedScore: number;
581
+ componentCount: number;
582
+ }
583
+
584
+ /**
585
+ * Constitution declared by the user (or auto-detected from package.json).
586
+ * Each allow-list field is a list of canonical names — agents and
587
+ * reviewers can match against import strings without needing the
588
+ * original package name. For example, `@reduxjs/toolkit` and `redux`
589
+ * both surface as `'redux'` under `stateManagement`.
590
+ *
591
+ * `forbidden` is the deny-list: bare package specifiers and `@scope/`
592
+ * prefixes that the project has decided are off-limits. Matched against
593
+ * imports during `slop_check_constitution` and the `slopbrick drift`
594
+ * command.
595
+ */
596
+ interface Constitution {
597
+ /** State-management libraries, e.g. 'zustand', 'redux', 'jotai'. */
598
+ stateManagement?: string[];
599
+ /** Data-fetching / server-state libraries, e.g. 'react-query', 'swr'. */
600
+ dataFetching?: string[];
601
+ /** UI component libraries, e.g. 'shadcn', 'radix', 'mui'. */
602
+ uiLibrary?: string[];
603
+ /** Form + schema-validation libraries, e.g. 'react-hook-form', 'zod'. */
604
+ forms?: string[];
605
+ /** Styling solutions, e.g. 'tailwind', 'styled-components', 'emotion'. */
606
+ styling?: string[];
607
+ /** Routing libraries, e.g. 'next', 'react-router', 'tanstack-router'. */
608
+ routing?: string[];
609
+ /** Free-form user-declared categories, keyed by category name. */
610
+ custom?: Record<string, string[]>;
611
+ /**
612
+ * Explicit deny-list of bare package specifiers and `@scope/`
613
+ * prefixes. Anything in this list is forbidden — even if it would
614
+ * otherwise match a canonical category. Matching rules (see
615
+ * `matchForbidden`):
616
+ * - `forbidden: ['moment']` matches `moment` exactly.
617
+ * - `forbidden: ['@types/']` matches any `@types/...` import.
618
+ * - `forbidden: ['lodash']` matches `lodash` but not `lodash-es`.
619
+ */
620
+ forbidden?: string[];
621
+ }
622
+
623
+ interface MagicNumberSpacingConfig {
624
+ ignoreProperties?: string[];
625
+ ignoreZero?: boolean;
626
+ }
627
+ interface RuleContext {
628
+ config: ResolvedConfig;
629
+ filePath: string;
630
+ cwd: string;
631
+ framework?: string;
632
+ uiLibraries?: string[];
633
+ hasTailwind?: boolean;
634
+ supportsRsc?: boolean;
635
+ hotspotIssues?: Issue[];
636
+ }
637
+ interface Rule<Context = unknown> {
638
+ id: string;
639
+ category: Category;
640
+ severity: Severity;
641
+ aiSpecific: boolean;
642
+ /** Round 25: short description shown by `slopbrick rules` and used in docs. */
643
+ description?: string;
644
+ create(context: RuleContext): Context;
645
+ analyze(context: Context, facts: ScanFacts): Issue[];
646
+ }
647
+ interface ResolvedConfig {
648
+ framework?: string;
649
+ hasTailwind?: boolean;
650
+ supportsRsc?: boolean;
651
+ uiLibraries?: string[];
652
+ include: string[];
653
+ exclude: string[];
654
+ rules: Record<string, RuleSeverity | 'off'>;
655
+ categoryWeights?: Record<Category, number>;
656
+ /**
657
+ * Defaults to { boundary: 0.40, context: 0.35, visual: 0.25 } when unset. */
658
+ compositeWeights?: {
659
+ boundary: number;
660
+ context: number;
661
+ visual: number;
662
+ };
663
+ frameworkMultipliers: Record<string, number>;
664
+ ruleConfig: Record<string, unknown>;
665
+ gapTokens?: string[];
666
+ globalCssTarget?: string;
667
+ projectMemory?: boolean;
668
+ telemetry?: boolean;
669
+ thresholds: {
670
+ meanSlop: number;
671
+ p90Slop: number;
672
+ individualSlopThreshold: number;
673
+ /** Round 20: per-category thresholds (security fails, typo warns). Optional. */
674
+ categoryThresholds?: Partial<Record<Category, number>>;
675
+ };
676
+ spacingScale?: number[];
677
+ /**
678
+ * Declared border-radius scale in rem (Tailwind default) plus the
679
+ * string 'full' for the 9999px utility. Numeric values that don't
680
+ * fall on the scale (after px→rem conversion) trigger the
681
+ * `visual/radius-scale-violation` rule.
682
+ */
683
+ radiusScale?: (number | 'full')[];
684
+ typographyScale?: string[];
685
+ arbitraryValueAllowlist: (string | RegExp)[];
686
+ clampAllowlist?: (string | RegExp)[];
687
+ /** Phase 2 §10: allowed import paths from brick.config.json. Imports
688
+ * from `@/components/*` not matching these patterns are flagged by
689
+ * `context/import-path-mismatch`. */
690
+ allowedImports?: string[];
691
+ wcag: {
692
+ targetSizeExemptSelectors: string[];
693
+ targetSizeRequireTailwind?: boolean;
694
+ };
695
+ /**
696
+ * Declared repository constitution: "this repo uses Zustand for
697
+ * state, shadcn for UI, react-query for data fetching" plus an
698
+ * explicit deny-list of forbidden packages. Drives the `slop_suggest`
699
+ * and `slop_check_constitution` MCP tools and the
700
+ * `slopbrick drift` reporter. Auto-detected from package.json when
701
+ * unset; user declarations always win (including explicit empty
702
+ * arrays, which mean "we deliberately don't use this category").
703
+ */
704
+ constitution?: Constitution;
705
+ /**
706
+ * PR slop score threshold (Phase 11). `slopbrick pr` exits 1 when
707
+ * the PR introduces more than this many weighted slop points across
708
+ * the changed files. Default: 20. Lower it to fail PRs that add any
709
+ * meaningful slop; raise it to be more permissive. Overridden on
710
+ * the CLI by `--threshold <n>`.
711
+ */
712
+ prScoreThreshold?: number;
713
+ /**
714
+ * Phase 5 — Test Intelligence opt-in toggles. The four `test/*`
715
+ * rules are safe-by-default (each rule short-circuits on non-test
716
+ * files via `isTestFile()`), but `test/missing-edge-case` walks
717
+ * production code to find untested branches. Off by default —
718
+ * opt-in per project.
719
+ */
720
+ testIntelligence?: {
721
+ /** Walk production AST to find branches without test coverage. */
722
+ missingEdgeCase?: boolean;
723
+ };
724
+ }
725
+
726
+ /**
727
+ * v0.15.0+: The user-facing 3-bucket taxonomy. Maps engine verdicts
728
+ * (6 values, used for LR math and calibration) to UI buckets (3 values,
729
+ * used in the report and JSON output).
730
+ *
731
+ * Per the user's design intent ("INVERTED is basically HYGIENE"), INVERTED
732
+ * rules are in the same UI bucket as HYGIENE rules. The engine still
733
+ * distinguishes them (INVERTED has LR < 1, HYGIENE has LR ≈ 1).
734
+ */
735
+ type Bucket = 'ai' | 'hygiene' | 'suppressed';
736
+
737
+ /**
738
+ * v0.18.4 (Phase B R-M2): types split.
739
+ * Module: report
740
+ */
741
+ /**
742
+ * Runtime shape of `.slopbrick/health.json`. The on-disk JSON
743
+ * matches `packages/core/schemas/v1/health.schema.json` (v3); this
744
+ * interface is the slopbrick-side runtime companion. Schema is the
745
+ * source of truth — keep this in sync with the codegen output in
746
+ * `packages/core/src/generated/health.ts` (RepositoryMemoryHealth).
747
+ *
748
+ * v3 dropped the single `slopIndex` headline in favor of four named
749
+ * scores:
750
+ * - `aiQuality` — AI-specific findings (USEFUL + OK verdicts)
751
+ * - `engineeringHygiene` — HYGIENE + INVERTED rules
752
+ * - `security` — security/* rules
753
+ * - `repositoryHealth` — composite (0.5*aiQ + 0.3*eng + 0.2*sec)
754
+ *
755
+ * Plus optional verdict + bucket distributions for agent-readable
756
+ * insight.
757
+ */
758
+ interface HealthFile {
759
+ version: typeof _usebrick_core.STRUCTURE_SCHEMA_VERSION;
760
+ generatedAt: string;
761
+ workspace: string;
762
+ aiQuality: number;
763
+ engineeringHygiene: number;
764
+ security: number;
765
+ repositoryHealth: number;
766
+ verdictDistribution?: Record<_usebrick_core.Verdict, number>;
767
+ bucketDistribution?: Record<Bucket, number>;
768
+ categoryScores: Record<string, number>;
769
+ issueCounts: {
770
+ high: number;
771
+ medium: number;
772
+ low: number;
773
+ };
774
+ constitutionDrift?: number;
775
+ topOffenseIds?: string[];
776
+ scanDurationMs?: number;
777
+ }
778
+ /**
779
+ * Categorical bucket for the AI Maintenance Cost score.
780
+ *
781
+ * low — ship as-is, revisit quarterly
782
+ * medium — schedule cleanup work
783
+ * high — block new feature work in the affected subsystem
784
+ * critical — dedicated refactor sprint required
785
+ *
786
+ * Deliberately a categorical label (not numeric). Same reasoning as
787
+ * `aiSecurityRisk`: a single bucket is harder to game than a number,
788
+ * and a manager can read "AI Maintenance Cost: HIGH" in two seconds.
789
+ *
790
+ * The numeric `health` (0-100, higher = better) and `monthlyUSD`
791
+ * (estimated monthly cost to fix the underlying issues) are exposed
792
+ * alongside the label for agents and trend pipelines that need them.
793
+ */
794
+ type AiMaintenanceCost = 'low' | 'medium' | 'high' | 'critical';
795
+ /** Per-axis contribution to the AI Maintenance Cost score. */
796
+ interface MaintenanceAxisHealth {
797
+ /** Stable axis name (`slopIndex`, `architectureConsistency`, `aiSecurityRisk`, ...). */
798
+ axis: string;
799
+ /** Display label printed by the pretty reporter. */
800
+ label: string;
801
+ /** 0-100, higher = better. Derived from the underlying signal. */
802
+ health: number;
803
+ /** How this was derived, e.g. `100 - slopIndex (inverted)`. */
804
+ source: string;
805
+ }
806
+ /** Inputs to the pure `computeAiMaintenanceCost` function. Every axis is optional. */
807
+ interface MaintenanceAxes {
808
+ /** v0.15.0 U.4+: 0-100, higher = better. The new headline score
809
+ * that replaces slopIndex. Tests and callers should pass this
810
+ * going forward. */
811
+ aiQuality?: number;
812
+ /** v0.15.0 U.4+: 0-100, higher = better. */
813
+ engineeringHygiene?: number;
814
+ /** v0.15.0 U.4+: 0-100, higher = better. */
815
+ security?: number;
816
+ /** v0.15.0 U.4+: 0-100, higher = better. */
817
+ repositoryHealth?: number;
818
+ /** 0-100, lower = better. @deprecated v0.15.0: use aiQuality. Kept
819
+ * for backward compat with existing test fixtures and historical
820
+ * telemetry. The axis inverts it internally. */
821
+ slopIndex?: number;
822
+ /** 0-100, higher = better. From `buildArchitectureScore`. */
823
+ architectureConsistency?: number;
824
+ /** Categorical — mapped to numeric via `MAINTENANCE_SECURITY_NUMERIC`. */
825
+ aiSecurityRisk?: 'low' | 'medium' | 'high' | 'critical';
826
+ /** Total constitution violations (sum across categories). */
827
+ constitutionViolations?: number;
828
+ /** Spacing + radius scale violation counts. */
829
+ designTokenDrift?: {
830
+ spacing: number;
831
+ radius: number;
832
+ };
833
+ /** Total high-severity issues across the scan. Extra penalty. */
834
+ highSeverityIssueCount?: number;
835
+ /** Approximate LoC; defaults to 0 if unknown (small-project path). */
836
+ linesOfCode?: number;
837
+ /** False when the user has not declared a constitution / design tokens
838
+ * (no drift run). When true, applies the 1.5–2.5× AI multiplier. */
839
+ hasAiSignals?: boolean;
840
+ }
841
+ /** Result returned by `computeAiMaintenanceCost` + `computeAiMaintenanceCostFromReport`. */
842
+ interface AiMaintenanceCostResult {
843
+ /** Categorical headline. */
844
+ cost: AiMaintenanceCost;
845
+ /** Weighted health in 0-100 (higher = better). Bucket is derived from this. */
846
+ health: number;
847
+ /** Estimated monthly USD cost to fix the underlying issues.
848
+ * Anchored to Sonar's $306K/yr/MLoC baseline ($25.50/1k LoC/month)
849
+ * with the CodeClimate grade→minutes mapping for per-issue cost. */
850
+ monthlyUSD: number;
851
+ /** Per-axis breakdown, sorted by health ascending (worst first). */
852
+ axes: MaintenanceAxisHealth[];
853
+ /** Per-bucket advice for managers. */
854
+ advice: string;
855
+ }
856
+ /**
857
+ * Categorical drift band on the doc-freshness score.
858
+ * low — 80-100
859
+ * medium — 60-79
860
+ * high — 40-59
861
+ * critical — 0-39
862
+ *
863
+ * Drives the `--strict` exit code for `slopbrick docs` (high/critical = 1).
864
+ */
865
+ type DocDriftLevel = 'low' | 'medium' | 'high' | 'critical';
866
+ /** A single doc-drift finding — one rule firing on one line. */
867
+ interface DocFinding {
868
+ ruleId: 'docs/stale-package-reference' | 'docs/stale-function-reference' | 'docs/expired-code-example' | 'docs/broken-link';
869
+ severity: 'low' | 'medium' | 'high';
870
+ docFile: string;
871
+ line: number;
872
+ column: number;
873
+ message: string;
874
+ advice: string;
875
+ /** For `stale-package-reference`: the package the doc references. */
876
+ package?: string;
877
+ /** For `stale-function-reference`: the identifier. */
878
+ identifier?: string;
879
+ /** For `broken-link`: the unresolved link target. */
880
+ link?: string;
881
+ }
882
+ /**
883
+ * Categorical drift band on the db-health score.
884
+ * low — 80-100
885
+ * medium — 60-79
886
+ * high — 40-59
887
+ * critical — 0-39
888
+ */
889
+ type DbDriftLevel = 'low' | 'medium' | 'high' | 'critical';
890
+ /** A single db-health finding — one rule firing on one table/file. */
891
+ interface DbFinding {
892
+ ruleId: 'db/missing-fk-index' | 'db/duplicate-index' | 'db/missing-not-null' | 'db/enum-sprawl' | 'db/naming-inconsistency' | 'db/sql-concat';
893
+ severity: 'low' | 'medium' | 'high';
894
+ dbFile: string;
895
+ line: number;
896
+ column: number;
897
+ message: string;
898
+ advice: string;
899
+ /** For rules that target a specific table. */
900
+ table?: string;
901
+ /** For column-level rules. */
902
+ columnName?: string;
903
+ }
904
+ /**
905
+ * Letter-grade band on the composite Repository Health score.
906
+ * Drives the `aiDebt` field — same categorical shape as `aiSecurityRisk`.
907
+ *
908
+ * low — 80-100 (clean; safe to ship)
909
+ * medium — 60-79 (manageable; revisit quarterly)
910
+ * high — 40-59 (block new feature work in affected subsystems)
911
+ * critical — 0-39 (dedicated refactor sprint required)
912
+ */
913
+ type AiDebt = 'low' | 'medium' | 'high' | 'critical';
914
+ /** Numeric mapping for categorical `aiSecurityRisk` used in the composite. */
915
+ declare const AI_SECURITY_NUMERIC: Record<'low' | 'medium' | 'high' | 'critical', number>;
916
+ /** Inputs to the pure `buildRepositoryHealth` function. Every input is optional. */
917
+ interface RepositoryHealthInputs {
918
+ /** v0.15.0 U.4+: 0-100, higher = better. The new headline score
919
+ * that replaces slopIndex. Tests and callers should pass this
920
+ * going forward. */
921
+ aiQuality?: number;
922
+ /** v0.15.0 U.4+: 0-100, higher = better. */
923
+ engineeringHygiene?: number;
924
+ /** v0.15.0 U.4+: 0-100, higher = better. */
925
+ security?: number;
926
+ /** v0.15.0 U.4+: 0-100, higher = better. */
927
+ repositoryHealth?: number;
928
+ /** 0-100, lower = better. Inverted to 100 - x for the composite.
929
+ * @deprecated v0.15.0: use aiQuality. Kept for backward compat. */
930
+ slopIndex?: number;
931
+ /** 0-100, higher = better. */
932
+ architectureConsistency?: number;
933
+ /** Categorical — mapped via AI_SECURITY_NUMERIC. */
934
+ aiSecurityRisk?: 'low' | 'medium' | 'high' | 'critical';
935
+ /** Spacing + radius scale violation counts. */
936
+ designTokenViolations?: {
937
+ spacing: number;
938
+ radius: number;
939
+ };
940
+ /** Total high-severity issue count (extra penalty). */
941
+ highSeverityIssueCount?: number;
942
+ /** 0-100, higher = better. From `slopbrick test` (Phase 5). */
943
+ testQuality?: number;
944
+ /** 0-100, higher = better. From `slopbrick business-logic` (Phase 7). */
945
+ businessLogicCoherence?: number;
946
+ /** 0-100, higher = better. From `slopbrick docs` (Phase 6). */
947
+ docFreshness?: number;
948
+ /** 0-100, higher = better. From `slopbrick db` (Phase 8). */
949
+ dbHealth?: number;
950
+ /** v0.10 — MDL log-likelihood ratio from `computeMDLikelihood`.
951
+ * Positive = rule-firing evidence favors m_ai; negative favors
952
+ * m_human. Surfaced on the result for reporting; NOT folded into
953
+ * the weighted-average composite (which is heuristic and remains
954
+ * unchanged in this phase). Caller computes the ratio from the
955
+ * distinct `ruleId` set across the report's issues. */
956
+ mdlLogRatio?: number;
957
+ }
958
+ /** Result returned by `buildRepositoryHealth` + `buildRepositoryHealthFromReport`. */
959
+ interface RepositoryHealth {
960
+ /** Composite 0-100 score (higher = better). */
961
+ score: number;
962
+ /** Letter-grade band derived from `score`. */
963
+ aiDebt: AiDebt;
964
+ /** Per-axis contribution breakdown (axis → 0-100, higher = better). */
965
+ breakdown: Record<string, number>;
966
+ /** Per-axis weight actually applied (post-renormalization). */
967
+ appliedWeights: Record<string, number>;
968
+ /** Warnings for managers (e.g. "Critical security risk overrides everything"). */
969
+ warnings: string[];
970
+ /** One-line summary. */
971
+ headline: string;
972
+ /** v0.10 — MDL log-likelihood ratio (positive = m_ai evidence).
973
+ * Surfaced when the caller passed `mdlLogRatio` to
974
+ * `buildRepositoryHealth`. Not folded into the weighted-average
975
+ * composite in this phase (kept separate so the heuristic score
976
+ * can be audited independently of the MDL axis). */
977
+ mdlLogRatio?: number;
978
+ }
979
+ /** Default weights — sum to 1.0. Optional axes are skipped, and the
980
+ * remaining weights renormalize to 1.0. Tuned to give `aiSecurityRisk`
981
+ * the heaviest weight (it is the catastrophic single-event dimension)
982
+ * while still letting slopIndex + architecture drive most cases. */
983
+ declare const REPOSITORY_HEALTH_WEIGHTS: {
984
+ readonly slopIndex: 0.2;
985
+ readonly architectureConsistency: 0.2;
986
+ readonly aiSecurityRisk: 0.2;
987
+ readonly designTokenViolations: 0.1;
988
+ readonly testQuality: 0.1;
989
+ readonly businessLogicCoherence: 0.1;
990
+ readonly docFreshness: 0.05;
991
+ readonly dbHealth: 0.05;
992
+ };
993
+
994
+ interface TopOffender {
995
+ filePath: string;
996
+ /** Count of issues attributed to this file (from per-file results). */
997
+ issueCount: number;
998
+ /** Adjusted score for the file (post-baseline, post-framework-multiplier). */
999
+ adjustedScore: number;
1000
+ }
1001
+
1002
+ type BusinessLogicCategory = 'pricing' | 'validation' | 'formatting';
1003
+ interface BusinessLogicIssue {
1004
+ category: BusinessLogicCategory;
820
1005
  filePath: string;
821
- rawScore: number;
822
- componentScore: number;
823
- adjustedScore: number;
824
- componentCount: number;
1006
+ line: number;
1007
+ column: number;
1008
+ ruleId: string;
1009
+ message: string;
1010
+ advice?: string;
825
1011
  }
1012
+
1013
+ interface CategoryDeduction {
1014
+ category: string;
1015
+ count: number;
1016
+ /** Weight per unit (per-extra or per-N-findings). */
1017
+ weight: number;
1018
+ /** Total deduction applied to the score. */
1019
+ deduction: number;
1020
+ /** Human-readable summary, e.g. "3 modal systems, 2 beyond baseline". */
1021
+ summary: string;
1022
+ /** Concrete findings (filenames, category labels, etc.). */
1023
+ findings: string[];
1024
+ }
1025
+
1026
+ /**
1027
+ * v0.18.4 (Phase B R-M2): types split.
1028
+ * Module: baseline
1029
+ *
1030
+ * Cross-module deps: `Category` + `Severity` from `./primitives`,
1031
+ * `Issue` from `./scan`.
1032
+ */
1033
+
826
1034
  interface BaselineMeta {
827
1035
  active: boolean;
828
1036
  version: string;
829
1037
  baselineRevision: number;
830
1038
  createdAt: string;
831
1039
  }
1040
+ interface BaselineCache {
1041
+ version: string;
1042
+ config_hash: string;
1043
+ git_head: string;
1044
+ baseline_created: string;
1045
+ baseline_revision: number;
1046
+ totalComponentCount: number;
1047
+ scores: Record<string, {
1048
+ baselineScore: number;
1049
+ componentCount: number;
1050
+ }>;
1051
+ }
1052
+ interface SlopAuditRun {
1053
+ timestamp: string;
1054
+ version: string;
1055
+ slopIndex: number;
1056
+ categoryScores: Record<Category, number>;
1057
+ topOffenseIds: string[];
1058
+ thresholdExceeded: boolean;
1059
+ }
1060
+ interface AutoTunedRule {
1061
+ ruleId: string;
1062
+ severity: Severity;
1063
+ reason: string;
1064
+ }
1065
+ interface RuleSuggestion {
1066
+ pattern: string;
1067
+ example: string;
1068
+ count: number;
1069
+ suggestedRuleId: string;
1070
+ }
1071
+ /**
1072
+ * Optional research/flywheel metrics. Surfaced in the JSON report and the
1073
+ * flywheel summary when present. Generated by the
1074
+ * `research generate | analyze | candidates` pipeline.
1075
+ */
1076
+ interface ResearchMetrics {
1077
+ generatedSampleCount: number;
1078
+ generatedRuleCoverage: number;
1079
+ /** Number of fingerprint clusters that turned into candidate rules. */
1080
+ candidateYield: number;
1081
+ /** When the research artifacts were last refreshed. */
1082
+ updatedAt: string;
1083
+ }
1084
+ interface FlywheelState {
1085
+ version: string;
1086
+ updatedAt: string;
1087
+ autoTuned: AutoTunedRule[];
1088
+ research?: ResearchMetrics;
1089
+ }
1090
+ interface FlywheelOutput {
1091
+ autoTuned: AutoTunedRule[];
1092
+ hotspotIssues: Issue[];
1093
+ suggestions: RuleSuggestion[];
1094
+ research?: ResearchMetrics;
1095
+ }
1096
+
832
1097
  interface ProjectReport {
833
1098
  version: string;
834
1099
  generatedAt: string;
@@ -1085,171 +1350,6 @@ interface ProjectReport {
1085
1350
  * previousSlopIndex so the delta line can say "vs 2026-06-27". */
1086
1351
  previousRunTimestamp?: string;
1087
1352
  }
1088
- interface TopOffender {
1089
- filePath: string;
1090
- /** Count of issues attributed to this file (from per-file results). */
1091
- issueCount: number;
1092
- /** Adjusted score for the file (post-baseline, post-framework-multiplier). */
1093
- adjustedScore: number;
1094
- }
1095
- interface BaselineCache {
1096
- version: string;
1097
- config_hash: string;
1098
- git_head: string;
1099
- baseline_created: string;
1100
- baseline_revision: number;
1101
- totalComponentCount: number;
1102
- scores: Record<string, {
1103
- baselineScore: number;
1104
- componentCount: number;
1105
- }>;
1106
- }
1107
- interface SlopAuditRun {
1108
- timestamp: string;
1109
- version: string;
1110
- slopIndex: number;
1111
- categoryScores: Record<Category, number>;
1112
- topOffenseIds: string[];
1113
- thresholdExceeded: boolean;
1114
- }
1115
- interface AutoTunedRule {
1116
- ruleId: string;
1117
- severity: Severity;
1118
- reason: string;
1119
- }
1120
- interface RuleSuggestion {
1121
- pattern: string;
1122
- example: string;
1123
- count: number;
1124
- suggestedRuleId: string;
1125
- }
1126
- /**
1127
- * Optional research/flywheel metrics. Surfaced in the JSON report and the
1128
- * flywheel summary when present. Generated by the
1129
- * `research generate | analyze | candidates` pipeline.
1130
- */
1131
- interface ResearchMetrics {
1132
- generatedSampleCount: number;
1133
- generatedRuleCoverage: number;
1134
- /** Number of fingerprint clusters that turned into candidate rules. */
1135
- candidateYield: number;
1136
- /** When the research artifacts were last refreshed. */
1137
- updatedAt: string;
1138
- }
1139
- interface FlywheelState {
1140
- version: string;
1141
- updatedAt: string;
1142
- autoTuned: AutoTunedRule[];
1143
- research?: ResearchMetrics;
1144
- }
1145
- interface FlywheelOutput {
1146
- autoTuned: AutoTunedRule[];
1147
- hotspotIssues: Issue[];
1148
- suggestions: RuleSuggestion[];
1149
- research?: ResearchMetrics;
1150
- }
1151
- interface MagicNumberSpacingConfig {
1152
- ignoreProperties?: string[];
1153
- ignoreZero?: boolean;
1154
- }
1155
- interface RuleContext {
1156
- config: ResolvedConfig;
1157
- filePath: string;
1158
- cwd: string;
1159
- framework?: string;
1160
- uiLibraries?: string[];
1161
- hasTailwind?: boolean;
1162
- supportsRsc?: boolean;
1163
- hotspotIssues?: Issue[];
1164
- }
1165
- interface Rule<Context = unknown> {
1166
- id: string;
1167
- category: Category;
1168
- severity: Severity;
1169
- aiSpecific: boolean;
1170
- /** Round 25: short description shown by `slopbrick rules` and used in docs. */
1171
- description?: string;
1172
- create(context: RuleContext): Context;
1173
- analyze(context: Context, facts: ScanFacts): Issue[];
1174
- }
1175
- interface ResolvedConfig {
1176
- framework?: string;
1177
- hasTailwind?: boolean;
1178
- supportsRsc?: boolean;
1179
- uiLibraries?: string[];
1180
- include: string[];
1181
- exclude: string[];
1182
- rules: Record<string, RuleSeverity | 'off'>;
1183
- categoryWeights?: Record<Category, number>;
1184
- /**
1185
- * Defaults to { boundary: 0.40, context: 0.35, visual: 0.25 } when unset. */
1186
- compositeWeights?: {
1187
- boundary: number;
1188
- context: number;
1189
- visual: number;
1190
- };
1191
- frameworkMultipliers: Record<string, number>;
1192
- ruleConfig: Record<string, unknown>;
1193
- gapTokens?: string[];
1194
- globalCssTarget?: string;
1195
- projectMemory?: boolean;
1196
- telemetry?: boolean;
1197
- thresholds: {
1198
- meanSlop: number;
1199
- p90Slop: number;
1200
- individualSlopThreshold: number;
1201
- /** Round 20: per-category thresholds (security fails, typo warns). Optional. */
1202
- categoryThresholds?: Partial<Record<Category, number>>;
1203
- };
1204
- spacingScale?: number[];
1205
- /**
1206
- * Declared border-radius scale in rem (Tailwind default) plus the
1207
- * string 'full' for the 9999px utility. Numeric values that don't
1208
- * fall on the scale (after px→rem conversion) trigger the
1209
- * `visual/radius-scale-violation` rule.
1210
- */
1211
- radiusScale?: (number | 'full')[];
1212
- typographyScale?: string[];
1213
- arbitraryValueAllowlist: (string | RegExp)[];
1214
- clampAllowlist?: (string | RegExp)[];
1215
- /** Phase 2 §10: allowed import paths from brick.config.json. Imports
1216
- * from `@/components/*` not matching these patterns are flagged by
1217
- * `context/import-path-mismatch`. */
1218
- allowedImports?: string[];
1219
- wcag: {
1220
- targetSizeExemptSelectors: string[];
1221
- targetSizeRequireTailwind?: boolean;
1222
- };
1223
- /**
1224
- * Declared repository constitution: "this repo uses Zustand for
1225
- * state, shadcn for UI, react-query for data fetching" plus an
1226
- * explicit deny-list of forbidden packages. Drives the `slop_suggest`
1227
- * and `slop_check_constitution` MCP tools and the
1228
- * `slopbrick drift` reporter. Auto-detected from package.json when
1229
- * unset; user declarations always win (including explicit empty
1230
- * arrays, which mean "we deliberately don't use this category").
1231
- */
1232
- constitution?: Constitution;
1233
- /**
1234
- * PR slop score threshold (Phase 11). `slopbrick pr` exits 1 when
1235
- * the PR introduces more than this many weighted slop points across
1236
- * the changed files. Default: 20. Lower it to fail PRs that add any
1237
- * meaningful slop; raise it to be more permissive. Overridden on
1238
- * the CLI by `--threshold <n>`.
1239
- */
1240
- prScoreThreshold?: number;
1241
- /**
1242
- * Phase 5 — Test Intelligence opt-in toggles. The four `test/*`
1243
- * rules are safe-by-default (each rule short-circuits on non-test
1244
- * files via `isTestFile()`), but `test/missing-edge-case` walks
1245
- * production code to find untested branches. Off by default —
1246
- * opt-in per project.
1247
- */
1248
- testIntelligence?: {
1249
- /** Walk production AST to find branches without test coverage. */
1250
- missingEdgeCase?: boolean;
1251
- };
1252
- }
1253
1353
 
1254
1354
  type Framework = 'react' | 'vue' | 'svelte' | 'solid' | 'qwik' | 'astro' | 'react-native' | 'expo';
1255
1355
  type StylingSolution = 'tailwind' | 'css-modules' | 'styled-components' | 'emotion' | 'panda' | 'other';