feat: moderation core — data model + resolver (Slice 1) #53

Merged
starsetbyte merged 9 commits from feat/moderation-core into main 2026-07-05 12:38:18 +00:00
Owner

Summary
Slice 1 of the moderation foundation: labels and viewer facts flow network → Room → cache, and a pure ModerationResolver turns them into per-context verdicts (filter/blur/alert/inform/noOverride). Zero user-visible behavior change — nothing is wired to UI yet.
Executes docs/superpowers/plans/2026-07-04-moderation-core.md task-by-task via subagent-driven development, with a task-scoped review after every task plus two deeper gate reviews (post-Task-3 DTO/Room/mapper plumbing, post-Task-6 resolver semantics) and a final whole-branch review. All reviews clean: 0 Critical, 0 Important findings across the branch.
The abandoned feat/moderation branch (PR #11, reference only, never merged) had a "default-Show label bug" where labels silently failed open. This slice's resolver was specifically traced end-to-end to confirm that bug is structurally impossible here.
What's in
Shared Label DTO + labels/viewer.blocking fields on the network models
Room v9→v10 additive migration persisting labels + viewer mute/block facts
FeedRepository mapper round-trips those facts through the cache
New pure data/moderation/ package: types, BuiltInLabels, MuteWordMatcher, ModerationResolver
Purity contract test locking data/moderation/ to zero android/room/retrofit/coroutines imports
Known gaps carried forward (documented, non-blocking)
Label.neg/Label.exp not filtered by the resolver (AppView pre-resolves)
Legacy label-alias settings-key normalization deferred to Stage 5
decideNotification has no direct test coverage
Design-spec doc citations point to docs/moderation-foundation, not this branch — dangling until that branch also merges
Test plan
[x] ./gradlew test — 612/612 passing (215 pre-existing + 397 new)
[x] ./gradlew :app:assembleDebug — schema v10 regenerated and verified
[x] ./gradlew lint — clean for data/moderation; 2 pre-existing unrelated errors confirmed to predate this branch
[x] 8 task-scoped reviews + 2 gate reviews + 1 final whole-branch review — all clean

Summary Slice 1 of the moderation foundation: labels and viewer facts flow network → Room → cache, and a pure ModerationResolver turns them into per-context verdicts (filter/blur/alert/inform/noOverride). Zero user-visible behavior change — nothing is wired to UI yet. Executes docs/superpowers/plans/2026-07-04-moderation-core.md task-by-task via subagent-driven development, with a task-scoped review after every task plus two deeper gate reviews (post-Task-3 DTO/Room/mapper plumbing, post-Task-6 resolver semantics) and a final whole-branch review. All reviews clean: 0 Critical, 0 Important findings across the branch. The abandoned feat/moderation branch (PR #11, reference only, never merged) had a "default-Show label bug" where labels silently failed open. This slice's resolver was specifically traced end-to-end to confirm that bug is structurally impossible here. What's in Shared Label DTO + labels/viewer.blocking fields on the network models Room v9→v10 additive migration persisting labels + viewer mute/block facts FeedRepository mapper round-trips those facts through the cache New pure data/moderation/ package: types, BuiltInLabels, MuteWordMatcher, ModerationResolver Purity contract test locking data/moderation/ to zero android/room/retrofit/coroutines imports Known gaps carried forward (documented, non-blocking) Label.neg/Label.exp not filtered by the resolver (AppView pre-resolves) Legacy label-alias settings-key normalization deferred to Stage 5 decideNotification has no direct test coverage Design-spec doc citations point to docs/moderation-foundation, not this branch — dangling until that branch also merges Test plan [x] ./gradlew test — 612/612 passing (215 pre-existing + 397 new) [x] ./gradlew :app:assembleDebug — schema v10 regenerated and verified [x] ./gradlew lint — clean for data/moderation; 2 pre-existing unrelated errors confirmed to predate this branch [x] 8 task-scoped reviews + 2 gate reviews + 1 final whole-branch review — all clean
\p{Punct} in Kotlin/Java regex is ASCII-only, so EDGE_PUNCTUATION and
PUNCT_OR_SPACE failed to strip em dashes, curly quotes, ellipses, and
CJK full-width punctuation before the token-equality check. A muted
word like "crypto" would silently fail to match "so much crypto—" or
"“crypto”" -- a real under-mute gap since autocorrect/IMEs insert this
punctuation routinely. Switch both regexes to \p{IsPunctuation}
(Unicode General_Category=Punctuation), which covers Pd (dashes) and
all other punctuation subcategories.

Flagged by task reviewer on Task 5 (8f4989c).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FJZABQvMnCr3mqbk7KG9M
test: contract test locking data/moderation purity
All checks were successful
peregrine-ci / assembleDebug Build succeeded (1m14s)
1bb08dd849
Sign in to join this conversation.
No description provided.