개발 기록 2026-10-19 docs/devlog/2026-10-19-6-라운드-3-통합.md GitHub에서 보기

라운드 3 통합

목표

라운드 3 워커 4명(A 실행 경로, B 룰 7종, C S3/S4 화면, D AI 클라이언트)의 결과를 검토해 하나씩 병합하고, 그동안 메인 세션은 워커 결과에 기대는 S1, S2 화면(WBS 2.5, 2.6)을 만든다. 워커 A 는 이 문서를 처음 쓸 때 아직 작업 중이었다. A 병합은 2026-10-20 에 이 문서에 이어 적었다 ("A 병합" 절).

한 일

병합 (메인, 워커 결과 검토 뒤)

순서 워커 병합 커밋 병합 뒤 mix precommit
1 B 룰 7종, 정답 라벨 직접 대조 ff95ad8 293 passed
2 C S3 ImportLive, S4 TestRunLive, TickmarkWeb.Format af65bb3 317 passed
3 D AI Client, Fake, Anthropic, 트리아지 프롬프트 (오탐 수정 90f6184 포함) 3706032 347 passed

라운드 시작 전 선행 작업 뒤가 267 passed 였다.

  • D 는 첫 보고에서 계약 항목(설정 키, 코드에 API 키 없음, Req 옵션, Fake 모델 이름, input_hash/1)은 모두 맞았다. 문제는 금지 표현 검사였다. 메인 세션이 @forbidden_expressions 정규식을 정상적인 감사절차 문장에 직접 돌려 보니 "승인이 필요한 금액 기준을 확인한다", "거래에 문제가 없는지 증빙과 대조한다" 같은 6개 문장이 거절됐다. 실제 키로 돌리면 대부분의 제안이 invalid_output (fatal) 로 끝나고 재시도도 없다(도메인 4.4). 통과해야 할 문장, 계속 거절할 문장을 적어 D 에게 후속 작업으로 돌려보냈다. D 는 종결형(단정)만 걸도록 패턴을 좁히고, 정상 문장 11개를 모든 출력 필드에서, 기존 거절 문장 15개를 회귀 시험으로 확인했다(90f6184).

S1 최근 테스트, 검토 큐 열 (WBS 2.5, 메인)

  • Testing.latest_run_summaries/1 (4bac346): 내가 멤버인 감사 대상마다 가장 최근 실행의 상태와 검토 큐 수를 돌려준다. DISTINCT ON 으로 최근 실행을 고르고, 검토 큐는 실행마다 저장된 triage_threshold 로 한 번에 센다. 쿼리는 2번이다. 실행 중이면 검토 큐는 nil.
  • EngagementLive.Index 에 두 열을 더했다(1283c6d). 실행 상태 표시는 S2 에서도 쓰도록 TestRunLive.Components.run_status/1 로 뒀다. 318 passed.

