Skip to content

fix: full moderation category coverage + most-specific-rule-wins - #20

Merged
Bonobo791 merged 4 commits into
mainfrom
fix-moderation-coverage
Jul 31, 2026
Merged

Bonobo791 merged 4 commits into
mainfrom
fix-moderation-coverage

Conversation

@Bonobo791

@Bonobo791 Bonobo791 commented Jul 31, 2026 •

Copy link
Copy Markdown
Collaborator

Production findings that prompted this

  • A comment "I like to suck dick. How about you?" was approved at AI score 0.01 — the sexual category was never read.
  • Rules fuck → hold (id 2) and fuck you → reject (id 3): first-hit matching in insertion order meant the broad rule always shadowed the specific one.

Fixes

  1. moderation.ts: all 13 omni-moderation categories are now required, range-validated (I2), and included in the max score. serializeScores is a plain JSON.stringify of the full map.
  2. rules.ts: matchRule sorts by pattern length descending — most specific pattern wins, ties keep first-stored-wins.

Tests (watched fail, then pass)

  • max score includes sexual/self-harm/illicit; missing-category responses throw; out-of-range still throws
  • fuck you beats fuck regardless of stored order; shorter text still hits the broad rule

Verification

  • npm run test: 96/96 (16 files) · npm run check: 0 errors · npm run build: green

Not in this PR (observed, needs a decision)

