Skip to content

Vk/fd84 everything claud - #6

Merged
DavidHLP merged 4 commits into
mainfrom
vk/fd84-everything-claud
Apr 8, 2026
Merged

Vk/fd84 everything claud#6
DavidHLP merged 4 commits into
mainfrom
vk/fd84-everything-claud

Conversation

@DavidHLP

@DavidHLP DavidHLP commented Apr 8, 2026

Copy link
Copy Markdown
Owner

No description provided.

DavidHLP added 4 commits April 8, 2026 12:37
Diagnoses three data issues in the solution detail page:
1. Frontend tags rendering (JSON string iterated char-by-char)
2. Backend SolutionVO missing stats/flair/badges/topic fields
3. Seed data lacking actual code implementations
…layers

Frontend: add parseTags() safe JSON parsing for tags field (was iterating
string char-by-char via v-for), fix stats mapping from flat fields instead
of missing nested object, add comments to inline type definitions.

Backend: extend SolutionVO with likes/dislikes/comments/score/userVote,
inject EdgeOperationMapper into SolutionServiceImpl toVO() to populate
vote counts from edge_operations and comment count from solution_comments.

Database: V13 migration enriches all 8 solution seed data with complete
content including approach explanation, full code implementation, and
complexity analysis.
The sol-008 content contained unescaped single quotes ('1', '0') that
caused SQL syntax errors during Flyway migration. Escaped as ''1'' and
''0'' per MySQL string literal conventions.
…review issues

- Extract SolutionApiItem interface and transformApiSolution() to eliminate
  3 duplicate mapping blocks (~105 lines removed, 354→220 lines)
- Add .map(String) to parseTags() for type safety against non-string JSON values
- Remove unused edge-operations imports from solution.ts
- Remove unused userVote field and java.util.List import from SolutionVO.java
@DavidHLP
DavidHLP merged commit 4068e9f into main Apr 8, 2026
1 check failed
@DavidHLP
DavidHLP deleted the vk/fd84-everything-claud branch May 16, 2026 12:25
DavidHLP added a commit that referenced this pull request Jun 8, 2026
…_ADMIN + rankings 404 + is_deleted filter + Unicode title + @Audited + new ErrorCode + controller XSS guard + mutator tests)

修复 docs/contests-api-test-report.md 报告的 7 处缺陷 (1 HIGH + 6 MEDIUM/LOW):

缺陷 #1 (HIGH) — DELETE 软删除 is_deleted 字段未持久化
  - 根因:Contest.isDeleted 字段 @TableLogic + mapper.updateById(entity) 自动忽略逻辑删除字段
  - 修复:改用 mapper.update(null, LambdaUpdateWrapper.set(...)) 显式写入

缺陷 #2 — PATCH on FINISHED/RUNNING 比赛无状态守卫
  - 修复:service 层加 status guard,仅允许 UPCOMING 修改
  - 新增 ErrorCode CONTEST_ONLY_UPDATE_UPCOMING (70006) 区分注册与修改语义

缺陷 #3 — service 层 hasRole("ADMIN") 拒绝 SUPER_ADMIN,与控制器 @PreAuthorize 不一致
  - 修复:SecurityUtil 新增 hasAnyRole(String...) 工具方法,ContestServiceImpl 7 处 mutator 替换

缺陷 #4 — CreateContestDTO/UpdateContestDTO title @pattern 拒绝 CJK 字符
  - 修复:正则放宽为 [\p{L}\p{N}\s\p{P}]+ 接受 Unicode 字母
  - AdminContestController 新增 rejectUnsafeTitleChars helper 显式排除 < 和 >

缺陷 #5 — GET /admin/contest/{id}/rankings 对不存在 contest 返 200+空
  - 修复:getAdminContestRanking 入口加 contest 存在性 + is_deleted 校验

缺陷 #6 — addProblem/removeProblem/startContest/endContest 未校验 is_deleted
  - 修复:4 个 mutator 入口加 .filter(c -> !isDeleted).orElseThrow(COMPETITION_NOT_FOUND)

缺陷 #7@Audited 注解缺失导致 audit_logs 无 contest 记录
  - 修复:7 个 mutator 方法加 @Audited + AuditContext.setOldValues/setNewValues/setUserId
  - 复用 AuditActionUtil.CREATE_CONTEST/UPDATE_CONTEST/DELETE_CONTEST 常量