S2 통계, 실행 목록, 테스트 실행 단추 (WBS 2.6, 메인, 5c7e560)

  • Testing.count_findings/2 에 approved (검토 큐 안의 승인 수) 를 더했다. 화면설계서 C-07 은 이것으로 해결했다.
  • EngagementLive.Components.stat_row/1: 전표, 라인(최근 성공 적재 기준), 검토 큐, 경미 적중, 승인 / 검토 큐(최근에 끝난 실행 기준). 끝난 실행이 없으면 "-" 와 안내 문구.
  • 실행 표(#test-runs 스트림): 시작 시각, 상태, 끝난 룰 / 전체 룰, 적중 전표, 검토 큐, 열기(S4). 실행 중이면 적중과 검토 큐는 "-".
  • 적재 표에 행동 열: 작성자에게만, 적재가 끝난 GL 에만 "테스트 실행" 단추가 보인다. 그 GL 에 실행 중인 테스트가 있으면 단추 대신 "실행 중" 문구.
  • {:test_run_updated, ...} 를 받으면 실행 행, 통계, 그 GL 의 단추를 다시 그린다. 351 passed.
  • 단추를 눌러 S4 로 가는 테스트는 아직 없다. start_test_run/3 이 A 병합 전까지 Runner 스텁에서 예외를 내기 때문이다.

문서

  • WBS 2.2, 2.3, 2.5, 3.1 완료, 2.4, 2.6 진행 중으로 갱신. TASKS.md, 색인, 화면설계서(v1.2 구현 상태, C-07 해결) 갱신.

A 병합 (2026-10-20 이어 씀)

  • 워커 A 보고: 실행 경로 완료, 새 테스트 34개, A 브랜치의 mix precommit 298 passed. 메인이 runner.ex, rule_worker.ex, failure_handler.ex, 동시성 테스트를 직접 읽고, finding 기본값 (status open, triage_status none) 과 application.ex 의 FailureHandler.attach/0 호출이 있는지 확인한 뒤 --no-ff 로 병합했다 (fadb51c).
  • A 가 작업 중에 계약 5.4 의 순서 변경을 물었다. 성공 경로가 "rule_results insert -> test_run FOR UPDATE" 이면 두 RuleWorker 가 동시에 끝날 때 교착이 난다. insert 의 FK 검사가 부모 test_run 에 FOR KEY SHARE 를 걸고, 그 뒤 FOR UPDATE 를 서로 기다리기 때문이다. "FOR UPDATE -> mark_rule -> insert" 로 바꾸는 것을 승인하고 계약에 적었다. 라운드 4 의 finding 자식 행 (ai_triages, conclusions, review_decisions) 에도 같은 lock-first 규칙을 적용한다.

측정

시점 mix test
선행 작업 뒤 267 passed
B 병합 293 passed
C 병합 317 passed
S1 (1283c6d) 318 passed
D 병합 347 passed
S2 (5c7e560) 351 passed (4.6초, 이 문서 작성 시 다시 실행)
A 병합 (fadb51c) 385 passed (7.6초)

결정과 트레이드오프

  • C-07 은 새 함수 대신 count_findings/2 를 넓혔다. 대안은 실행 요약 전용 함수(run_summary/2 등)였다. count_findings/2 는 이미 S4 탭 숫자에 쓰고, Ecto filter/2 로 칸 하나를 더하면 쿼리 수가 그대로다. 반환 맵에 키가 하나 늘어서 S4 기본값과 기존 테스트도 같이 고쳤다.
  • S2 실행 표의 룰 열은 실패한 룰도 끝난 룰로 센다. S4 진행 요약(test_progress/1)이 이미 그렇게 센다. 화면설계서 S2.2 상태 변형은 일부 실패를 "7 / 8" 로 그렸지만, 같은 실행이 S2 에서 7 / 8, S4 에서 8 / 8 로 보이는 것이 더 헷갈린다고 봤다. 실패 여부는 상태 열("일부 실패")이 따로 보여준다. 화면설계서 S2 영역 8 비고에 적었다.
  • S1 은 실시간이 아니다. 화면설계서 S1.3 대로 아무 토픽도 구독하지 않고 mount 때 한 번 읽는다. 감사 대상이 많아도 토픽 수가 늘지 않는다. 대신 목록을 다시 열어야 최신 상태가 보인다.
  • 단추를 숨겨도 서버 검사는 그대로 둔다. 화면은 실행 중인 GL 의 단추를 숨기지만, 다른 탭에서 동시에 누르는 경우는 start_test_run/3 의 :run_in_progress 가 막는다. 이 오류와 :import_not_ready, :forbidden 은 각각 한국어 flash 로 바꿨고 changeset 오류는 내부 내용을 보이지 않는 일반 문구로 둔다.
  • D 의 오탐은 메인이 고치지 않고 D 에게 돌려보냈다. anthropic.ex 는 D 소유 파일이고, 메인이 고치면 D 의 테스트와 devlog 가 실제 코드와 어긋난다. 대신 통과/거절 문장 목록을 계약처럼 넘겨 판단 기준을 메인이 정했다.

막힌 점과 해결

  • 스트림 행을 테스트가 못 찾았다. stream(:test_runs, ...) 의 기본 dom_id 는 "test_runs-<id>" 다(스트림 이름 그대로). 화면설계서의 id 규칙은 하이픈이라 stream/4 에 dom_id: &"test-runs-#{&1.id}" 를 줬다.

  • 약한 단언 두 개. "2 / 1" 이 맞게 나온 것은 fixture 의 rule_count 기본값 때문이었다. finished_run/3 이 rule_count: 2 를 넘기게 고쳤다. 검토자에게 행동 열이 없음을 확인하는 refute 가 #imports th 를 봤는데 #imports 는 tbody 라 언제나 통과했다. #imports-section th 로 고쳤다.

  • 임의 id 브로드캐스트. 기존 "다른 토픽 메시지" 테스트가 아무 id 로 test_run_updated 를 보냈다. 이제 핸들러가 get_test_run! 을 부르므로 프로세스가 죽는다. 그 테스트에서 빼고, 실제 실행으로 다시 그리는 경우를 새 테스트로 덮었다.

  • 승인 발견을 공용 setup 에 넣으면 다른 목록 테스트가 깨졌다. 승인 개수 테스트를 따로 만들어 그 안에서만 넣었다.

  • Orca 터미널. 세 가지가 있었다.

    • 워커 A 의 첫 실행에서 프롬프트가 전달되지 않아 다시 띄웠다.
    • git worktree add 로 만든 worktree 의 터미널은 Orca 에 등록되지 않아 orphaned 로 남고 화면에 안 보였다. orca worktree create 로 만들고 --worktree name: 으로 띄우도록 CLAUDE.md 에 적었다(801d680).
    • terminal_worktree_mismatch 는 --worktree path: 를 줘서 풀었다.
  • 정답 라벨 시험의 간헐 timeout. A 병합 뒤 라운드 4 선행 작업 중 전체 실행에서 RulesLabelTest 2건이 15초 timeout 으로 실패했다 (387/389, 36.8초). 단독으로 돌리면 4.8초에 통과했고 다시 전체를 돌리면 389 passed 였다. DB 에 남은 행이나 다른 활동은 없었다. DuplicateEntry 의 그룹 쿼리가 부하가 걸린 DB 에서 느려지는 것으로 보고, 원인 조사 (EXPLAIN ANALYZE) 는 라운드 4 W5 에 맡겼다.

Elixir 학습 포인트

  • LiveView 스트림은 서버에 행을 남기지 않는다. 렌더한 뒤 행 데이터는 클라이언트 DOM 에만 있다. 그래서 다른 assign(running_import_ids) 이 바뀌어도 이미 그린 적재 행의 단추는 저절로 바뀌지 않는다. 같은 항목을 stream_insert/3 로 다시 넣으면 같은 dom_id 의 행이 그 자리에서 바뀐다. 실행 행은 at: 0 으로 넣어 새 실행이 위에 온다.
  • 스트림 항목은 스키마가 아니어도 된다. %{id: ..., run: ..., done: ..., review_queue: ...} 처럼 :id 가 있는 맵이면 된다. 행마다 미리 계산한 값을 들고 다니게 해서 템플릿에서 쿼리를 하지 않는다.
  • HEEx :let 은 컴포넌트와 슬롯에만 쓴다. <tr :let={...}> 는 컴파일 오류다. 행마다 계산이 필요하면 헬퍼 함수(summary_for/2) 를 부른다. :if, :for 는 일반 태그에도 된다.
  • Ecto filter/2 집계. filter(count(f.id), f.score >= ^threshold and f.status == :approved) 는 SQL COUNT(...) FILTER (WHERE ...) 가 된다. 한 번의 쿼리로 검토 큐, 경미, 승인 세 칸을 센다.
  • DISTINCT ON 으로 그룹별 최신 행. distinct: r.engagement_id, order_by: [desc: r.started_at, desc: r.id] 는 감사 대상마다 가장 최근 실행 한 건을 고른다. Ecto 가 distinct 열을 ORDER BY 맨 앞에 넣어 주므로 order_by 에는 "무엇이 최신인가" 만 쓴다. r.id 는 같은 시각일 때 결과를 고정한다. PostgreSQL 전용 기능이다.

다음 할 일

  • (완료) 워커 A 병합과 해제.
  • 라운드 4 W5: S2 단추 클릭 테스트(성공 시 S4 로 이동, :run_in_progress flash), Runner 를 거친 정답 라벨 시험(2.4). 메인: S1-S4 브라우저 확인.
  • 라운드 4 W1: FailureHandler 에 Triage 분기. 라운드 4b 메인: S4 AI 열. 이어서 라운드 4 계약과 통합.
  • 사용자 결정 대기: PR #2 병합(1.19), 배포 대상(5.1).

관련 커밋

  • 9f0261b, d61146d, 456da8d: 라운드 3 선행 작업과 계약
  • 4bac346, daa9ce9: latest_run_summaries/1 과 계약 반영
  • 801d680: Orca 터미널 규칙
  • ff95ad8: 워커 B 병합 (24c2536, 1f6257b)
  • af65bb3: 워커 C 병합 (c62a65c, 04986da)
  • 90f6184, 3706032: D 오탐 수정과 워커 D 병합 (e4c1cd1, cd4497e)
  • 1283c6d: S1 두 열
  • 5c7e560: S2 통계, 실행 목록, 단추
  • fadb51c: 워커 A 병합 (1d28488, 2436619, aba197b)