Skip to content

feat(drift): make staleness thresholds configurable (#6) - #35

Merged
theDakshJaitly merged 2 commits into
mex-memory:mainfrom
mvanhorn:osc/6-configurable-staleness-thresholds
Apr 23, 2026
Merged

feat(drift): make staleness thresholds configurable (#6)#35
theDakshJaitly merged 2 commits into
mex-memory:mainfrom
mvanhorn:osc/6-configurable-staleness-thresholds

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Closes #6.

Summary

Thresholds used by the staleness checker (warnDays / errorDays / warnCommits / errorCommits) were hardcoded at 30 / 90 / 50 / 200. Fast-moving projects want a tighter bar; stable projects want a looser one. They're now configurable via three precedence layers (highest first):

  1. CLI flags on mex check:
    • --stale-warn-days <n> / --stale-error-days <n>
    • --stale-warn-commits <n> / --stale-error-commits <n>
  2. Config file (.mex/config.json) under a new staleness key:
    { "aiTools": ["claude"], "staleness": { "warnDays": 14, "errorDays": 30 } }
    Partial overrides are allowed — any missing field falls back to the default.
  3. Defaults — exported as DEFAULT_STALENESS_THRESHOLDS (30 / 90 / 50 / 200), unchanged from the previous constants.

Why four flags, not two

The issue's example (--stale-days 14 --stale-commits 20) suggests two knobs, but the checker has four thresholds (warn + error for each of days and commits) and the relationship between them isn't a clean multiplier (90/30=3, 200/50=4). Exposing all four keeps the CLI honest and lets users tighten just the warn bar without moving the error bar. Happy to collapse to two if you'd prefer — the shape of the data flow is unchanged either way.

Internals

  • StalenessThresholds added to src/types.ts.
  • MexConfig.stalenessThresholds is optional; findConfig loads it from .mex/config.json via a new loadStalenessThresholds helper that validates each field as a non-negative number and falls back to defaults for any missing field.
  • checkStaleness(...) takes thresholds as an optional fourth arg, default DEFAULT_STALENESS_THRESHOLDS.
  • runDriftCheck threads config.stalenessThresholds through to the checker.
  • mex check validates flag values via commander's InvalidArgumentError so --stale-warn-days foo fails fast with a clear message.

Tests

test/staleness.test.ts — 8 new cases, all passing:

  • DEFAULT_STALENESS_THRESHOLDS exports the documented defaults (30/90/50/200).
  • Silent when the file is fresh.
  • Warnings at the default warn bar (31d / 60c).
  • Errors at the default error bar (120d / 300c), with thresholds printed in the message.
  • Custom warnDays=14 triggers at 15d with a threshold: 14d message.
  • Custom errorDays=30 triggers at 45d with a threshold: 30d message.
  • Custom commit-only thresholds isolate from day thresholds.
  • Silent when custom thresholds are raised above the reality.

All 102 tests pass. npm run typecheck and npm run build also succeed.

Not in scope

  • Exposing thresholds to mex setup / mex sync. If you want mex setup to prompt for staleness preferences, that's a separate UX change.
  • Per-file overrides. Current config is repo-wide.

@theDakshJaitly theDakshJaitly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this @mvanhorn — overall really nice shape. A few things before I can merge.

The main blocker: this conflicts with your own PR #34.

#34 refactored checkStaleness to collapse the day and commit signals into a single compound issue (fixing the double-score-penalty bug in #31). This PR is built on top of the old four-branch if/else and just parameterizes the constants. Two concrete consequences after #34 merges:

  1. Merge conflict in src/drift/checkers/staleness.ts — both PRs rewrite the same block.

  2. Two tests in test/staleness.test.ts will fail because they assume the pre-#34 behavior:

    • "emits warnings at the default warn thresholds" — asserts toHaveLength(2) for days=31 + commits=60
    • "emits errors at the default error thresholds" — asserts toHaveLength(2) for days=120 + commits=300

    Both become toHaveLength(1) with a compound message under #34's model.

I'd like to merge #34 first, then have you rebase this on top and update those two tests to match the collapsed-issue behavior.

Smaller things while you're in there (not blocking):

  • In src/config.ts, loadStalenessThresholds hardcodes the fallbacks 30/90/50/200 inline. Could you import DEFAULT_STALENESS_THRESHOLDS from the staleness checker and spread that instead? Keeps the defaults defined in exactly one place.
  • No invariant check that errorDays >= warnDays (same for commits). A misconfigured warnDays: 90, errorDays: 30 silently makes the warn path unreachable. Not a regression vs. the hardcoded values, but since the config is now user-facing, a small validation in loadStalenessThresholds (warn once and fall back to defaults) would be friendlier. Happy to leave this for a follow-up if you'd rather keep the PR tight.

Things I liked:

  • Going with 4 flags instead of the 2 from the issue. The warn/error ratios aren't a clean multiplier (3x for days, 4x for commits) so collapsing would've been lossy. Good call, and the PR body explains the reasoning well.
  • Precedence chain (CLI > config > defaults) reads cleanly.
  • Non-negative integer validation via InvalidArgumentError → users get a clear error on bad input.
  • Tests use module-level vi.mock cleanly, same pattern as #34.

Let me know when you've rebased.

Thresholds (warnDays / errorDays / warnCommits / errorCommits) used to
be hardcoded in the staleness checker as 30 / 90 / 50 / 200. They are
now configurable via three precedence layers, highest first:

1. CLI flags on `mex check`:
   --stale-warn-days / --stale-error-days
   --stale-warn-commits / --stale-error-commits
2. `staleness` object in `.mex/config.json`:
   { "staleness": { "warnDays": 14, "errorDays": 30 } }
   (partial overrides fall back to defaults for any missing fields)
3. DEFAULT_STALENESS_THRESHOLDS (30 / 90 / 50 / 200)

Internals:

- `StalenessThresholds` added to `types.ts`.
- `MexConfig.stalenessThresholds` is optional; `findConfig` now loads
  it from `.mex/config.json` if present.
- `checkStaleness(...)` takes thresholds as a fourth arg with a default.
- `runDriftCheck` passes `config.stalenessThresholds` through.
- `mex check` accepts four `--stale-*` integer flags (validated via
  commander's `InvalidArgumentError`).

Tests (test/staleness.test.ts, 8 new cases):

- Defaults (30/90/50/200) exported as DEFAULT_STALENESS_THRESHOLDS.
- Silent when fresh, warnings at default warn bar, errors at default
  error bar.
- Custom warn-only, error-only, and commit-only overrides each isolate
  cleanly.
- Silent when custom thresholds raise the bar above reality.

`npm run typecheck`, `npm test` (102/102 pass), and `npm run build`
all succeed.
… invariants

- Import DEFAULT_STALENESS_THRESHOLDS from the staleness checker in
  loadStalenessThresholds instead of hardcoding 30/90/50/200. Defaults
  are now defined in exactly one place; spreading from the shared
  constant keeps partial-override behavior intact.
- Warn and fall back to defaults when errorDays < warnDays or
  errorCommits < warnCommits. A misconfigured warnDays: 90, errorDays: 30
  silently made the warn path unreachable; the invariant check surfaces
  it instead of honoring a config that disables half the checker.

Both items per @theDakshJaitly's review on mex-memory#35.
@mvanhorn
mvanhorn force-pushed the osc/6-configurable-staleness-thresholds branch from ee085f6 to 06cb4c9 Compare April 22, 2026 02:53
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Rebased and pushed, @theDakshJaitly:

  • 3da554e: rebased onto upstream/main on top of fix(staleness): collapse day + commit thresholds into a single issue #34. The two staleness tests that asserted toHaveLength(2) now assert toHaveLength(1) with a combined day+commit message, matching the compound-issue shape from fix(staleness): collapse day + commit thresholds into a single issue #34.
  • 06cb4c9: applied both non-blocking items. loadStalenessThresholds now imports DEFAULT_STALENESS_THRESHOLDS from the staleness checker and spreads from it, so the defaults live in one place. Added an invariant guard: if errorDays < warnDays or errorCommits < warnCommits, the loader warns once and falls back to defaults rather than silently disabling the warn path. 4 new tests in test/config.test.ts cover the full-load, partial-fill, and both invariant-violation cases.

112/112 tests pass locally, tsc --noEmit clean. Ready for another look when you have time.

@theDakshJaitly theDakshJaitly left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Rebase onto #34 is clean, tests match the compound-issue shape, and both non-blocking items (shared DEFAULT_STALENESS_THRESHOLDS + warn/error invariant guard) are in with coverage. CI green on 18/20/22.

@theDakshJaitly
theDakshJaitly merged commit 178b283 into mex-memory:main Apr 23, 2026
3 checks passed
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Thanks for landing the configurable thresholds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make staleness checker thresholds configurable

2 participants