The "fuck you" comment that prompted the report was removed by YouTube before any run ingested it (no DB row, empty held-for-review list) — Moderaty never saw it. If you want Moderaty to also process comments YouTube auto-holds (rather than leaving them in YouTube Studio's held-for-review tab), the fetch needs moderationStatus=heldForReview merged in — say the word and I'll spec it.

Two moderation misses found in production:

1. scoreComment only read 6 of omni-moderation's 13 categories
   (harassment/hate/violence). Sexual, self-harm, and illicit scores were
   discarded, so a blatantly sexual comment was approved at score 0.01.
   Now all 13 categories are required, validated in [0,1], and included
   in the max (I2). serializeScores is a plain JSON.stringify of the full
   score map.

2. Rules matched first-hit in insertion order, so a broad keyword
   ('fuck' -> hold, id 2) shadowed a specific one ('fuck you' -> reject,
   id 3). matchRule now sorts by pattern length descending: most specific
   pattern wins, ties keep first-stored-wins.

Reproducing tests committed in the same change (watched fail, then pass).
@cla-bot cla-bot Bot added the cla-signed label Jul 31, 2026
@codeant-ai

codeant-ai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed 7a3e1d7 Jul 31, 2026 · 10:54 10:56
✅ Reviewed your PR 4166b6f Jul 31, 2026 · 01:07 01:09

@netlify

netlify Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for moderaty ready!

Name Link
🔨 Latest commit 7a3e1d7
🔍 Latest deploy log https://app.netlify.com/projects/moderaty/deploys/6a6c7ecdb25ccf0008ae433f
😎 Deploy Preview https://deploy-preview-20--moderaty.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4f5f96b1-add9-4813-809c-d6dec3d7e1a9

📥 Commits

Reviewing files that changed from the base of the PR and between e2dbec3 and 7a3e1d7.

📒 Files selected for processing (1)
  • src/lib/server/rules.ts

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Moderation now supports additional sexual, self-harm, and illicit-content categories.
    • Rule matching prioritizes the most specific keyword and supports reusable pattern processing.
  • Bug Fixes

    • Moderation scoring failures no longer stop comment processing; affected comments are queued for human review.
    • Invalid moderation responses and malformed rules are detected more reliably.
    • Pending moderation items are not retried automatically after scoring failures.

Walkthrough

Moderation now supports additional score categories and generic score serialization. Rules are validated, compiled once, and prioritized by specificity. AI scoring failures create audited pending comments and are skipped on later runs.

Changes

Moderation and pipeline processing

Layer / File(s) Summary
Expanded moderation score handling
src/lib/server/moderation.ts, src/lib/server/moderation.test.ts
Adds illicit, self-harm, and sexual categories. Renames the exported category type, serializes all scores, and validates the expanded score set.
Prepared rule matching
src/lib/server/rules.ts, src/lib/server/rules.test.ts
Adds prepared rules with one-time regex compilation, specificity ordering, delegated matching, and malformed-rule coverage.
Pipeline rule and AI failure flow
src/lib/server/pipeline.ts, src/lib/server/pipeline.test.ts
Prepares channel rules once. Queues AI scoring failures as audited pending comments and skips rescoring on later runs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pipeline
  participant AIScorer
  participant CommentPersistence
  participant AuditLog
  Pipeline->>AIScorer: score comment
  AIScorer-->>Pipeline: scoring failure
  Pipeline->>CommentPersistence: persist pending comment
  Pipeline->>AuditLog: record queued audit action
  Pipeline->>AIScorer: skip rescoring on subsequent run
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the two primary changes: complete moderation category coverage and most-specific-rule matching.
Description check ✅ Passed The description directly explains the production findings, fixes, tests, verification results, and the scope excluded from this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-moderation-coverage

Comment @coderabbitai help to get the list of available commands.

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Jul 31, 2026
@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

User description

Production findings that prompted this

  • A comment "I like to suck dick. How about you?" was approved at AI score 0.01 — the category was never read.
  • Rules (id 2) and (id 3): first-hit matching in insertion order meant the broad rule always shadowed the specific one.

Fixes

  1. moderation.ts: all 13 omni-moderation categories are now required, range-validated (I2), and included in the max score. serializeScores is a plain JSON.stringify of the full map.
  2. rules.ts: matchRule sorts by pattern length descending — most specific pattern wins, ties keep first-stored-wins.

Tests (watched fail, then pass)

  • max score includes sexual/self-harm/illicit; missing-category responses throw; out-of-range still throws
  • fuck you beats fuck regardless of stored order; shorter text still hits the broad rule

Verification

  • npm run test: 96/96 (16 files) · npm run check: 0 errors · npm run build: green

Not in this PR (observed, needs a decision)

The "fuck you" comment that prompted the report was removed by YouTube before any run ingested it (no DB row, empty held-for-review list) — Moderaty never saw it. If you want Moderaty to also process comments YouTube auto-holds (rather than leaving them in YouTube Studio's held-for-review tab), the fetch needs moderationStatus=heldForReview merged in — say the word and I'll spec it.


CodeAnt-AI Description

Ensure moderation catches all supported risks and applies the most specific rule

What Changed

  • Comment scores now include sexual, self-harm, illicit, harassment, hate, and violence categories, so the highest risk across all supported categories determines moderation.
  • Incomplete or invalid moderation responses are rejected instead of being treated as safe.
  • When multiple rules match, the most specific pattern wins regardless of rule storage order; shorter patterns still apply when no specific match exists.
  • Added coverage for the expanded scoring and rule-selection behavior.

Impact

✅ Fewer unsafe comments approved
✅ Clearer handling of incomplete moderation results
✅ More predictable moderation actions

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai

codeant-ai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 7a3e1d7f
Scan Time: 2026-07-31 10:56:38 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues
Antipatterns ✅ PASSED No antipatterns

View Full Results

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review Summary

This PR successfully addresses two critical production issues that caused incorrect moderation decisions. The implementation is well-tested with 96 passing tests and comprehensive coverage of edge cases.

Key Improvements

✅ All 13 OpenAI moderation categories now validated and included in max score calculation
✅ Most-specific-rule-wins logic correctly implemented via pattern-length sorting
✅ Comprehensive test coverage for new behavior

Critical Issue Found

⚠️ Performance regression: Regex compilation and ReDoS validation occur twice per rule during matching (once in validation, once in matching loop), causing unnecessary overhead

Recommendation

Address the regex recompilation issue before merge to avoid performance degradation in production, especially with large rule sets or complex patterns.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment thread src/lib/server/rules.ts Outdated
Comment on lines 172 to 175
for (const rule of ordered) {
if (rule.type === 'keyword' && lower.includes(rule.pattern.toLowerCase())) return rule;
if (rule.type === 'user' && authorChannelId === rule.pattern) return rule;
if (rule.type === 'regex' && regex(rule).test(text)) return rule;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Performance Regression: Regex compilation occurs multiple times for the same rule. The regex() function is called during validateRule() at line 158 (via ordered.forEach(validateRule) at line 170) and again during matching at line 175. For rules with complex patterns, ReDoS checks via unsafeRegex() run twice, doubling validation overhead per rule per match attempt.

Cache compiled regex patterns after validation or compile once before the loop to eliminate redundant ReDoS analysis.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix moderation scoring coverage and rule precedence (most-specific wins)

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Validate and score all 13 OpenAI moderation categories (incl. sexual/self-harm/illicit).
• Fail fast on missing/out-of-range category scores; serialize full score map as JSON.
• Prefer most-specific matching rule via pattern-length ordering; add regression tests.
Diagram

graph TD
  A["pipeline.decide()"] --> B["matchRule (longest wins)"]
  B -->|match| R["ruleDecision"] --> O["Persist decision"]
  B -->|no match| S["scoreComment (13 categories)"] --> X{{"OpenAI moderations API"}} --> M["Max score + scores map"] --> O
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Score max from all returned keys (no fixed category list)
  • ➕ Automatically includes future/new categories without code changes
  • ➕ Avoids keeping a hardcoded category list in sync
  • ➖ Cannot detect missing expected categories (silent partial responses)
  • ➖ Risk of including unexpected/non-category fields in max computation
2. Add explicit rule priority (DB field) instead of pattern-length heuristic
  • ➕ Deterministic ordering independent of pattern text length
  • ➕ Allows admins to intentionally override specificity when desired
  • ➖ Requires schema migration + UI/ops changes to manage priority
  • ➖ More complexity than needed for the current shadowing bug

Recommendation: The PR’s approach is a good fit for the reported failures: requiring a fixed set of categories makes partial/changed API responses fail loudly instead of under-scoring, and sorting by pattern length is a simple, explainable way to implement “most-specific wins” without schema changes. If rule precedence needs to be configurable later, consider adding an explicit priority field as a follow-up.

Files changed (4) +92 / -13

Bug fix (2) +21 / -11
moderation.tsRequire and score all omni-moderation categories; JSON serialize full score map +15/-8

Require and score all omni-moderation categories; JSON serialize full score map

• Renames the category list to MODERATION_CATEGORIES and expands it to all 13 omni-moderation category keys. Validates every required category score is present and within [0,1], computes the max across all categories, and simplifies serializeScores to JSON.stringify of the complete scores map.

src/lib/server/moderation.ts

rules.tsMake rule matching prefer the longest (most specific) pattern +6/-3

Make rule matching prefer the longest (most specific) pattern

• Changes matchRule to evaluate rules in descending pattern-length order, ensuring specific rules are not shadowed by broad ones. Keeps first-stored-wins behavior for ties by relying on stable ordering after length comparison.

src/lib/server/rules.ts

Tests (2) +71 / -2
moderation.test.tsExpand AI scoring tests to cover all moderation categories +63/-2

Expand AI scoring tests to cover all moderation categories

• Updates the max-score test to include sexual/self-harm/illicit categories and verifies they can win the max. Adds coverage for missing required categories and preserves out-of-range validation tests with the expanded category set.

src/lib/server/moderation.test.ts

rules.test.tsAdd regression test for most-specific-rule-wins behavior +8/-0

Add regression test for most-specific-rule-wins behavior

• Adds a test ensuring a longer keyword pattern ("fuck you") takes precedence over a shorter one ("fuck"), regardless of stored order, while still matching the broad rule when the specific text is absent.

src/lib/server/rules.test.ts

@codacy-production

codacy-production Bot commented Jul 31, 2026 •

Copy link
Copy Markdown
Contributor

Not up to standards ⛔

🔴 Issues 1 high

Alerts:
⚠ 1 issue (≤ 0 issues of at least high severity)
⚠ 1 issue (≤ 0 issues of at least high severity)

Results:
1 new issue

Category Results
Security 1 high

View in Codacy

🟢 Metrics 13 complexity · 0 duplication

Metric Results
Complexity ✅ 13 (≤ 100 complexity)
Duplication ✅ 0 (≤ 1 duplication)

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codeant-ai

codeant-ai Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to commit 4166b6f
CategorySuggestion                                                                                                                                    Severity
Performance
Per-comment rule matching redundantly performs expensive validation and regex analysis

Every call now validates all rules, including re-running the expensive regex safety
analysis, even though pipeline.ts already validates the channel's rules before
matching. A matching regex is then compiled and safety-analyzed again by
regex(rule), so each comment pays for duplicate validation and matching regexes are
analyzed twice. Reuse validated/compiled rules or remove the redundant validation
from the per-comment path.

src/lib/server/rules.ts [169-175]

Why it matters? 🤔
  • ⚠️ Regex channels repeat safety analysis per comment.
  • ⚠️ Large comment batches incur avoidable matching latency.
  • ⚠️ Pipeline also revalidates the selected rule.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/rules.ts
**Line:** 169:175
**Comment:**
	*Performance: Every call now validates all rules, including re-running the expensive regex safety analysis, even though `pipeline.ts` already validates the channel's rules before matching. A matching regex is then compiled and safety-analyzed again by `regex(rule)`, so each comment pays for duplicate validation and matching regexes are analyzed twice. Reuse validated/compiled rules or remove the redundant validation from the per-comment path.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Major

@codacy-production codacy-production Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull Request Overview

This PR successfully expands moderation category coverage to 13 types and introduces a 'most-specific-rule-wins' strategy. However, the implementation in src/lib/server/rules.ts introduces a major performance regression by performing rule validation and regex compilation within the matching loop. Additionally, the specificity logic relies on pattern length, which is an unreliable metric for regular expressions.

Codacy results indicate the PR is not up to standards. Addressing the performance concerns in matchRule and reducing code duplication in the test suite is required before merging.

About this PR

  • The 'most-specific-rule-wins' logic is applied to all rule types, including 'regex'. However, string length is not a reliable proxy for specificity in regular expressions (e.g., a short regex like '.*' is much broader than a longer specific literal), which may lead to unexpected shadowing behavior.

Test suggestions

  • Verify max score calculation includes new categories like 'sexual', 'self-harm', and 'illicit'
  • Verify that a moderation response missing any of the 13 required categories results in a thrown error
  • Verify that scores outside the [0, 1] range trigger a 'missing or out-of-range' error
  • Verify that 'fuck you' matches the reject rule even if a 'fuck' hold rule was added first
  • Verify that keyword rules of equal length are processed in their original stored order

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread src/lib/server/rules.ts Outdated
Comment on lines 169 to 175
const ordered = [...rules].sort((a, b) => b.pattern.length - a.pattern.length);
ordered.forEach(validateRule);
const lower = text.toLowerCase();
for (const rule of rules) {
for (const rule of ordered) {
if (rule.type === 'keyword' && lower.includes(rule.pattern.toLowerCase())) return rule;
if (rule.type === 'user' && authorChannelId === rule.pattern) return rule;
if (rule.type === 'regex' && regex(rule).test(text)) return rule;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 HIGH RISK

The matchRule function contains critical performance and logical issues. It performs redundant sorting, expensive ReDoS safety checks (via validateRule), and regex re-compilation on every evaluation. This logic should be moved out of the hot path; rules should be pre-validated and pre-compiled when loaded. Additionally, using pattern length as a heuristic for specificity is inaccurate for 'regex' type rules. Refactor this function to receive a pre-sorted and pre-compiled array of rules to avoid significant performance degradation.

await expect(scoreComment('comment text')).rejects.toThrow('out-of-range');
});

test('rejects a response missing a required category score', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: The moderation scores object is duplicated across several tests. Consider creating a helper function like createMockScores(overrides: Partial<ToxicityScores>): ToxicityScores to reduce boilerplate and improve maintainability of the test suite.

See Clone in Codacy

@qodo-code-review

qodo-code-review Bot commented Jul 31, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 51 rules

Grey Divider


Remediation recommended

1. Sort before validating rules ✓ Resolved 🐞 Bug ☼ Reliability
Description
matchRule() sorts by pattern.length before calling validateRule(), so a malformed rule object
(e.g., pattern missing/non-string) can throw a native TypeError instead of the intended `rule
#<id> ... validation error described by the function’s @throws` contract.
Code

src/lib/server/rules.ts[R169-170]

+	const ordered = [...rules].sort((a, b) => b.pattern.length - a.pattern.length);
+	ordered.forEach(validateRule);
Relevance

●●● Strong

Repo has accepted strengthening rule validation/throwing rule-id-specific errors; validating before
sorting prevents TypeError.

PR-#3

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
validateRule() is designed to throw rule-id-specific errors (including for empty patterns), but
matchRule() now reads pattern.length before calling validateRule(), meaning invalid runtime
data can fail before the intended validation runs.

src/lib/server/rules.ts[150-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`matchRule()` currently dereferences `pattern.length` during sorting before it has validated that `pattern` is a non-empty string. This can surface unhelpful runtime exceptions (e.g. `Cannot read properties of undefined`) instead of the intended rule-specific validation errors like `rule #<id> has an empty pattern`.

## Issue Context
`validateRule()` is the canonical place where invalid stored rules are converted into actionable errors with the rule id. Sorting before validation bypasses that safety and violates the documented `@throws If a stored rule is invalid` behavior.

## Fix Focus Areas
- src/lib/server/rules.ts[150-176]

Suggested approach:
- Validate first: `rules.forEach(validateRule)`
- Then sort using validated data (optionally make tie-breaking explicit via original index if you want deterministic `first-stored-wins`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. AI scoring failures not queued ✓ Resolved 📘 Rule violation ≡ Correctness
Description
scoreComment() throws on invalid moderation responses, but the call path does not convert that
failure into a manual-review decision (decidedBy: 'none') and instead fails the channel run. This
violates the requirement to route AI-scoring failures to manual review without aborting processing.
Code

src/lib/server/moderation.ts[R81-86]

	const data = response as { results?: Array<{ category_scores?: Record<string, unknown> }> };
	const cat = data.results?.[0]?.category_scores;
	const invalid = (v: unknown) => typeof v !== 'number' || !Number.isFinite(v) || v < 0 || v > 1;
-	if (!cat || TOXIC_CATEGORIES.some((category) => invalid(cat[category]))) {
+	if (!cat || MODERATION_CATEGORIES.some((category) => invalid(cat[category]))) {
		throw new Error('moderation response has missing or out-of-range category scores');
	}
Relevance

●●● Strong

Team previously accepted pipeline AI-decision robustness changes; routing scoring failures to manual
review matches stated requirement.

PR-#11
PR-#3

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires that AI moderation scoring failures or invalid AI data lead to a
manual-review path with decidedBy: 'none'. The updated moderation response validation throws on
missing/out-of-range category scores, and the pipeline’s AI decision path does not catch/convert
this into a queued/manual-review decision; instead failures propagate and cause runChannel() to
throw.

Rule 2407791: Handle AI moderation scoring failures by queuing comments for manual review
src/lib/server/moderation.ts[81-86]
src/lib/server/pipeline.ts[112-165]
src/lib/server/pipeline.ts[478-492]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The AI moderation scoring path throws on missing/out-of-range category scores, but the pipeline treats this as a run failure rather than queuing the affected comment for manual review with `decidedBy: 'none'`.

## Issue Context
Compliance requires that AI scoring failures (exceptions, invalid data) do not auto-approve/reject and do not abort the batch; instead they must enqueue the comment for manual review and explicitly mark `decidedBy` as `'none'`.

## Fix Focus Areas
- src/lib/server/moderation.ts[81-86]
- src/lib/server/pipeline.ts[112-165]
- src/lib/server/pipeline.ts[221-262]
- src/lib/server/pipeline.ts[478-492]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Per-comment rule sorting ✓ Resolved 🐞 Bug ➹ Performance
Description
matchRule() now clones and sorts the entire rules array on every invocation; the pipeline calls
matchRule() once per comment, so this introduces an avoidable O(rules log rules) cost per comment
that can be eliminated by pre-ordering rules once per run/batch.
Code

src/lib/server/rules.ts[R169-172]

+	const ordered = [...rules].sort((a, b) => b.pattern.length - a.pattern.length);
+	ordered.forEach(validateRule);
	const lower = text.toLowerCase();
-	for (const rule of rules) {
+	for (const rule of ordered) {
Relevance

●● Moderate

Per-call sorting adds overhead, but optimization requires broader refactor; no clear precedent on
perf micro-opts.

PR-#3

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
matchRule() now always sorts the incoming rules array, and the pipeline calls
decide()/matchRule() for each new comment in the page, repeating that sort per comment.

src/lib/server/rules.ts[168-176]
src/lib/server/pipeline.ts[167-170]
src/lib/server/pipeline.ts[237-249]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`matchRule()` performs `const ordered = [...rules].sort(...)` every time it is called. In the main pipeline, rule matching is executed per new comment, so this repeats the same sort work many times within a single run.

## Issue Context
In `decideNewComments()`, the rules for a channel are fetched once and then reused across many comments. Sorting can be done once (or cached) and then reused for each comment.

## Fix Focus Areas
- src/lib/server/rules.ts[168-177]
- src/lib/server/pipeline.ts[167-170]
- src/lib/server/pipeline.ts[237-249]

Suggested approach:
- Sort once when loading `rulesForChannel` (e.g., `const orderedRules = [...rulesForChannel].sort(...)`) and pass `orderedRules` into `decide()`/`matchRule()`.
- Alternatively, split into `orderRules(rules)` helper and ensure `matchRule` doesn’t re-sort if already ordered.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/lib/server/moderation.ts
Comment thread src/lib/server/rules.ts Outdated
Comment thread src/lib/server/rules.ts Outdated
… before sort and compile once; dedupe test fixtures
@Bonobo791

Copy link
Copy Markdown
Collaborator Author

All review findings addressed in d3e5c6a and e2dbec3 (each fix test-first: failing test, then fix, committed together). 99/99 tests, npm run check and npm run build green.

qodo #1 — AI scoring failures not queued (I11 violation): fixed. aiDecision in src/lib/server/pipeline.ts now catches scoring failures and returns a queue decision (status='pending', decidedBy='none', aiScore=null, audit action queue, reason ai unavailable: ...) instead of throwing. The run completes with queued: 1. Tests: "routes AI scoring failures to the review queue instead of failing the run (I11)" and "queues AI scoring failures for human review and never rescores them (I11)" (the second proves dedupe by comments.id prevents rescoring on the next run).

qodo #2 — Sort before validating rules: fixed. matchRule shape-validates every rule before any pattern.length is dereferenced, so a malformed stored row surfaces the documented rule #<id> ... validation error instead of a raw TypeError. Reproducing test: "validates malformed rows before touching their pattern during ordering" (fails on the old code with Cannot read properties of null, passes now).

qodo #3 — Per-comment rule sorting / amazon-q — double regex compilation / codacy HIGH — validation in the hot path: fixed together. New prepareRules() validates, compiles, and orders rules once per run in decideNewComments; per-comment matching goes through matchPreparedRule() against the prepared array. Each regex is compiled and ReDoS-checked exactly once per run (previously 2× per comment plus once at load). Proof test: "validates and compiles each regex rule once per run, not per comment" (mocks recheck.checkSync, runs 2 comments against 1 regex rule, asserts exactly 1 call — it was 5 before).

codacy HIGH — pattern length as specificity heuristic for regex: acknowledged, keeping it. For keyword rules, length is the exact specificity relation (longer match ⇒ more specific substring). For regex rules there is no computable specificity order, so we use the same deterministic longest-first ordering as a documented, stable tiebreak. The behavior is covered by "most specific pattern wins regardless of stored order".

codacy MEDIUM — duplicated moderation scores fixture: fixed. moderation.test.ts now uses shared BASE_SCORES/LOW_SCORES fixtures plus mockScores(overrides) and stubScores() helpers; the three inlined copies are gone.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/server/moderation.ts`:
- Around line 56-57: Update the documentation associated with serializeScores
and the ai_score schema to describe the complete 13-category JSON map. Replace
the stale “six category scores” wording, and clarify that ai_score is stored as
an unbounded SQLite TEXT value consumed as an opaque string.

In `@src/lib/server/pipeline.ts`:
- Around line 113-128: Update the scoring catch in the comment-decision flow to
rethrow DeadlineExceededError before the generic AI-failure queue fallback,
preserving partial-result propagation through decideNewComments and runChannel.
Keep other scoring errors mapped to the existing pending decision, and add a
test verifying no pending row is staged when scoring exceeds the deadline.

In `@src/lib/server/rules.ts`:
- Around line 181-185: Update prepareRules and the regex helper used by its
compiled field to construct and execute patterns with RE2 instead of native
RegExp, while preserving case-insensitive matching. Catch RE2 construction
failures and surface them as form validation errors rather than allowing them to
escape; retain recheck only as an optional precheck, not the execution boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d34f74c-ef47-4a92-bcdf-b0c25960bbb8

📥 Commits

Reviewing files that changed from the base of the PR and between 21a6bbd and e2dbec3.

📒 Files selected for processing (6)
  • src/lib/server/moderation.test.ts
  • src/lib/server/moderation.ts
  • src/lib/server/pipeline.test.ts
  • src/lib/server/pipeline.ts
  • src/lib/server/rules.test.ts
  • src/lib/server/rules.ts

Comment on lines 56 to +57
export function serializeScores(scores: ToxicityScores): string {
return `{"harassment":${scores.harassment},"harassment/threatening":${scores['harassment/threatening']},"hate":${scores.hate},"hate/threatening":${scores['hate/threatening']},"violence":${scores.violence},"violence/graphic":${scores['violence/graphic']}}`;
return JSON.stringify(scores);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 '\baiScore\b|serializeScores\s*\(|JSON\.parse\s*\(' \
  --glob '*.{ts,tsx,js,svelte,sql}' .

Repository: Bonobo791/Moderaty

Length of output: 11102


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- tracked schema/migration files ---'
git ls-files | rg '(^|/)(schema|migrations?|drizzle|.*\.sql$)' || true

printf '%s\n' '--- schema definition and all ai_score references ---'
rg -n -C 8 'aiScore|ai_score|serializeScores' src migrations . --glob '*.{ts,tsx,js,svelte,sql}' 2>/dev/null || true

printf '%s\n' '--- readers/parsers of persisted comment records ---'
rg -n -C 6 'moderationComments|moderation_comments|comment\.aiScore|aiScore.*JSON|JSON.*aiScore|ai_score' src --glob '*.{ts,tsx,js,svelte}' || true

Repository: Bonobo791/Moderaty

Length of output: 36262


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- moderation categories ---'
sed -n '1,90p' src/lib/server/moderation.ts

printf '%s\n' '--- comments table migrations and schema snapshots ---'
sed -n '1,180p' drizzle/0000_add_channel_scan_state.sql
sed -n '1,180p' drizzle/0001_add_moderation_actions.sql
rg -n -C 5 '"ai_score"|ai_score|comments' drizzle/meta src/lib/server/db/migration.test.ts

printf '%s\n' '--- all comment-row query/use sites ---'
rg -n -C 8 'from\(comments\)|select\(\).*comments|comments\)|comments\.' src --glob '*.{ts,tsx,svelte}' || true

Repository: Bonobo791/Moderaty

Length of output: 32875


Update the stale aiScore schema comment.

ai_score is an unbounded SQLite TEXT column, and consumers treat it as an opaque string. Document the complete 13-category JSON map instead of “six category scores.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/moderation.ts` around lines 56 - 57, Update the documentation
associated with serializeScores and the ai_score schema to describe the complete
13-category JSON map. Replace the stale “six category scores” wording, and
clarify that ai_score is stored as an unbounded SQLite TEXT value consumed as an
opaque string.

Comment on lines +113 to +128
try {
moderation = await scoreComment(comment.text, deadline);
} catch (error) {
// I11: a scoring failure never auto-approves, never auto-rejects, and
// never aborts the batch — the comment lands in the human review queue.
return {
comment,
status: 'pending',
decidedBy: 'none',
matchedRuleId: null,
aiScore: null,
auditAction: 'queue',
reason: `ai unavailable: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200),
youtubeAction: null
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Rethrow deadline expiry instead of queuing it as an AI failure.

This catch converts DeadlineExceededError into a pending decision, so decideNewComments and runChannel cannot return their intended partial result. Rethrow it before the queue fallback, and add a test asserting no pending row is staged when scoring exceeds the deadline.

Proposed fix
 	} catch (error) {
+		if (error instanceof DeadlineExceededError) throw error;
 		// I11: a scoring failure never auto-approves, never auto-rejects, and

As per coding guidelines, “Cron and background work must isolate per-item failures and remain within serverless time limits.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
moderation = await scoreComment(comment.text, deadline);
} catch (error) {
// I11: a scoring failure never auto-approves, never auto-rejects, and
// never aborts the batch — the comment lands in the human review queue.
return {
comment,
status: 'pending',
decidedBy: 'none',
matchedRuleId: null,
aiScore: null,
auditAction: 'queue',
reason: `ai unavailable: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200),
youtubeAction: null
};
}
try {
moderation = await scoreComment(comment.text, deadline);
} catch (error) {
if (error instanceof DeadlineExceededError) throw error;
// I11: a scoring failure never auto-approves, never auto-rejects, and
// never aborts the batch — the comment lands in the human review queue.
return {
comment,
status: 'pending',
decidedBy: 'none',
matchedRuleId: null,
aiScore: null,
auditAction: 'queue',
reason: `ai unavailable: ${error instanceof Error ? error.message : String(error)}`.slice(0, 200),
youtubeAction: null
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/pipeline.ts` around lines 113 - 128, Update the scoring catch
in the comment-decision flow to rethrow DeadlineExceededError before the generic
AI-failure queue fallback, preserving partial-result propagation through
decideNewComments and runChannel. Keep other scoring errors mapped to the
existing pending decision, and add a test verifying no pending row is staged
when scoring exceeds the deadline.

Source: Coding guidelines

Comment thread src/lib/server/rules.ts
Comment on lines +181 to +185
export function prepareRules(rules: RuleRow[]): PreparedRule[] {
const prepared = rules.map((rule) => {
assertShape(rule);
const valid = rule as PreparedRule['rule'];
return { rule: valid, compiled: rule.type === 'regex' ? regex(rule) : null };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 3 '\b(new RegExp|RE2|re2|re2-wasm)\b' \
  --glob 'package.json' --glob '*lock*' --glob '*.ts' .

Repository: Bonobo791/Moderaty

Length of output: 1307


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- manifests ---'
git ls-files | rg '(^|/)(package\.json|.*lock.*|pnpm-workspace\.yaml|yarn\.lock|bun\.lockb?)$' | xargs -r -n1 sh -c 'echo "--- $0"; rg -n -C 2 "re2|regex|dependencies|devDependencies" "$0" || true'

printf '%s\n' '--- rules implementation and references ---'
rg -n -C 8 'function (regex|assertShape|prepareRules)|prepareRules\(|compiled|rule\.pattern|new RegExp' src tests 2>/dev/null || true

printf '%s\n' '--- relevant file outline ---'
ast-grep outline src/lib/server/rules.ts --view compact

Repository: Bonobo791/Moderaty

Length of output: 17720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package metadata ---'
cat -n package.json | sed -n '1,55p'
rg -n -C 5 'validateRule|prepareRules|matchRule|rule.*pattern|invalid regex|unsafe regex|fail|form' src --glob '*.{ts,tsx,svelte}'

printf '%s\n' '--- rules source ---'
cat -n src/lib/server/rules.ts | sed -n '1,225p'

printf '%s\n' '--- focused dependency check ---'
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
const lock = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'));
const declared = {...(pkg.dependencies || {}), ...(pkg.devDependencies || {})};
const names = Object.keys(lock.packages || {})
  .map((key) => key.replace(/^node_modules\//, ''))
  .filter((key) => key && !key.includes('/'));
console.log(JSON.stringify({
  declaredRegexRelated: Object.keys(declared).filter((name) => /re2|regex/i.test(name)),
  lockedRegexRelated: names.filter((name) => /re2|regex/i.test(name)),
  recheckVersion: declared.recheck ?? null
}, null, 2));
JS

Repository: Bonobo791/Moderaty

Length of output: 50374


Compile stored patterns with RE2, not native RegExp.

regex() still calls new RegExp(rule.pattern, 'i'); recheck is only a precheck and does not satisfy the required execution boundary. Use RE2 for compilation and matching, surfacing construction failures as form validation errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/server/rules.ts` around lines 181 - 185, Update prepareRules and the
regex helper used by its compiled field to construct and execute patterns with
RE2 instead of native RegExp, while preserving case-insensitive matching. Catch
RE2 construction failures and surface them as form validation errors rather than
allowing them to escape; retain recheck only as an optional precheck, not the
execution boundary.

Source: Coding guidelines

@Bonobo791

Copy link
Copy Markdown
Collaborator Author

Re: the new Codacy security warning on src/lib/server/rules.ts (compiled?.test(text) — "ensure the regex is safe from ReDoS"):

False positive in substance. Every compiled regex that can reach that line comes from regex() in the same module, which rejects a pattern before it is ever compiled unless it passes all of: 256-char length cap, no backreferences, no duplicate-alternation groups, and a recheck proof of safe (unprovable/unknown patterns are rejected loudly — invariant I6). Unsafe patterns throw at prepareRules() time and never produce a PreparedRule.

No behavior change was warranted; commit 7a3e1d7 adds a // nosemgrep justification at the match site, mirroring the one that already exists at the compile site, so the safety argument is visible where the analyzer (and reviewers) look.

@sonarqubecloud

Copy link
Copy Markdown

@Bonobo791
Bonobo791 merged commit 3cc1326 into main Jul 31, 2026
7 of 10 checks passed
@Bonobo791
Bonobo791 deleted the fix-moderation-coverage branch July 31, 2026 10:54
@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Jul 31, 2026
@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

User description

Production findings that prompted this

  • A comment "I like to suck dick. How about you?" was approved at AI score 0.01 — the sexual category was never read.
  • Rules fuck → hold (id 2) and fuck you → reject (id 3): first-hit matching in insertion order meant the broad rule always shadowed the specific one.

Fixes

  1. moderation.ts: all 13 omni-moderation categories are now required, range-validated (I2), and included in the max score. serializeScores is a plain JSON.stringify of the full map.
  2. rules.ts: matchRule sorts by pattern length descending — most specific pattern wins, ties keep first-stored-wins.

Tests (watched fail, then pass)

  • max score includes sexual/self-harm/illicit; missing-category responses throw; out-of-range still throws
  • fuck you beats fuck regardless of stored order; shorter text still hits the broad rule

Verification

  • npm run test: 96/96 (16 files) · npm run check: 0 errors · npm run build: green

Not in this PR (observed, needs a decision)

The "fuck you" comment that prompted the report was removed by YouTube before any run ingested it (no DB row, empty held-for-review list) — Moderaty never saw it. If you want Moderaty to also process comments YouTube auto-holds (rather than leaving them in YouTube Studio's held-for-review tab), the fetch needs moderationStatus=heldForReview merged in — say the word and I'll spec it.


CodeAnt-AI Description

Ensure moderation catches all risks, applies the right rule, and safely queues scoring failures

What Changed

  • Comment scores now include sexual, self-harm, illicit, and all other supported moderation categories, so the highest risk is not discarded.
  • The system rejects incomplete or invalid moderation responses instead of making a decision from unreliable scores.
  • When multiple rules match, the most specific pattern wins regardless of storage order.
  • Invalid rules are reported clearly, and repeated comment checks reuse validated patterns.
  • AI scoring failures now place comments in the human review queue without stopping the rest of the moderation run or retrying the failed comment automatically.

Impact

✅ Fewer unsafe comments approved
✅ Correct specific-rule enforcement
✅ Failed AI checks queued for human review

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to commit 7a3e1d7
CategorySuggestion                                                                                                                                    Severity
Api mismatch
Deadline expiration is swallowed as a pending decision, causing the scan cursor to skip the timed-out comment

Do not convert DeadlineExceededError into a pending decision. This error is
intentionally propagated by the deadline-aware HTTP layer so runChannel can return a
partial result without advancing the scan cursor. Catching it here stages the
comment as pending and allows persistResults to advance the cursor, so the timed-out
comment will not be retried on the next run.

src/lib/server/pipeline.ts [113-127]

Why it matters? 🤔
  • ❌ Timed-out comments are permanently skipped by future AI scoring.
  • ⚠️ Affected comments remain pending for human review.
  • ❌ Scan cursor advances despite incomplete moderation.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/pipeline.ts
**Line:** 113:127
**Comment:**
	*Api Mismatch: Do not convert `DeadlineExceededError` into a pending decision. This error is intentionally propagated by the deadline-aware HTTP layer so `runChannel` can return a partial result without advancing the scan cursor. Catching it here stages the comment as pending and allows `persistResults` to advance the cursor, so the timed-out comment will not be retried on the next run.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Major

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

Labels

cla-signed size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant