slopbrick 0.39.0 → 0.40.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/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.0";
39
+ VERSION = "0.40.0";
40
40
  }
41
41
  });
42
42
 
@@ -50764,6 +50764,12 @@ function severityBump(severity) {
50764
50764
  const idx = order.indexOf(severity);
50765
50765
  return order[Math.min(idx + 1, order.length - 1)];
50766
50766
  }
50767
+ function severityRelax(severity) {
50768
+ const order = ["off", "low", "medium", "high"];
50769
+ const idx = order.indexOf(severity);
50770
+ if (idx <= 0) return "off";
50771
+ return order[idx - 1];
50772
+ }
50767
50773
  function resolveEffectiveSeverity(override, defaultSeverity) {
50768
50774
  if (override === void 0 || override === "off" || override === "auto") {
50769
50775
  return defaultSeverity;
@@ -50794,6 +50800,7 @@ function countConsecutiveTopFileAppearances(currentHash, recentTopHashes) {
50794
50800
  }
50795
50801
  function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatchedStringLiterals, config, rules) {
50796
50802
  const autoTuned = [];
50803
+ const autoRelaxed = [];
50797
50804
  const hotspotIssues = [];
50798
50805
  const suggestions = [];
50799
50806
  const ruleById = new Map(rules.map((r) => [r.id, r]));
@@ -50814,6 +50821,29 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50814
50821
  }
50815
50822
  }
50816
50823
  }
50824
+ if (runs.length >= IGNORE_THRESHOLD) {
50825
+ const window = runs.slice(-IGNORE_THRESHOLD);
50826
+ const candidateRuleIds = new Set(window.flatMap((r) => r.topOffenseIds));
50827
+ for (const ruleId of candidateRuleIds) {
50828
+ const rule = ruleById.get(ruleId);
50829
+ const defaultSeverity = rule?.severity ?? "medium";
50830
+ const currentSeverity = resolveEffectiveSeverity(
50831
+ config.rules[ruleId] ?? "auto",
50832
+ defaultSeverity
50833
+ );
50834
+ const streak = countConsecutiveTopOffenses(runs, ruleId);
50835
+ if (streak >= IGNORE_THRESHOLD) {
50836
+ autoRelaxed.push({
50837
+ ruleId,
50838
+ severity: severityRelax(currentSeverity),
50839
+ previousSeverity: currentSeverity,
50840
+ reason: `In top-3 offenses for ${streak} consecutive scans; corpus prior ${DEFAULT_RELAXATION_PRIOR.toFixed(2)} suggests the rule is high-FP for this corpus slice.`,
50841
+ defaultPrior: DEFAULT_RELAXATION_PRIOR,
50842
+ relaxedAt: (/* @__PURE__ */ new Date()).toISOString()
50843
+ });
50844
+ }
50845
+ }
50846
+ }
50817
50847
  if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
50818
50848
  for (const file of currentTopFiles) {
50819
50849
  const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
@@ -50848,13 +50878,14 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50848
50878
  });
50849
50879
  }
50850
50880
  }
50851
- return { autoTuned, hotspotIssues, suggestions };
50881
+ return { autoTuned, autoRelaxed, hotspotIssues, suggestions };
50852
50882
  }
50853
50883
  function migrateFlywheelState(state) {
50854
50884
  return {
50855
50885
  version: FLYWHEEL_VERSION,
50856
50886
  updatedAt: state.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
50857
50887
  autoTuned: state.autoTuned ?? [],
50888
+ autoRelaxed: state.autoRelaxed ?? [],
50858
50889
  research: state.research
50859
50890
  };
50860
50891
  }
