Skip to content

docs(S15P11A705-160): 재스캔 명세 3.1의 「Finalize를 먼저」 근거 정정 - #107

Merged
colosair merged 1 commit into
devfrom
docs/S15P11A705-160-rescan-finalize-order-rationale
Jul 30, 2026
Merged

docs(S15P11A705-160): 재스캔 명세 3.1의 「Finalize를 먼저」 근거 정정#107
colosair merged 1 commit into
devfrom
docs/S15P11A705-160-rescan-finalize-order-rationale

Conversation

@colosair

Copy link
Copy Markdown
Member

요약

재스캔 명세 3.1이 「Finalize를 먼저」의 근거로 든 시나리오는 실제로는 발생하지 않습니다. 실제로 마지막 재시도의 실행 창을 만드는 장치(retry_count 증가 시의 updated_at 갱신)가 명세에서 누락돼 있었고, 이를 3.1에 명시하고 근거를 6.1과 일치시켰습니다. 문서 전용 변경으로, 코드·DB·API 계약 변경은 없습니다.

Jira (필수)

  • 키 또는 URL: S15P11A705-160

관련 GitHub Issue (선택)

변경 사항

  • docs/ai/spec/ai-rescan-scheduler.md §3.1 — 처리 순서 3단계를 retry_count 증가에서 retry_count 증가 + updated_at 갱신으로 고치고, 스키마의 updated_at TIMESTAMPTZ NOT NULL DEFAULT now()가 INSERT 기본값이라 UPDATE를 자동 갱신하지 않는다는 점을 명시했습니다. 명세만 보고 구현하면 이 갱신이 빠질 수 있던 자리입니다.
  • docs/ai/spec/ai-rescan-scheduler.md §3.1 — 「Finalize를 먼저」의 근거를 6.1과 같은 서술로 맞췄습니다. 창을 만드는 것은 만료 조건 + updated_at 갱신이고, 순서는 그 위에 겹치는 심층 방어라는 관계를 명시했습니다. 순서 자체는 유지했습니다(아래 리뷰 포인트 2).
  • docs/ai/spec/ai-rescan-scheduler.md §5 — 코드블록에만 있던 updated_at = now()에 왜 선택이 아닌지를 붙이고, 누락 시 어떤 일이 벌어지는지(다음 회차 Finalizer 첫 단계에 잡힘)를 적었습니다.
  • docs/ai/spec/ai-rescan-scheduler.md §6.1 — 마지막 문장 한 줄을 정밀화해 3.1을 역참조하게 했습니다. 6.1은 이미 "함께"라고 정확히 적고 있었으나 무엇과 함께인지가 updated_at 갱신까지 닿아 있지 않았습니다. 6.1 재작성은 하지 않았습니다.
  • docs/ai/WORKLOG.md — 한 줄 추가. AI 파트 문서 구역이라 docs/backend/WORKLOG.md는 건드리지 않았습니다(#104와 충돌 없음).

테스트 / 검증

  • ./gradlew clean check --no-daemon
  • DB 변경 시 PostgreSQL 통합 테스트 — DB 변경 없음
  • migration 변경 시 빈 DB migration 테스트 — migration 변경 없음
  • API 계약 변경 시 관련 문서 갱신 — API 계약 변경 없음
  • 되돌리기 어려운 결정 시 BD 추가 — 새 결정이 아니라 기존 결정의 근거 정정. 실측 기록은 #104의 BI-28에 있습니다.

RED / GREEN: 해당 없음 — 문서 전용 변경이라 이 PR이 통과시키는 새 테스트가 없습니다. 숨기지 않고 적습니다. 이 정정을 촉발한 실측은 이 PR이 아니라 #104에서 나왔고, 그쪽 테스트가 두 장치를 각각 고정하고 있습니다.

  • AiRescanSchedulerTests#theLastRetryActuallyGoesOutAndIsNotFinalizedInTheSameRound — 마지막 재시도가 같은 회차에 종결되지 않음 (실제 방어: 만료 조건 + updated_at 갱신)
  • AiRescanSchedulerOrderTest#finalizeRunsBeforeTheCandidateClaimInEveryRound — Mockito InOrder로 호출 순서 고정 (심층 방어)

Regression — 이 브랜치에서 실행했습니다.

$ ./gradlew clean check --no-daemon
BUILD SUCCESSFUL in 1m 41s
9 actionable tasks: 9 executed

배경

#104 구현 중 실측으로 드러났습니다. runOnce의 두 줄(Finalize / 재스캔)을 맞바꿔도 테스트가 통과합니다. 3.1이 경고한 상황이 발생하지 않기 때문입니다.

원인은 재시도 증가 SQL입니다.

UPDATE ai.context_ai_state
SET retry_count = retry_count + 1,
    updated_at = now()
WHERE context_id IN (:contextIds)

updated_at이 함께 갱신되므로 그 행은 즉시 만료 술어(updated_at < now() - interval)를 벗어나고, 같은 회차에서 Finalizer 후보 조건(6.1)에 걸릴 수 없습니다.

그런데 명세는 두 곳에서 다르게 말하고 있었습니다.

서술 문제
3.1 "Finalize를 먼저 수행합니다. 나중에 두면 같은 회차에서 방금 retry_count를 3으로 올린 행을 곧바로 FAILED로 종결해…" 순서가 유일한 방어로 읽힘
6.1 "3.1의 「Finalize를 먼저」 순서와 이 만료 조건이 함께 그 창을 확보합니다" 이쪽이 정확

게다가 3.1의 처리 순서는 retry_count 증가 → Commit까지만 적고 있었습니다. 스키마의 DEFAULT now()는 INSERT 기본값이라 UPDATE를 자동 갱신하지 않으므로, 실제로 창을 확보하는 장치가 명세에 빠져 있고 구현이 그것을 스스로 채운 상태였습니다. 다른 구현이 이 갱신을 누락하면 3.1이 경고한 시나리오가 그때는 진짜로 발생합니다.

리뷰 포인트

  1. 6.1을 얼마나 건드릴 것인가. 6.1은 이미 정확해 재작성 대상이 아니었지만, "함께"의 대상에 updated_at 갱신이 빠져 있어 3.1만 고치면 두 절이 여전히 다른 말을 하게 됩니다. 한 문장 정밀화 + 3.1 역참조로 최소 침습을 택했습니다. 이 정도가 적절한지 봐 주세요.
  2. 「Finalize를 먼저」 순서를 유지한 이유. 근거 하나가 무너졌다고 순서를 뒤집으면, 나중에 어느 구현이 updated_at 갱신을 빠뜨렸을 때 아무 방어도 남지 않고 그 사실을 알아챌 방법도 없습니다. 두 장치가 서로를 받치는 구조로 서술을 남겼습니다. 순서를 명세에서 빼자는 반대 의견이 있다면 여기서 다뤄 주세요.
  3. §5의 서술 정확도. 초안에서 "빠뜨리면 같은 회차의 Finalizer 후보로 잡힌다"고 썼다가, 그건 3.1의 순서 방어를 무시한 서술이라 "바로 다음 회차의 Finalizer 첫 단계에 잡힌다"로 고쳤습니다. 순서 방어가 살아 있는 전제에서 이 표현이 맞는지 확인 부탁드립니다.

미결 / 후속

  • #104와의 관계 — 이 PR은 코드를 건드리지 않고 docs/ai/spec/ai-rescan-scheduler.md만 고칩니다. #104는 이 파일을 건드리지 않으므로 파일 충돌은 없습니다. 어느 쪽이 먼저 머지돼도 무방합니다.
  • 공용 계약 Team-PinLog/docs static/05_AI_설계.md §10.3·§10.4 — 실행 순서만 규정하고 updated_at 갱신이라는 세부는 담고 있지 않습니다. 이 티켓 범위 밖이며, 반영이 필요하다고 판단되면 별건으로 올립니다.
  • 문서 머리의 "현재 코드가 없는 구현 예정 명세입니다" 표기는 #104가 머지되는 시점에 낡습니다. docs/ai/README.md의 같은 문구와 함께 정리해야 하며, 이 PR 범위 밖입니다.

3.1이 근거로 든 시나리오("나중에 두면 같은 회차에서 방금 retry_count를
3으로 올린 행을 곧바로 FAILED로 종결")는 실제로 발생하지 않습니다.
retry_count 증가가 updated_at을 함께 갱신하므로 그 행은 즉시 만료 술어를
벗어나고, 같은 회차의 Finalizer 후보 조건(6.1)에 걸릴 수 없습니다.

그런데 그 updated_at 갱신이 3.1 처리 순서에 빠져 있었습니다. 스키마의
DEFAULT now()는 INSERT 기본값이라 UPDATE를 자동 갱신하지 않으므로,
명세만 보고 구현하면 갱신이 누락될 수 있는 상태였습니다.

- 3.1 처리 순서 3단계에 updated_at 갱신을 명시하고 스키마 기본값 함정을 기록
- 3.1의 근거를 6.1과 일치시킴 — 창을 만드는 것은 만료 조건 + updated_at
  갱신이고, 「Finalize를 먼저」 순서는 그 위에 겹치는 심층 방어
- 순서 자체는 유지 — updated_at 갱신이 빠졌을 때 남는 유일한 방어선
- 3.1 / 5장 / 6.1이 서로를 참조하도록 연결

구현 변경은 없습니다. back#104가 이미 옳고, 명세를 구현에 맞춘 것입니다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@colosair
colosair merged commit 67e412a into dev Jul 30, 2026
2 checks passed
@colosair
colosair deleted the docs/S15P11A705-160-rescan-finalize-order-rationale branch July 30, 2026 05:38
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