新增测试 ContestServiceImplMutatorTest:5 case 覆盖缺陷 #2/#5/#6 的状态守卫 + 软删除过滤 + 404 路径。
缺陷 #1 的 LambdaUpdateWrapper.getSqlSet() 断言需 @SpringBootTest 上下文,保留为 follow-up,
运行时 curl 验证已确认三列同改 (docs/contests-api-test-report.md §3.5)。

验证:
- mvnw compile -B:0 错误
- mvnw test -Dtest=ContestServiceImplMutatorTest:5/5 通过,2.0s
- 运行时 curl 5/5 critical 缺陷修复确认 (PM2 重启 ulticode-9001)

Artifacts:
- Plan: .claude/PRPs/plans/completed/admin-contests-mutator-fixes.plan.md
- Report: .claude/PRPs/reports/admin-contests-mutator-fixes-report.md
- Review v1: .claude/reviews/admin-contests-mutator-review.md
- Review v2: .claude/reviews/admin-contests-mutator-review-v2.md
- Test report: docs/contests-api-test-report.md
DavidHLP pushed a commit that referenced this pull request Jun 11, 2026
… handler, compute real progress

Fixes 6 issues from docs/achievement-api-test-report-2026-06-11.md
plus 2 newly-discovered typeHandler bugs (Bug #7, Bug #8) surfaced
during validation.

CRITICAL #1 — escape `key` column via @TableField(value = "`key`")
  - Achievement.java:19 — affects MyBatis-Plus auto-generated column
    lists in selectById / selectPage / selectBatchIds.
  - Without this, every SQL that lists the achievements columns fails
    with 'You have an error in your SQL syntax ... near key,...'.

CRITICAL #2 — escape `key` in @select SQL
  - AchievementMapper.findByKey:24 — WHERE clause now backticks.
  - Fixes admin POST /achievements which called findByKey for dedup.

HIGH #3 — T2b /achievements/{invalid} now returns 404 (was 500)
  - Follows from CRITICAL #1+#2: the BadSqlGrammarException
    no longer fires before the business-level ACHIEVEMENT_NOT_FOUND
    can be raised.

MEDIUM #4 — BadSqlGrammarException → DATABASE_ERROR (50001)
  - GlobalExceptionHandler:227 — new handler preserves root cause
    in server log while returning generic 'Database error' to clients.
  - Pairs with existing MyBatisSystemException / BindingException /
    DataIntegrityViolationException handlers (no fallback to 50000
    'Unknown error').

LOW #5 — getUserAchievements progress is no longer hard-coded to 0
  - AchievementServiceImpl:280 — mirrors getUserProgress logic,
    reads criteria.type from JSON Map, dispatches to SubmissionMapper
    counter.

LOW #6 — re-scoped Javadoc: getUserAchievements and
  getUserProgress are NOT duplicates (verified UserController
  /me/achievements/progress uses the latter). Both serve different
  endpoints with different DTO shapes.

Bug #7 — findAllActive missing JacksonTypeHandler
  - AchievementMapper:46 — @select bypassed @TableField(typeHandler=
    JacksonTypeHandler.class), returning criteria=null.
  - Converted to default method via BaseMapper.selectList.

Bug #8 — findByKey same typeHandler bypass
  - Discovered by AchievementMapperIT (L2) on first run;
    findByKey_criteriaIsDeserializedAsMap failed.
  - AchievementMapper:24 — also converted to default method.
  - Latent: only caller (create) never read criteria.

Refactors
  - M1 (review): getUserAchievements extracted buildProgressDTO helper
    (52 → 16 lines).
  - L1 (review): findAllActive uses LambdaQueryWrapper<Achievement>
    method refs instead of string column names.
  - L3 (review): Javadoc clarified the two service methods.

Tests
  + AchievementMapperSQLGuardTest: 3 reflection-guard tests
    (CRITICAL #1, CRITICAL #2, no @select on findAllActive/findByKey).
  + AchievementMapperIT: 5 Testcontainers MySQL 8.0 IT tests
    verifying JacksonTypeHandler runtime behavior + Bug #7/#8
    regression guards.
  + AchievementServiceTest: 3 progress tests + 2 new mock fields
    (SubmissionMapper, ContestParticipantMapper).
  + GlobalExceptionHandlerTest: 2 BadSqlGrammarException tests.

Validation
  - compile / package: BUILD SUCCESS
  - Unit tests: 36/36 pass
  - Testcontainers IT: 5/5 pass
  - 8 curl integration tests on worktree:9091: all match expected
    HTTP codes (200/400/404/409) and real progress values
    (admin 6 accepted problems → progress=6 for all 3 rows).

Refs: docs/achievement-api-test-report-2026-06-11.md,
      .claude/PRPs/plans/completed/achievement-api-fixes.plan.md,
      .claude/PRPs/reports/achievement-api-fixes-report.md,
      .claude/reviews/achievement-api-fixes-review.md
DavidHLP pushed a commit that referenced this pull request Jun 11, 2026
…dation

Merges the fix/achievement-api worktree changes:
- CRITICAL #1, #2 (MySQL reserved word `key`)
- HIGH #3 (T2b 404)
- MEDIUM #4 (BadSqlGrammarException handler)
- LOW #5 (real progress computation)
- LOW #6 (Javadoc re-scoped)
- Bug #7 (findAllActive typeHandler)
- Bug #8 (findByKey typeHandler, IT-discovered)

M1/L1/L2/L3/L4 from .claude/reviews/achievement-api-fixes-review.md
all addressed. Test coverage: 36 unit + 5 Testcontainers IT.
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP pushed a commit that referenced this pull request Jun 13, 2026
…nedAt)

2 review findings about intentId() collisions on the ledger UNIQUE
(intent_id, channel_id) constraint:

- SubmissionCompletedIntent: factory of() now throws IllegalStateException
  when submission.getGeneration() is null, instead of silently falling
  back to 0L. Two distinct submissions (one real g=0, one with null
  generation) would have produced identical intentIds, and the second
  dispatch would have been silently dropped by tryClaim returning 0.
  The null-generation case indicates a hydration bug, not user data, so
  fail-fast is correct. Fixes finding #5.
- AchievementEarnedIntent: added earnedAt field, intentId now includes
  the millisecond timestamp (:at<epochMs>). Without this, a re-issued
  event (tier-up promotion, system re-trigger) for the same
  achievement collapsed under the existing intentId and was silently
  dropped on every channel. The natural key for achievement
  notifications now includes the issue time, matching the
  AchievementEarnedEvent one-publish-per-earn invariant. Fixes
  finding #6.

Test updates: 5 test files updated to pass the new
earnedAt=Instant.now() arg. The sample intents in the channel contract
test and the per-channel tests use the new field; the legacy
follow-system test path is unchanged (FollowReceivedIntent has no
earnedAt).

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#5, #6).
DavidHLP added a commit that referenced this pull request Jun 13, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP added a commit that referenced this pull request Jun 13, 2026
…nedAt)