@@ -50910,7 +50941,7 @@ function saveFlywheelState(cwd, state) {
50910
50941
  function hashFile(filePath) {
50911
50942
  return (0, import_node_crypto6.createHash)("sha256").update(filePath).digest("hex").slice(0, 16);
50912
50943
  }
50913
- var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
50944
+ var import_node_crypto6, import_node_fs14, import_node_path15, FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, IGNORE_THRESHOLD, DEFAULT_RELAXATION_PRIOR, FLYWHEEL_VERSION;
50914
50945
  var init_flywheel = __esm({
50915
50946
  "src/engine/flywheel.ts"() {
50916
50947
  "use strict";
@@ -50920,7 +50951,9 @@ var init_flywheel = __esm({
50920
50951
  FLYWHEEL_DIR = ".slopbrick/flywheel";
50921
50952
  STATE_FILE = "auto-tuned.json";
50922
50953
  CONSECUTIVE_THRESHOLD = 3;
50923
- FLYWHEEL_VERSION = "2";
50954
+ IGNORE_THRESHOLD = 5;
50955
+ DEFAULT_RELAXATION_PRIOR = 0.3;
50956
+ FLYWHEEL_VERSION = "3";
50924
50957
  }
50925
50958
  });
50926
50959
 
@@ -54623,6 +54656,7 @@ async function persistRun(input) {
54623
54656
  );
54624
54657
  const state = loadFlywheelState(cwd);
54625
54658
  state.autoTuned = flywheelOutput.autoTuned;
54659
+ state.autoRelaxed = flywheelOutput.autoRelaxed;
54626
54660
  state.research = loadResearchMetricsFromDisk(cwd);
54627
54661
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
54628
54662
  saveFlywheelState(cwd, state);
@@ -56903,6 +56937,13 @@ async function runScan(options, explicitPaths) {
56903
56937
  if (defaultOffRules.has(tuned.ruleId)) continue;
56904
56938
  config.rules[tuned.ruleId] = tuned.severity;
56905
56939
  }
56940
+ for (const relaxed of flywheelState.autoRelaxed) {
56941
+ if (config.rules[relaxed.ruleId] === "off") {
56942
+ continue;
56943
+ }
56944
+ if (defaultOffRules.has(relaxed.ruleId)) continue;
56945
+ config.rules[relaxed.ruleId] = relaxed.severity;
56946
+ }
56906
56947
  }
56907
56948
  const pool = new WorkerPool({
56908
56949
  config,
package/dist/index.d.cts CHANGED
@@ -1207,6 +1207,48 @@ interface AutoTunedRule {
1207
1207
  severity: Severity;
1208
1208
  reason: string;
1209
1209
  }
1210
+ /**
1211
+ * v0.40.0 (Sprint 2.1): reverse direction of the flywheel ratchet.
1212
+ *
1213
+ * The flywheel has historically been one-way — `severityBump()` raises
1214
+ * a rule's severity after 3 consecutive top-3 appearances. The
1215
+ * relaxed half tells users when the data suggests a rule is *less*
1216
+ * relevant to *this* repository than the corpus default — i.e.
1217
+ * a high-FP rule that the user keeps ignoring.
1218
+ *
1219
+ * Distinct from `AutoTunedRule` on purpose:
1220
+ * - `severity` widens to `Severity | 'off'` because the relaxation
1221
+ * ratchets past `'low'` into `'off'` (the floor). Once off, the
1222
+ * read-side skips the rule entirely (same as a user `'off'`
1223
+ * override).
1224
+ * - `reason` references the ignore-streak and the corpus prior
1225
+ * that motivated the relaxation, so users can audit the
1226
+ * decision later (read `.slopbrick/flywheel/auto-tuned.json`).
1227
+ * - `previousSeverity` makes it explicit what we ratcheted *from*
1228
+ * (vs `AutoTunedRule` whose `severity` is just the new value;
1229
+ * the bump is always `prev + 1` so the prior is implied).
1230
+ * - Schema carries `defaultPrior` so re-calibration can replay
1231
+ * the relaxation against fresh corpus data (the relaxation
1232
+ * was based on a specific prior — if the prior changes, the
1233
+ * relaxation decision may no longer hold).
1234
+ */
1235
+ interface AutoRelaxedRule {
1236
+ ruleId: string;
1237
+ /**
1238
+ * New effective severity. Floors at `'off'` (the ratchet
1239
+ * ultimate floor). Note the deliberate asymmetry with
1240
+ * `AutoTunedRule` which can never write `'off'` — the bump
1241
+ * direction saturates at `'high'`, but relaxation walks
1242
+ * out the other end.
1243
+ */
1244
+ severity: Severity | 'off';
1245
+ previousSeverity: Severity;
1246
+ reason: string;
1247
+ /** AI-corpus prevalence assumed when the relaxation was decided. */
1248
+ defaultPrior: number;
1249
+ /** When the relaxation was decided (ISO 8601). */
1250
+ relaxedAt: string;
1251
+ }
1210
1252
  interface RuleSuggestion {
1211
1253
  pattern: string;
1212
1254
  example: string;
@@ -1230,10 +1272,30 @@ interface FlywheelState {
1230
1272
  version: string;
1231
1273
  updatedAt: string;
1232
1274
  autoTuned: AutoTunedRule[];
1275
+ /**
1276
+ * v0.40.0+: rules the flywheel has *relaxed* based on observed
1277
+ * ignore behavior across consecutive scans. Mirrors `autoTuned`
1278
+ * in lifecycle and persistence shape, but represents a downward
1279
+ * (not upward) severity adjustment. The next scan reads both
1280
+ * lists and applies them on top of the corpus-default severity.
1281
+ *
1282
+ * Persisted to `.slopbrick/flywheel/auto-tuned.json` alongside
1283
+ * `autoTuned` for backward compat with v0.39.x consumers of
1284
+ * the file (they just ignore the unknown field). Migration to
1285
+ * v3 happens in `migrateFlywheelState`.
1286
+ */
1287
+ autoRelaxed: AutoRelaxedRule[];
1233
1288
  research?: ResearchMetrics;
1234
1289
  }
1235
1290
  interface FlywheelOutput {
1236
1291
  autoTuned: AutoTunedRule[];
1292
+ /**
1293
+ * v0.40.0+: relaxation candidates produced by the current scan.
1294
+ * Read by `persistRun.ts` which writes them into the next
1295
+ * persisted `FlywheelState`. Empty when no rule has hit the
1296
+ * `IGNORE_THRESHOLD` yet.
1297
+ */
1298
+ autoRelaxed: AutoRelaxedRule[];
1237
1299
  hotspotIssues: Issue[];
1238
1300
  suggestions: RuleSuggestion[];
1239
1301
  research?: ResearchMetrics;
@@ -1612,4 +1674,4 @@ declare function formatBadge(report: ProjectReport): string;
1612
1674
  /** Render an array of values as a Unicode sparkline (▁▂▃▄▅▆▇█). */
1613
1675
  declare function formatSparkline(values: number[]): string;
1614
1676
 
1615
- export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
1677
+ export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoRelaxedRule, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
package/dist/index.d.ts CHANGED
@@ -1207,6 +1207,48 @@ interface AutoTunedRule {
1207
1207
  severity: Severity;
1208
1208
  reason: string;
1209
1209
  }
1210
+ /**
1211
+ * v0.40.0 (Sprint 2.1): reverse direction of the flywheel ratchet.
1212
+ *
1213
+ * The flywheel has historically been one-way — `severityBump()` raises
1214
+ * a rule's severity after 3 consecutive top-3 appearances. The
1215
+ * relaxed half tells users when the data suggests a rule is *less*
1216
+ * relevant to *this* repository than the corpus default — i.e.
1217
+ * a high-FP rule that the user keeps ignoring.
1218
+ *
1219
+ * Distinct from `AutoTunedRule` on purpose:
1220
+ * - `severity` widens to `Severity | 'off'` because the relaxation
1221
+ * ratchets past `'low'` into `'off'` (the floor). Once off, the
1222
+ * read-side skips the rule entirely (same as a user `'off'`
1223
+ * override).
1224
+ * - `reason` references the ignore-streak and the corpus prior
1225
+ * that motivated the relaxation, so users can audit the
1226
+ * decision later (read `.slopbrick/flywheel/auto-tuned.json`).
1227
+ * - `previousSeverity` makes it explicit what we ratcheted *from*
1228
+ * (vs `AutoTunedRule` whose `severity` is just the new value;
1229
+ * the bump is always `prev + 1` so the prior is implied).
1230
+ * - Schema carries `defaultPrior` so re-calibration can replay
1231
+ * the relaxation against fresh corpus data (the relaxation
1232
+ * was based on a specific prior — if the prior changes, the
1233
+ * relaxation decision may no longer hold).
1234
+ */
1235
+ interface AutoRelaxedRule {
1236
+ ruleId: string;
1237
+ /**
1238
+ * New effective severity. Floors at `'off'` (the ratchet
1239
+ * ultimate floor). Note the deliberate asymmetry with
1240
+ * `AutoTunedRule` which can never write `'off'` — the bump
1241
+ * direction saturates at `'high'`, but relaxation walks
1242
+ * out the other end.
1243
+ */
1244
+ severity: Severity | 'off';
1245
+ previousSeverity: Severity;
1246
+ reason: string;
1247
+ /** AI-corpus prevalence assumed when the relaxation was decided. */
1248
+ defaultPrior: number;
1249
+ /** When the relaxation was decided (ISO 8601). */
1250
+ relaxedAt: string;
1251
+ }
1210
1252
  interface RuleSuggestion {
1211
1253
  pattern: string;
1212
1254
  example: string;
@@ -1230,10 +1272,30 @@ interface FlywheelState {
1230
1272
  version: string;
1231
1273
  updatedAt: string;
1232
1274
  autoTuned: AutoTunedRule[];
1275
+ /**
1276
+ * v0.40.0+: rules the flywheel has *relaxed* based on observed
1277
+ * ignore behavior across consecutive scans. Mirrors `autoTuned`
1278
+ * in lifecycle and persistence shape, but represents a downward
1279
+ * (not upward) severity adjustment. The next scan reads both
1280
+ * lists and applies them on top of the corpus-default severity.
1281
+ *
1282
+ * Persisted to `.slopbrick/flywheel/auto-tuned.json` alongside
1283
+ * `autoTuned` for backward compat with v0.39.x consumers of
1284
+ * the file (they just ignore the unknown field). Migration to
1285
+ * v3 happens in `migrateFlywheelState`.
1286
+ */
1287
+ autoRelaxed: AutoRelaxedRule[];
1233
1288
  research?: ResearchMetrics;
1234
1289
  }
1235
1290
  interface FlywheelOutput {
1236
1291
  autoTuned: AutoTunedRule[];
1292
+ /**
1293
+ * v0.40.0+: relaxation candidates produced by the current scan.
1294
+ * Read by `persistRun.ts` which writes them into the next
1295
+ * persisted `FlywheelState`. Empty when no rule has hit the
1296
+ * `IGNORE_THRESHOLD` yet.
1297
+ */
1298
+ autoRelaxed: AutoRelaxedRule[];
1237
1299
  hotspotIssues: Issue[];
1238
1300
  suggestions: RuleSuggestion[];
1239
1301
  research?: ResearchMetrics;
@@ -1612,4 +1674,4 @@ declare function formatBadge(report: ProjectReport): string;
1612
1674
  /** Render an array of values as a Unicode sparkline (▁▂▃▄▅▆▇█). */
1613
1675
  declare function formatSparkline(values: number[]): string;
1614
1676
 
1615
- export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
1677
+ export { AI_SECURITY_NUMERIC, type AiDebt, type AiMaintenanceCost, type AiMaintenanceCostResult, type AstroComponentFact, type AutoRelaxedRule, type AutoTunedRule, type BaselineCache, type BaselineMeta, type CachedFile, type Category, type ClassNameFact, type CommentFact, type ComponentFacts, type ComponentScore, type ComponentSizeFact, type ConsoleCallFact, type Constitution, DEFAULT_CONFIG, type DangerouslySetInnerHtmlFact, type DbDriftLevel, type DbFinding, type DialogCallFact, type DisabledLintRuleFact, type DocDriftLevel, type DocFinding, type DomQueryFact, type ElementFact, type EvalCallFact, type ExplicitAnyFact, type FetchCallFact, type FileScanResult, type FixSuggestion, type FlywheelOutput, type FlywheelState, type Framework$1 as Framework, type HealthFile, type HookCallFact, type HookDependencyArrayFact, type HookFact, type ImportFact, type InlineEventHandlerFact, type Issue, type JsxAttributeStringLiteralFact, type JsxTextLiteralFact, type KeyPropFact, type LogicalExpressionFact, type MagicNumberSpacingConfig, type MaintenanceAxes, type MaintenanceAxisHealth, type NonNullAssertionFact, type OptimisticUpdateFact, type ProjectReport, type PropMutationFact, type PropPassThroughFact, REPOSITORY_HEALTH_WEIGHTS, type ReportReadResult, type RepositoryHealth, type RepositoryHealthInputs, type ResearchMetrics, type ResolvedConfig, type Rule, type RuleContext, type RuleSeverity, type RuleSuggestion, type ScanCache, type ScanFacts, type ScanProjectOptions, type ScanSelfScanConfig, type Severity, type SlopAuditRun, type StateBinding, type StateBindingFact, type StringLiteralFact, type StylePropFact, type TamaguiStylePropFact, type TopOffender, type UseEffectBodyFact, VERSION, baselineStatusMessage, colorForSlop, failedThresholdCount, filterByDisabledDirectives, filterIssues, formatBadge, formatReportFromFile, formatSparkline, loadConfig, readReportFile, runCli, runInitWizard, scanProject, serializeConfig, stagedGating, thresholdExceeded };
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ var VERSION;
19
19
  var init_header = __esm({
20
20
  "src/types/_header.ts"() {
21
21
  "use strict";
22
- VERSION = "0.39.0";
22
+ VERSION = "0.40.0";
23
23
  }
24
24
  });
25
25
 
@@ -50744,6 +50744,12 @@ function severityBump(severity) {
50744
50744
  const idx = order.indexOf(severity);
50745
50745
  return order[Math.min(idx + 1, order.length - 1)];
50746
50746
  }
50747
+ function severityRelax(severity) {
50748
+ const order = ["off", "low", "medium", "high"];
50749
+ const idx = order.indexOf(severity);
50750
+ if (idx <= 0) return "off";
50751
+ return order[idx - 1];
50752
+ }
50747
50753
  function resolveEffectiveSeverity(override, defaultSeverity) {
50748
50754
  if (override === void 0 || override === "off" || override === "auto") {
50749
50755
  return defaultSeverity;
@@ -50774,6 +50780,7 @@ function countConsecutiveTopFileAppearances(currentHash, recentTopHashes) {
50774
50780
  }
50775
50781
  function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatchedStringLiterals, config, rules) {
50776
50782
  const autoTuned = [];
50783
+ const autoRelaxed = [];
50777
50784
  const hotspotIssues = [];
50778
50785
  const suggestions = [];
50779
50786
  const ruleById = new Map(rules.map((r) => [r.id, r]));
@@ -50794,6 +50801,29 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50794
50801
  }
50795
50802
  }
50796
50803
  }
50804
+ if (runs.length >= IGNORE_THRESHOLD) {
50805
+ const window = runs.slice(-IGNORE_THRESHOLD);
50806
+ const candidateRuleIds = new Set(window.flatMap((r) => r.topOffenseIds));
50807
+ for (const ruleId of candidateRuleIds) {
50808
+ const rule = ruleById.get(ruleId);
50809
+ const defaultSeverity = rule?.severity ?? "medium";
50810
+ const currentSeverity = resolveEffectiveSeverity(
50811
+ config.rules[ruleId] ?? "auto",
50812
+ defaultSeverity
50813
+ );
50814
+ const streak = countConsecutiveTopOffenses(runs, ruleId);
50815
+ if (streak >= IGNORE_THRESHOLD) {
50816
+ autoRelaxed.push({
50817
+ ruleId,
50818
+ severity: severityRelax(currentSeverity),
50819
+ previousSeverity: currentSeverity,
50820
+ reason: `In top-3 offenses for ${streak} consecutive scans; corpus prior ${DEFAULT_RELAXATION_PRIOR.toFixed(2)} suggests the rule is high-FP for this corpus slice.`,
50821
+ defaultPrior: DEFAULT_RELAXATION_PRIOR,
50822
+ relaxedAt: (/* @__PURE__ */ new Date()).toISOString()
50823
+ });
50824
+ }
50825
+ }
50826
+ }
50797
50827
  if (currentTopFiles.length > 0 && runs.length >= CONSECUTIVE_THRESHOLD) {
50798
50828
  for (const file of currentTopFiles) {
50799
50829
  const consecutive = countConsecutiveTopFileAppearances(file.hash, recentTopHashes);
@@ -50828,13 +50858,14 @@ function computeFlywheelOutput(runs, currentTopFiles, recentTopHashes, unmatched
50828
50858
  });
50829
50859
  }
50830
50860
  }
50831
- return { autoTuned, hotspotIssues, suggestions };
50861
+ return { autoTuned, autoRelaxed, hotspotIssues, suggestions };
50832
50862
  }
50833
50863
  function migrateFlywheelState(state) {
50834
50864
  return {
50835
50865
  version: FLYWHEEL_VERSION,
50836
50866
  updatedAt: state.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
50837
50867
  autoTuned: state.autoTuned ?? [],
50868
+ autoRelaxed: state.autoRelaxed ?? [],
50838
50869
  research: state.research
50839
50870
  };
50840
50871
  }
@@ -50890,14 +50921,16 @@ function saveFlywheelState(cwd, state) {
50890
50921
  function hashFile(filePath) {
50891
50922
  return createHash8("sha256").update(filePath).digest("hex").slice(0, 16);
50892
50923
  }
50893
- var FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, FLYWHEEL_VERSION;
50924
+ var FLYWHEEL_DIR, STATE_FILE, CONSECUTIVE_THRESHOLD, IGNORE_THRESHOLD, DEFAULT_RELAXATION_PRIOR, FLYWHEEL_VERSION;
50894
50925
  var init_flywheel = __esm({
50895
50926
  "src/engine/flywheel.ts"() {
50896
50927
  "use strict";
50897
50928
  FLYWHEEL_DIR = ".slopbrick/flywheel";
50898
50929
  STATE_FILE = "auto-tuned.json";
50899
50930
  CONSECUTIVE_THRESHOLD = 3;
50900
- FLYWHEEL_VERSION = "2";
50931
+ IGNORE_THRESHOLD = 5;
50932
+ DEFAULT_RELAXATION_PRIOR = 0.3;
50933
+ FLYWHEEL_VERSION = "3";
50901
50934
  }
50902
50935
  });
50903
50936
 
@@ -54610,6 +54643,7 @@ async function persistRun(input) {
54610
54643
  );
54611
54644
  const state = loadFlywheelState(cwd);
54612
54645
  state.autoTuned = flywheelOutput.autoTuned;
54646
+ state.autoRelaxed = flywheelOutput.autoRelaxed;
54613
54647
  state.research = loadResearchMetricsFromDisk(cwd);
54614
54648
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
54615
54649
  saveFlywheelState(cwd, state);
@@ -56886,6 +56920,13 @@ async function runScan(options, explicitPaths) {
56886
56920
  if (defaultOffRules.has(tuned.ruleId)) continue;
56887
56921
  config.rules[tuned.ruleId] = tuned.severity;
56888
56922
  }
56923
+ for (const relaxed of flywheelState.autoRelaxed) {
56924
+ if (config.rules[relaxed.ruleId] === "off") {
56925
+ continue;
56926
+ }
56927
+ if (defaultOffRules.has(relaxed.ruleId)) continue;
56928
+ config.rules[relaxed.ruleId] = relaxed.severity;
56929
+ }
56889
56930
  }
56890
56931
  const pool = new WorkerPool({
56891
56932
  config,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopbrick",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Discovered, modeled, and governed repository structure. SlopBrick scans source code, classifies it against 103 rules in 24 categories, computes 4 scores (aiSlopScore: lower=cleaner, engineeringHygiene, security, repositoryHealth composite), and persists the structure for AI agents and CI. v10-calibrated against 576,750 real files; v0.38.0 trims to 103 rules by deleting 37 v10-DORMANT rules.",
5
5
  "type": "module",
6
6
  "bin": {