feat(selfhost): add a modular moderation-rules engine with a cross-repo violation ledger - #2746
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
gittensory-ui | 14356e3 | Commit Preview URL Branch Preview URL |
Jul 03 2026, 07:35 PM |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2746 +/- ##
==========================================
+ Coverage 96.13% 96.18% +0.04%
==========================================
Files 248 249 +1
Lines 27569 27689 +120
Branches 10012 10065 +53
==========================================
+ Hits 26503 26632 +129
+ Misses 443 434 -9
Partials 623 623
🚀 New features to boost your workflow:
|
|
Warning 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏸️ Gittensory review result - manual review recommendedReview updated: 2026-07-03 18:40:27 UTC
⏸️ Suggested Action - Manual Review
Review summary Blockers
Nits — 5 non-blocking
Concerns raised — review before merging
Review context
Contributor next steps
Signal definitions
🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by Gittensory, a quiet PR intelligence layer for OSS maintainers.
|
e4fb013 to
27d4902
Compare
27d4902 to
eea87e9
Compare
…po violation ledger Adds a single, config-driven moderation layer over the three existing per-PR anti-abuse mechanisms (contributor cap, blacklist, review-nag): every time one of them fires against a non-exempt contributor, it now counts toward that login's install-wide violation tally. At >=1 lifetime violation the contributor gets a configurable "warning" label; at the configured ban threshold (default 5) they get a "banned" label and, when enabled, are auto-added to the existing global contributor blacklist. - global_moderation_config: a new singleton table holding the whole-layer on/off switch, which of the three rules participate, label text, ban threshold, an optional violation-decay window (permanent by default), and whether a ban auto-enforces. - Per-repo overrides in repository_settings (moderationGateMode, moderationRules, moderationWarningLabel, moderationBannedLabel), wired through the full settings pipeline: migration, Drizzle schema, RepositorySettings, .gittensory.yml parsing, OpenAPI. - The violation ledger reuses the existing audit_events table (install-wide by construction, same shape review-nag's own cooldown counter already uses) rather than a new table. - A single convergence point in the PR/issue action executors: every anti-abuse close already tags itself with a closeKind (blacklist / contributor_cap / review_nag), so escalation hooks in there instead of duplicating wiring at each mechanism's own several call sites. - Fully generic: no repo-specific values are hardcoded into the engine. A self-hoster's own deployment config chooses everything; the whole layer defaults OFF until an operator opts in.
….gittensory.yml override normalizeModerationRules degrades BOTH a genuinely empty yml list and a malformed one (non-array, or every entry invalid) to the same empty array, but only the former is an intentional "opt every rule out for this repo" -- the latter is bad config that should leave the DB value untouched, not silently disable moderation for the repo. Distinguish them by the raw input's own shape (a literal empty array) rather than the normalized result alone.
…ng 0102 merged upstream
…-effective rules maybeEscalateModeration counted every rule type ever recorded (Object.values(MODERATION_VIOLATION_EVENT_TYPE)) toward the ban threshold, regardless of which rules the global/per-repo config currently has enabled. A rule an operator has excluded still influenced the ban decision as long as a violation of that kind was recorded at some point (globally, or on a repo that still counts it). Scope the count to effectiveRules (already computed for the record-time gate just above) instead, so excluding a rule is an ongoing policy stance about what a contributor's standing is judged on, consistently applied at both record time and count time.
…amp the decay window Two gate-flagged defects in the moderation-rules engine: - recordModerationViolation recorded a fresh violation on every completed tracked close with no idempotency check, so a webhook redelivery or queue retry re-executing an already-recorded close could double-count it and falsely push a contributor toward the ban threshold. Now idempotent per (actor, eventType, targetKey); a duplicate returns false and the caller skips the rest of escalation. - violationDecayDays was normalized with the generic, unbounded normalizeOpenItemCap, but it feeds Date arithmetic on the live close path (Date.now() - days * 86400000).toISOString(); an unbounded value could overflow into an Invalid Date and throw a RangeError, crashing the close. Clamped to MAX_MODERATION_VIOLATION_DECAY_DAYS on both read and write, mirroring reviewNagCooldownDays' own clamping shape for the same family of day-count settings. Also fixes two non-blocking doc-comment nits the same review pass raised (a stale migration-number cross-reference, and an over-broad description of what "fails open" on malformed JSON).
…olliding 0103 merged upstream
eea87e9 to
14356e3
Compare
Summary
global_moderation_config: a new singleton table holding the whole-layer on/off switch (default off), which of the three rules participate, label text (defaultsmod:warning/mod:banned), the ban threshold, an optional violation-decay window (permanent by default, matching the existing blacklist's permanent-ban philosophy), and whether a ban auto-enforces.repository_settings(moderationGateMode,moderationRules,moderationWarningLabel,moderationBannedLabel), wired through the full settings pipeline in this same PR: migration, Drizzle schema,RepositorySettings,.gittensory.ymlparsing, OpenAPI.audit_eventstable (already install-wide by construction -- no repo/installation column at all -- the same shape review-nag's own cooldown counter already relies on) rather than adding a new table.closeaction with a matchingcloseKind, so escalation hooks in there instead of duplicating wiring at each mechanism's several call sites inqueue/processors.ts.Scope
CONTRIBUTING.md.Validation
git diff --checknpm run actionlintnpm run db:migrations:check/npm run db:schema-drift:checknpm run typecheck/npm run ui:typechecknpm run test:coverage-- 100% line + branch coverage on every changed line across all 8 touchedsrc/**files (verified via direct lcov cross-reference against the diff hunks, not just the aggregate report)npm run ui:openapi:check/npm run ui:openapi:settings-paritynpm audit --audit-level=moderate.gittensory.ymllayering, and 14 executor-level integration scenarios including the exact "4 violations -> warning only, 5th -> banned + auto-blacklist" escalation path, decay-window scoping, per-repo gate/rule/label overrides, dry-run/denied-close no-ops, and double-add prevention.Safety
upsertGlobalContributorBlacklist) rather than adding a new enforcement mechanism, and is itself gated behind an explicit opt-in (autoBlacklistOnBan, plus the whole layer's own off-by-default master switch).Notes