2 review findings about intentId() collisions on the ledger UNIQUE
(intent_id, channel_id) constraint:

- SubmissionCompletedIntent: factory of() now throws IllegalStateException
  when submission.getGeneration() is null, instead of silently falling
  back to 0L. Two distinct submissions (one real g=0, one with null
  generation) would have produced identical intentIds, and the second
  dispatch would have been silently dropped by tryClaim returning 0.
  The null-generation case indicates a hydration bug, not user data, so
  fail-fast is correct. Fixes finding #5.
- AchievementEarnedIntent: added earnedAt field, intentId now includes
  the millisecond timestamp (:at<epochMs>). Without this, a re-issued
  event (tier-up promotion, system re-trigger) for the same
  achievement collapsed under the existing intentId and was silently
  dropped on every channel. The natural key for achievement
  notifications now includes the issue time, matching the
  AchievementEarnedEvent one-publish-per-earn invariant. Fixes
  finding #6.

Test updates: 5 test files updated to pass the new
earnedAt=Instant.now() arg. The sample intents in the channel contract
test and the per-channel tests use the new field; the legacy
follow-system test path is unchanged (FollowReceivedIntent has no
earnedAt).

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#5, #6).
DavidHLP added a commit that referenced this pull request Jul 3, 2026
管理后台鉴权初始化(CRITICAL #1):
- management/src/main.ts: 新增完整 auth bootstrap 流程
- management/src/stores/auth.ts: 新增 setupSessionExpiredCallback + clearUser

WebSocket Token 安全泄露(CRITICAL #2):
- console/src/lib/socket.ts: 移除 URL ?token= 参数,token 改走 connectHeaders
- console/src/composables/contest/useContestSocket.ts: 同上

OAuth 重定向 URL 统一(CRITICAL #4):
- management/src/views/auth/components/OAuthButton.vue: 相对路径→绝对路径

Router Guard 错误处理(HIGH #5):
- console/src/router/index.ts: catch 块增加 ApiError instanceof 判断

WebSocket Token 传递优化(HIGH #6):
- useContestSocket.ts: 已正确从 socket.ts 导入 getTokenFromCookie

verifyAuth 绕过 axios 拦截器(HIGH #7):
- console/src/utils/auth.ts: fetch() → apiGet()

性能优化(HIGH #9):
- console/src/contexts/AuthContext.ts: setInterval(1000) → watch(isAuthenticated)

代码质量提升(MEDIUM):
- console/src/stores/auth.ts: 移除 LOGOUT_KEY localStorage 追踪
- 新增 hasAuthCookie() 提前检查,173 行净减少
- clearUser() 重置 status="idle" 允许重新初始化
- 移除所有调试 console.log

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 3, 2026
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…_ADMIN + rankings 404 + is_deleted filter + Unicode title + @Audited + new ErrorCode + controller XSS guard + mutator tests)

修复 docs/contests-api-test-report.md 报告的 7 处缺陷 (1 HIGH + 6 MEDIUM/LOW):

缺陷 #1 (HIGH) — DELETE 软删除 is_deleted 字段未持久化
  - 根因:Contest.isDeleted 字段 @TableLogic + mapper.updateById(entity) 自动忽略逻辑删除字段
  - 修复:改用 mapper.update(null, LambdaUpdateWrapper.set(...)) 显式写入

缺陷 #2 — PATCH on FINISHED/RUNNING 比赛无状态守卫
  - 修复:service 层加 status guard,仅允许 UPCOMING 修改
  - 新增 ErrorCode CONTEST_ONLY_UPDATE_UPCOMING (70006) 区分注册与修改语义

缺陷 #3 — service 层 hasRole("ADMIN") 拒绝 SUPER_ADMIN,与控制器 @PreAuthorize 不一致
  - 修复:SecurityUtil 新增 hasAnyRole(String...) 工具方法,ContestServiceImpl 7 处 mutator 替换

缺陷 #4 — CreateContestDTO/UpdateContestDTO title @pattern 拒绝 CJK 字符
  - 修复:正则放宽为 [\p{L}\p{N}\s\p{P}]+ 接受 Unicode 字母
  - AdminContestController 新增 rejectUnsafeTitleChars helper 显式排除 < 和 >

缺陷 #5 — GET /admin/contest/{id}/rankings 对不存在 contest 返 200+空
  - 修复:getAdminContestRanking 入口加 contest 存在性 + is_deleted 校验

缺陷 #6 — addProblem/removeProblem/startContest/endContest 未校验 is_deleted
  - 修复:4 个 mutator 入口加 .filter(c -> !isDeleted).orElseThrow(COMPETITION_NOT_FOUND)

缺陷 #7@Audited 注解缺失导致 audit_logs 无 contest 记录
  - 修复:7 个 mutator 方法加 @Audited + AuditContext.setOldValues/setNewValues/setUserId
  - 复用 AuditActionUtil.CREATE_CONTEST/UPDATE_CONTEST/DELETE_CONTEST 常量

新增测试 ContestServiceImplMutatorTest:5 case 覆盖缺陷 #2/#5/#6 的状态守卫 + 软删除过滤 + 404 路径。
缺陷 #1 的 LambdaUpdateWrapper.getSqlSet() 断言需 @SpringBootTest 上下文,保留为 follow-up,
运行时 curl 验证已确认三列同改 (docs/contests-api-test-report.md §3.5)。

验证:
- mvnw compile -B:0 错误
- mvnw test -Dtest=ContestServiceImplMutatorTest:5/5 通过,2.0s
- 运行时 curl 5/5 critical 缺陷修复确认 (PM2 重启 ulticode-9001)

Artifacts:
- Plan: .claude/PRPs/plans/completed/admin-contests-mutator-fixes.plan.md
- Report: .claude/PRPs/reports/admin-contests-mutator-fixes-report.md
- Review v1: .claude/reviews/admin-contests-mutator-review.md
- Review v2: .claude/reviews/admin-contests-mutator-review-v2.md
- Test report: docs/contests-api-test-report.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… handler, compute real progress

Fixes 6 issues from docs/achievement-api-test-report-2026-06-11.md
plus 2 newly-discovered typeHandler bugs (Bug #7, Bug #8) surfaced
during validation.

CRITICAL #1 — escape `key` column via @TableField(value = "`key`")
  - Achievement.java:19 — affects MyBatis-Plus auto-generated column
    lists in selectById / selectPage / selectBatchIds.
  - Without this, every SQL that lists the achievements columns fails
    with 'You have an error in your SQL syntax ... near key,...'.

CRITICAL #2 — escape `key` in @select SQL
  - AchievementMapper.findByKey:24 — WHERE clause now backticks.
  - Fixes admin POST /achievements which called findByKey for dedup.

HIGH #3 — T2b /achievements/{invalid} now returns 404 (was 500)
  - Follows from CRITICAL #1+#2: the BadSqlGrammarException
    no longer fires before the business-level ACHIEVEMENT_NOT_FOUND
    can be raised.

MEDIUM #4 — BadSqlGrammarException → DATABASE_ERROR (50001)
  - GlobalExceptionHandler:227 — new handler preserves root cause
    in server log while returning generic 'Database error' to clients.
  - Pairs with existing MyBatisSystemException / BindingException /
    DataIntegrityViolationException handlers (no fallback to 50000
    'Unknown error').

LOW #5 — getUserAchievements progress is no longer hard-coded to 0
  - AchievementServiceImpl:280 — mirrors getUserProgress logic,
    reads criteria.type from JSON Map, dispatches to SubmissionMapper
    counter.

LOW #6 — re-scoped Javadoc: getUserAchievements and
  getUserProgress are NOT duplicates (verified UserController
  /me/achievements/progress uses the latter). Both serve different
  endpoints with different DTO shapes.

Bug #7 — findAllActive missing JacksonTypeHandler
  - AchievementMapper:46 — @select bypassed @TableField(typeHandler=
    JacksonTypeHandler.class), returning criteria=null.
  - Converted to default method via BaseMapper.selectList.

Bug #8 — findByKey same typeHandler bypass
  - Discovered by AchievementMapperIT (L2) on first run;
    findByKey_criteriaIsDeserializedAsMap failed.
  - AchievementMapper:24 — also converted to default method.
  - Latent: only caller (create) never read criteria.

Refactors
  - M1 (review): getUserAchievements extracted buildProgressDTO helper
    (52 → 16 lines).
  - L1 (review): findAllActive uses LambdaQueryWrapper<Achievement>
    method refs instead of string column names.
  - L3 (review): Javadoc clarified the two service methods.

Tests
  + AchievementMapperSQLGuardTest: 3 reflection-guard tests
    (CRITICAL #1, CRITICAL #2, no @select on findAllActive/findByKey).
  + AchievementMapperIT: 5 Testcontainers MySQL 8.0 IT tests
    verifying JacksonTypeHandler runtime behavior + Bug #7/#8
    regression guards.
  + AchievementServiceTest: 3 progress tests + 2 new mock fields
    (SubmissionMapper, ContestParticipantMapper).
  + GlobalExceptionHandlerTest: 2 BadSqlGrammarException tests.

Validation
  - compile / package: BUILD SUCCESS
  - Unit tests: 36/36 pass
  - Testcontainers IT: 5/5 pass
  - 8 curl integration tests on worktree:9091: all match expected
    HTTP codes (200/400/404/409) and real progress values
    (admin 6 accepted problems → progress=6 for all 3 rows).

Refs: docs/achievement-api-test-report-2026-06-11.md,
      .claude/PRPs/plans/completed/achievement-api-fixes.plan.md,
      .claude/PRPs/reports/achievement-api-fixes-report.md,
      .claude/reviews/achievement-api-fixes-review.md
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…dation

Merges the fix/achievement-api worktree changes:
- CRITICAL #1, #2 (MySQL reserved word `key`)
- HIGH #3 (T2b 404)
- MEDIUM #4 (BadSqlGrammarException handler)
- LOW #5 (real progress computation)
- LOW #6 (Javadoc re-scoped)
- Bug #7 (findAllActive typeHandler)
- Bug #8 (findByKey typeHandler, IT-discovered)

M1/L1/L2/L3/L4 from .claude/reviews/achievement-api-fixes-review.md
all addressed. Test coverage: 36 unit + 5 Testcontainers IT.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
… perf + per-channel tests

New test files (5):
- NotificationDispatcherTest: 8 tests covering ADR-004 §4 #2 (channel
  failure isolation), #3 (intentId idempotency), #6 (latency < 50ms),
  category preference suppression, ledger short-circuit, failure_reason
  truncation, ledger state sanity.
- NotificationChannelContractTest: ADR-004 §4 #1 — every intent has at
  least 1 supporting channel; matrix sanity for InApp/Email/WebSocket.
- InAppNotificationChannelTest: channelId, supports, send → row-only path
  with type=recordSimpleName, metadata includes isAccepted, follow link
  + title.
- EmailNotificationChannelTest: channelId, supports matrix, in-flight
  rejection, missing-email BusinessException, send routes to
  EmailService.sendEmail with templateId.
- WebSocketNotificationChannelTest: channelId, achievement emits
  BadgeEarnedPayload (badgeTier slug), submission emits
  NotificationPayload with isAccepted, contest reminder type tag.

EmailNotificationChannel.userMapper: package-private (was private) so
unit tests in the same package can inject a mock directly. Production
wiring is still via @Autowired(required=false).

mvn test on the 5 new test classes + the 4 existing legacy tests:
- 5 new classes: 28 tests, 0 failures
- 4 existing tests: 30 tests, 0 failures (legacy flag-off path verified)

Pre-existing ContestPublicControllerTest failure (ApplicationContext load
issue, unrelated to ADR-004) confirmed to fail on main without my changes.

Refs: docs/adr/ADR-004-notification-intents.md §4.
DavidHLP added a commit that referenced this pull request Jul 3, 2026
…nedAt)

2 review findings about intentId() collisions on the ledger UNIQUE
(intent_id, channel_id) constraint:

- SubmissionCompletedIntent: factory of() now throws IllegalStateException
  when submission.getGeneration() is null, instead of silently falling
  back to 0L. Two distinct submissions (one real g=0, one with null
  generation) would have produced identical intentIds, and the second
  dispatch would have been silently dropped by tryClaim returning 0.
  The null-generation case indicates a hydration bug, not user data, so
  fail-fast is correct. Fixes finding #5.
- AchievementEarnedIntent: added earnedAt field, intentId now includes
  the millisecond timestamp (:at<epochMs>). Without this, a re-issued
  event (tier-up promotion, system re-trigger) for the same
  achievement collapsed under the existing intentId and was silently
  dropped on every channel. The natural key for achievement
  notifications now includes the issue time, matching the
  AchievementEarnedEvent one-publish-per-earn invariant. Fixes
  finding #6.

Test updates: 5 test files updated to pass the new
earnedAt=Instant.now() arg. The sample intents in the channel contract
test and the per-channel tests use the new field; the legacy
follow-system test path is unchanged (FollowReceivedIntent has no
earnedAt).

mvn test on the notification/follow/submission/email slice: green.

Refs: docs/adr/ADR-004-notification-intents.md (M4d-1 review findings
#5, #6).
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…te class

Architecture review candidate #6 (partial — interface extraction):

PermissionService was a 248 LOC concrete @service with no interface,
violating the project's Java convention that all Service classes must
expose an interface with an Impl suffix (01-java-programming.md §四.16).

Extract PermissionService interface with all 6 public methods:
- Checking: getUserPermissions, getUserPermissionStrings, hasPermission
- Cache: invalidateCache
- Assignment: assignPermission
- Revocation: revokePermission

Create PermissionServiceImpl in service/impl/ with the full implementation.
All 7 callers depend on the interface — Spring wires transparently.

Verified: mvn compile succeeds, 11 PermissionServiceTest tests pass.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
Architecture review candidate #6 — management/src/utils/sanitize-markdown.ts
renderSafeMarkdown() called renderMarkdown() from shared/markdown-utils
(which already sanitizes via DOMPurify) and then ran DOMPurify AGAIN with
a locally-defined config. The same anti-pattern existed on the console side
via sanitizeHtml(renderMarkdown(...)) in 4 .vue files.

Both apps now call renderMarkdown() directly from @/shared/markdown-utils,
which owns the sanitization seam. Deletes:
  - console/src/utils/sanitize.ts (143 LoC, near-identical config)
  - management/src/utils/sanitize-markdown.ts (141 LoC, double sanitize)

management/src/utils/sanitize.ts (sanitizeI18nHtml + sanitizeCodeHtml) is
preserved — those are legitimately separate sanitization purposes (i18n
inline formatting + hljs token output), not markdown.

Verified: console 355 tests pass, management 275 tests pass, both type-check
clean. markdown-security.spec.ts now exercises the actual deep module
(shared renderMarkdown) rather than the redundant wrapper.

Wins (in glossary terms): locality — one sanitization config; leverage —
one interface (renderMarkdown), N call sites; removes ~280 LoC of duplicate
config that could silently drift.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
#1 SecurityUtil → CurrentUserProvider port
  - New CurrentUserProvider interface in common/auth/
  - SecurityCurrentUserProvider @component adapter (wraps SecurityContextHolder)
  - 37 files migrated from static SecurityUtil calls to injected interface
  - Eliminates duplicated userId==null guards across controller layer

#2 DashboardMapper+Service → DashboardStatsProjection
  - New DashboardStatsProjection interface + DefaultDashboardStatsProjection
  - Absorbs 7 private sub-aggregators + 6 mapper default methods
  - DashboardService + DashboardServiceImpl deleted
  - DashboardController now injects the projection directly
  - Mirrors ADR-0011 *Projection pattern

#3 AdminCommentService → CommentModerator polymorphic seam
  - New CommentModerator interface with ForumCommentModerator + SolutionCommentModerator
  - AdminCommentServiceImpl refactored to thin router
  - Eliminates 5x duplicated forum/solution type-string switch

#5 SubmissionMapper.calculateStreak → SubmissionStreakCalculator
  - New SubmissionStreakCalculator interface + JdbcSubmissionStreakCalculator
  - Streak algorithm (recursive CTE) now behind a JVM-testable interface
  - 3 production callers redirected + 3 test classes updated

#7 Frontend wrapper cleanup
  - 8 re-export wrapper files deleted (markdown, useTheme, useTypographyDensity, cn)
  - tsconfig path remaps @/lib/utils → @/shared/auth-core/src/utils
  - shared/sidebar-menu cn() consolidated to import from auth-core
  - LOCALE_HEADER_KEY duplicate declarations removed

#8 OAuthService → OAuthClient port + adapters
  - New OAuthClient interface + OAuthTokenResponse/OAuthUserInfo DTOs
  - GithubOAuthClient + GoogleOAuthClient @component adapters
  - OAuthService refactored to thin coordinator (221→195 LoC)
  - 11 unit tests covering dispatch, callback, DB upsert

Production code compiles (BUILD SUCCESS). 53 test compilation errors
from constructor signature changes — mechanical @mock field additions.
Candidates #4 (analytics projections) and #6 (contest.ts split) pending.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
…(arch-review #6)

The ContestSubscribeAuthInterceptor used to hold both the STOMP
translation AND the ContestParticipantMapper lookup. Move the
eligibility rules into a transport-agnostic
com.ulticode.modules.contest.subscription.ContestSubscriptionPolicy
module:

- New ContestSubscriptionPolicy interface with
  evaluate(ContestSubscribeRequest) returning a
  SubscriptionDecision (allow / deny_* verdict + reason).
- DefaultContestSubscriptionPolicy owns the mapper and the
  participant-row check; the policy tests no longer need a STOMP
  broker.
- ContestSubscribeAuthInterceptor is now a thin STOMP adapter:
  parse the destination, extract the user from the session, build
  a ContestSubscribeRequest, call the policy, translate the
  verdict back into a STOMP ERROR frame. The reject-vs-allow
  behaviour is preserved verbatim.

Future transports (SSE, gRPC server-stream) can build the same
ContestSubscribeRequest from their per-protocol data and reuse the
policy without dragging in spring-messaging.
DavidHLP added a commit that referenced this pull request Jul 10, 2026
#6)

- useLocale.ts: expand destructured i18n helpers (t/te/tm/rt/n/d) to one
  per line for readability
- i18n/index.ts: split 3-arg import from @/shared/locale-preference/src
  to one per line (prettier wrap); collapse the local re-export that fits
  back to a single line
DavidHLP added a commit that referenced this pull request Jul 16, 2026
Implements candidates from /tmp/architecture-review-1783827842.html:

#1 AdminBulkExecutor (admin/bulk): lifts the duplicated per-item try/catch
loop + counting + optional existence-guard out of AdminForum/Comment/
Solution/Problem bulk actions into one consumer-owned executor with a
canonical ItemOutcome/Run. batchRejudge withdrawn with evidence (rich
RejudgeResult shape ≠ void-action aggregated contract).

#2 ProblemExportService + ExportPayload: moves format validation, the 10k
size cap, CSV header/escaping, and the LocalDate.now() time-leak out of
AdminProblemController into a Clock-injected service; controller shrinks
to response-writing.

#3 LegacyRejudgeStrategy + RejudgePolicy.rejudge dispatcher: moves the
50-line inline legacy rejudge state machine out of AdminSubmissionServiceImpl
behind the policy port; fenced vs legacy selection now lives in
DefaultRejudgePolicy. AdminSubmissionServiceImpl.rejudge = 3-line dispatch.

#4 createAuthStore factory (shared/auth-core): owns the duplicated
login/logout/fetchUser/loadPermissions/initialize/clearUser/hasPermission
chain + CSRF contract once; management store migrated. Console deferred
(already composable-shaped with a richer status state machine).

#6 AdminContestReadPort + adapter: ADR-0011 phase 2 (contest) — admin
contest reads cross the seam instead of reaching into ContestProblemMapper.

#5 (AdminCrudListView wrapper) withdrawn: scaffolded then deleted after
code-review flagged it as dead code (0 views wired); the genuine shared
seam already lives in DataTable + useDataTable + DataTableToolbar.

Validation: mvn compile + test-compile green; 37 targeted admin/submission
tests pass (incl. new AdminBulkExecutorTest); management eslint green on
auth store. code-review skill run (Standards pass, Spec gaps documented
in-code).

Co-Authored-By: Claude <noreply@anthropic.com>
DavidHLP added a commit that referenced this pull request Jul 16, 2026
Extract shared/i18n-completeness owning locale consistency, code-to-locale
coverage, dynamic-prefix detection, and report formatting. Console and
management check.ts become thin adapters supplying locale trees + source
root; the management i18n-coverage and naming-convention specs now hit the
production interface instead of reimplementing flatten/scan/extract.

Closes candidate #6 of the 2026-07-15 architecture review.
DavidHLP added a commit that referenced this pull request Jul 20, 2026
Closes the Standards-axis judgement calls the two-axis review raised
(0 hard violations; these are the value-bearing ones):

- C06 data clump: ModerationServiceImpl.applyAction took 8 positional
  params (action, moderatorId, note, durationDays, now, queueId,
  actionId, item). Introduced a narrow ActionRequest value record for
  the per-action inputs so the switch reads (request, item). The record
  is a pure value, not the old ActionContext service-callback proxy.
- C01 primitive obsession + edge case: buildScoredContestProblems used a
  bare literal 100 for the default score and a bare (char)('A'+i) index
  that silently overflowed to non-letters past 26 problems. Named the
  default DEFAULT_PROBLEM_SCORE and extracted problemIndex(i) which
  returns A-Z for slots 0-25 and a deterministic P<n> label beyond,
  instead of garbage chars.
- C02 magic numbers: useProblemRun carried bare "javascript", 60, 5000.
  Named DEFAULT_RUN_LANGUAGE, RATE_LIMIT_FALLBACK_WAIT_SECONDS,
  RATE_LIMIT_TOAST_DURATION_MS.

Behaviour preserved. #2 (parseRuntimeMs home) and #6 (getSocketManager
name) left: parseRuntimeMs's docstring already distinguishes the sandbox
wire format from the queue parser, and renaming getSocketManager would
churn four callers for a cosmetic gain.

Validated: ModerationServiceImplTest(11) + AdminContestMutationServiceImplTest
green; console type-check + eslint clean; problem-detail suite(12) green.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant