Repository navigation
fix: full moderation category coverage + most-specific-rule-wins - #20
Conversation
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).
🤖 CodeAnt AI — Review Status
|
✅ Deploy Preview for moderaty ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughSummary by CodeRabbit
WalkthroughModeration 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. ChangesModeration and pipeline processing
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
User descriptionProduction findings that prompted this
Fixes
Tests (watched fail, then pass)
Verification
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 CodeAnt-AI DescriptionEnsure moderation catches all supported risks and applies the most specific rule What Changed
Impact
💡 Usage GuideChecking Your Pull RequestEvery 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 AIGot 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: This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code. ExamplePreserve Org Learnings with CodeAntYou 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: This helps CodeAnt AI learn and adapt to your team's coding style and standards. ExampleRetrigger reviewAsk CodeAnt AI to review the PR again, by typing: Check Your Repository HealthTo 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 Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
There was a problem hiding this comment.
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
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.
| 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; |
There was a problem hiding this comment.
🛑 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.
PR Summary by QodoFix moderation scoring coverage and rule precedence (most-specific wins)
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 1 high |
🟢 Metrics 13 complexity · 0 duplication
Metric Results Complexity ✅ 13 (≤ 100 complexity) Duplication ✅ 0 (≤ 1 duplication)
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
PR Code Suggestions ✨Latest suggestions up to commit
|
| Category | Suggestion | Severity |
| Performance |
Per-comment rule matching redundantly performs expensive validation and regex analysisEvery call now validates all rules, including re-running the expensive regex safety src/lib/server/rules.ts [169-175] Why it matters? 🤔
(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 |
There was a problem hiding this comment.
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
| 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; |
There was a problem hiding this comment.
🔴 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 () => { |
There was a problem hiding this comment.
🟡 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.
Code Review by Qodo
Context used✅ Compliance rules (platform):
51 rules 1.
|
… before sort and compile once; dedupe test fixtures
|
All review findings addressed in qodo #1 — AI scoring failures not queued (I11 violation): fixed. qodo #2 — Sort before validating rules: fixed. qodo #3 — Per-comment rule sorting / amazon-q — double regex compilation / codacy HIGH — validation in the hot path: fixed together. New codacy HIGH — pattern length as specificity heuristic for regex: acknowledged, keeping it. For codacy MEDIUM — duplicated moderation scores fixture: fixed. |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
src/lib/server/moderation.test.tssrc/lib/server/moderation.tssrc/lib/server/pipeline.test.tssrc/lib/server/pipeline.tssrc/lib/server/rules.test.tssrc/lib/server/rules.ts
| 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); |
There was a problem hiding this comment.
🗄️ 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}' || trueRepository: 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}' || trueRepository: 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.
| 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 | ||
| }; | ||
| } |
There was a problem hiding this comment.
🩺 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, andAs 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.
| 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
| 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 }; |
There was a problem hiding this comment.
🔒 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 compactRepository: 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));
JSRepository: 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
|
Re: the new Codacy security warning on False positive in substance. Every compiled regex that can reach that line comes from No behavior change was warranted; commit |
|
User descriptionProduction findings that prompted this
Fixes
Tests (watched fail, then pass)
Verification
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 CodeAnt-AI DescriptionEnsure moderation catches all risks, applies the right rule, and safely queues scoring failures What Changed
Impact
💡 Usage GuideChecking Your Pull RequestEvery 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 AIGot 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: This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code. ExamplePreserve Org Learnings with CodeAntYou 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: This helps CodeAnt AI learn and adapt to your team's coding style and standards. ExampleRetrigger reviewAsk CodeAnt AI to review the PR again, by typing: Check Your Repository HealthTo 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. |
PR Code Suggestions ✨Latest suggestions up to commit
|
| Category | Suggestion | Severity |
| Api mismatch |
Deadline expiration is swallowed as a pending decision, causing the scan cursor to skip the timed-out commentDo not convert src/lib/server/pipeline.ts [113-127] Why it matters? 🤔
(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 |



Production findings that prompted this
sexualcategory was never read.fuck → hold(id 2) andfuck you → reject(id 3): first-hit matching in insertion order meant the broad rule always shadowed the specific one.Fixes
moderation.ts: all 13 omni-moderation categories are now required, range-validated (I2), and included in the max score.serializeScoresis a plainJSON.stringifyof the full map.rules.ts:matchRulesorts by pattern length descending — most specific pattern wins, ties keep first-stored-wins.Tests (watched fail, then pass)
fuck youbeatsfuckregardless of stored order; shorter text still hits the broad ruleVerification
npm run test: 96/96 (16 files) ·npm run check: 0 errors ·npm run build: greenNot 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=heldForReviewmerged in — say the word and I'll spec it.