Skip to content

Feat(#47): AI 사기문자 변형 공격 테스트 시스템 구축 - #48

Open
kite-pp wants to merge 1 commit into
developfrom
feat/47-adversarial-mutation-test
Open

Feat(#47): AI 사기문자 변형 공격 테스트 시스템 구축#48
kite-pp wants to merge 1 commit into
developfrom
feat/47-adversarial-mutation-test

Conversation

@kite-pp

@kite-pp kite-pp commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

📝 개요

규칙 엔진 단독으로는 탐지에 실패하지만 SafeFam 전체 파이프라인(나이브베이즈+Gemini 하이브리드 문맥분석)은 여전히 탐지함을 정량적으로 검증하는 내부 레드팀 테스트 하네스를 추가합니다. 실제 신고 데이터셋(phishing_total_dataset_2705.csv) 기반 코퍼스에 10종 변형(띄어쓰기 삽입, 특수문자 삽입, 기관명 변형, 숫자→문자 치환, URL 제거, 오탈자, 그리고 Gemini 패러프레이즈 기반 긴급성 순화/문체 정상화/축약/전화상담 유도)을 적용해 원본 vs 변형의 규칙엔진/전체 파이프라인 탐지율을 비교합니다.

app/, data_science/SMSModel/ 등 프로덕션 코드·모델은 무수정이며, 기존 테스트(tests/analysis, tests/security) 84건 통과 확인했습니다.

🔗 관련 이슈

🎯 주요 변경 사항

  • scripts/adversarial_test/corpus.py: 카테고리/키워드/규칙엔진 점수 기준으로 사기문자 코퍼스 선정
  • scripts/adversarial_test/mutations.py: 변형 10종 생성기 (규칙기반 6종 + Gemini 패러프레이즈 4종)
  • scripts/adversarial_test/evaluate.py, aggregate.py: 규칙엔진 단독 vs 전체 파이프라인 평가 및 탐지율 집계, 요약 리포트 생성
  • scripts/adversarial_test/daily_batch.py, import_manual_mutations.py, rate_limit.py: Gemini 무료 티어 일일 할당량(모델당 20건) 제약 안에서 평가 매트릭스를 재개 가능한 배치로 나눠 수행
  • .gitignore: scripts/adversarial_test/output/(코퍼스·결과·리포트 산출물) 추가

📸 사진

생략

✅ PR 체크리스트

  • 관련 이슈를 연결했습니다.
  • 구현 범위와 변경 이유를 설명했습니다.
  • 로컬 테스트(uvicorn 구동 또는 테스트 코드)를 통과했습니다.
  • API 변경 사항이 있다면 Swagger / API 명세에 반영했습니다. (해당 없음 - API 변경 없음)
  • 민감 정보(API Key, 시크릿 키 등)가 코드·로그·테스트 데이터에 포함되지 않았습니다.
  • 프론트엔드 또는 메인 백엔드(Spring)에 영향을 주는 응답 스키마 또는 Enum 변경이 있다면 팀에 공유했습니다. (해당 없음 - 응답 스키마 변경 없음)
  • 병합(Merge) 전 작업 브랜치를 삭제하지 않았습니다.

Summary by CodeRabbit

  • New Features
    • Added an adversarial testing toolkit for evaluating fraud-message detection robustness.
    • Supports generating representative test corpora, applying text mutations, and running automated evaluations.
    • Added configurable batch processing with sampling, filtering, mutation selection, concurrency, and budget controls.
    • Added CSV result export and Markdown summaries with detection rates, scores, mutation statistics, and missed-case examples.
  • Chores
    • Excluded generated adversarial-test output files from version control.

규칙 엔진 단독으로는 탐지 실패하지만 SafeFam 전체 파이프라인(나이브베이즈+
Gemini 하이브리드 문맥분석)은 여전히 탐지함을 정량 검증하는 내부 레드팀
테스트 하네스. 실제 사기문자 코퍼스에 10종 변형(규칙기반 6종 + Gemini
패러프레이즈 4종)을 적용해 탐지율을 비교한다.

- corpus.py: 카테고리/키워드/규칙엔진 점수 기준 코퍼스 선정
- mutations.py: 변형 10종 생성기
- evaluate.py / aggregate.py: 규칙엔진 단독 vs 전체 파이프라인 평가 및 집계
- daily_batch.py / import_manual_mutations.py / rate_limit.py: Gemini 무료
  티어 일일 할당량 제약 내에서 평가를 재개 가능한 배치로 분할 수행

app/, data_science/SMSModel/ 등 프로덕션 코드/모델은 무수정.
@kite-pp kite-pp added the feat New feature or functional additions to the application label Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added an adversarial SMS testing workflow. It selects corpus records, generates deterministic and Gemini-based mutations, evaluates rule and pipeline detection, supports budgeted execution, and writes CSV and Markdown reports.

Changes

Adversarial SMS evaluation

Layer / File(s) Summary
Corpus selection and mutation generation
scripts/adversarial_test/corpus.py, scripts/adversarial_test/mutations.py, scripts/adversarial_test/rate_limit.py
Adds filtered corpus generation, deterministic sampling, rule-based mutations, Gemini paraphrasing, and shared rate limiting.
Concurrent adversarial evaluation
scripts/adversarial_test/evaluate.py
Evaluates original and mutated messages with rule and pipeline analysis, then serializes successful results.
Budgeted and manual evaluation workflows
scripts/adversarial_test/daily_batch.py, scripts/adversarial_test/import_manual_mutations.py
Adds budget-aware daily processing and manual mutation imports with duplicate filtering and incremental CSV persistence.
End-to-end runner and result reporting
scripts/adversarial_test/run.py, scripts/adversarial_test/aggregate.py, .gitignore
Adds the complete CLI workflow, detection-rate aggregation, Markdown reporting, and ignored output files.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Runner
  participant CorpusSelector
  participant MutationRegistry
  participant AnalysisService
  participant ResultStore
  participant ReportBuilder
  Runner->>CorpusSelector: select and persist corpus
  Runner->>MutationRegistry: generate original and mutated messages
  MutationRegistry->>AnalysisService: submit messages for analysis
  AnalysisService->>ResultStore: write evaluation results
  ResultStore->>ReportBuilder: load CSV results
  ReportBuilder->>Runner: write Markdown summary
Loading

Possibly related issues

Possibly related PRs

  • SafeFam/SafeFam_AI#20 — Introduces scoring and analysis behavior exercised by the new adversarial evaluation scripts.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 AI 사기문자 변형 공격 테스트 시스템을 구축하는 변경 사항의 주요 목적을 명확하게 요약합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/47-adversarial-mutation-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
scripts/adversarial_test/mutations.py (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer collections.abc.Callable.

typing.Callable is deprecated since Python 3.9. Use the collections.abc alias for new code.

♻️ Proposed refactor
 import logging
 import random
 import re
-from typing import Callable
+from collections.abc import Callable
🤖 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 `@scripts/adversarial_test/mutations.py` around lines 1 - 9, Replace the
typing.Callable import in mutations.py with Callable imported from
collections.abc, preserving its existing usage throughout the module.
scripts/adversarial_test/run.py (1)

20-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Share the corpus CLI options through a parent parser.

Five arguments here (--csv, --per-type, --category-overrides, --keywords, --min-rule-score) are copied verbatim from scripts/adversarial_test/corpus.py, including the help strings. A change in one file will not reach the other, and the two entry points will accept different defaults.

Define the shared options once in corpus.py as a parser with add_help=False, then pass it through parents=[...] here.

🤖 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 `@scripts/adversarial_test/run.py` around lines 20 - 45, Define the shared
corpus CLI options once in corpus.py using an argparse parent parser with
add_help=False, including --csv, --per-type, --category-overrides, --keywords,
and --min-rule-score with their existing defaults and help text. Update main in
run.py to use that parser via parents=[...] and remove the duplicated argument
definitions, leaving run.py-specific options unchanged.
scripts/adversarial_test/import_manual_mutations.py (1)

30-41: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Validate mutation_type against the known mutation names.

The keys come from a hand-written JSON file. No check rejects an unknown name. A typo such as shortenning is written to results.csv and build_summary in scripts/adversarial_test/aggregate.py groups it as a separate mutation type. The report then shows a spurious row and understates the real mutation's sample count.

The key ORIGINAL is also accepted. It would count manual text as a baseline original and distort the comparison in section 1 of the report.

♻️ Proposed fix
+from scripts.adversarial_test.evaluate import ORIGINAL_LABEL
+from scripts.adversarial_test.mutations import MUTATIONS
...
         for mutation_type, text in mutations.items():
+            if mutation_type == ORIGINAL_LABEL or mutation_type not in MUTATIONS:
+                logger.warning(
+                    "[Import] 알 수 없는 변형 유형 건너뜀: %s/%s", sample_id, mutation_type
+                )
+                continue
             if not text or (sample_id, mutation_type) in done:
                 continue
             queue.append((sample, mutation_type, text))
🤖 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 `@scripts/adversarial_test/import_manual_mutations.py` around lines 30 - 41,
Validate each mutation_type in the manual mutation import loop against the
established known mutation-name set before queueing it; skip unknown names and
reject ORIGINAL so invalid keys cannot reach results.csv or build_summary. Keep
valid, non-empty, non-completed mutations queued as before.
scripts/adversarial_test/evaluate.py (1)

44-50: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reusable workflow steps are exposed under private names. _evaluate_one, _load_done, and _append_result are the shared building blocks of three entry points, but the underscore prefix marks them as module-private. Two modules must therefore import private symbols across module boundaries, and a future rename inside evaluate.py or daily_batch.py breaks them with no signal.

  • scripts/adversarial_test/evaluate.py#L44-L50: rename _evaluate_one to evaluate_one and keep it as the public single-case entry point.
  • scripts/adversarial_test/daily_batch.py#L30-L44: rename _load_done to load_done and _append_result to append_result, or move both into evaluate.py next to write_results_csv so persistence lives in one module.
  • scripts/adversarial_test/daily_batch.py#L10: update the import to the renamed public evaluate_one.
  • scripts/adversarial_test/import_manual_mutations.py#L8-L9: update both imports to the renamed public load_done, append_result, and evaluate_one.
🤖 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 `@scripts/adversarial_test/evaluate.py` around lines 44 - 50, Expose the shared
workflow helpers under public names: rename evaluate.py’s _evaluate_one to
evaluate_one, daily_batch.py’s _load_done to load_done, and _append_result to
append_result. Update imports and call sites in
scripts/adversarial_test/daily_batch.py lines 30-44 and 10, and
scripts/adversarial_test/import_manual_mutations.py lines 8-9, to use the
renamed symbols; no direct change is needed beyond these renames and references.
scripts/adversarial_test/daily_batch.py (1)

19-27: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the Gemini mutation set from MUTATIONS instead of duplicating it.

_GEMINI_MUTATION_NAMES restates which mutations call Gemini. MUTATIONS already encodes this: the Gemini mutations are the coroutine functions. If a new async mutation is added to scripts/adversarial_test/mutations.py and this set is not updated, _estimate_cost under-counts by one call per case and the batch exceeds the free-tier daily limit.

♻️ Proposed refactor
+import inspect
...
-# LLM 기반 변형은 변형 생성(1콜) + 파이프라인 평가(1콜) = 2콜, 그 외는 파이프라인 평가만 1콜.
-# Gemini 무료 티어 일일 한도(모델당 20건)를 며칠에 걸쳐 나눠 쓰기 위한 하루치 예산 산정에 사용.
-_GEMINI_MUTATION_NAMES = {"urgency_softening", "tone_normalization", "shortening", "phone_call_redirect"}
+# LLM 기반 변형은 변형 생성(1콜) + 파이프라인 평가(1콜) = 2콜, 그 외는 파이프라인 평가만 1콜.
+# Gemini 호출 여부는 MUTATIONS의 코루틴 여부로 판별해 목록 중복을 없앤다.
+_GEMINI_MUTATION_NAMES = {
+    name for name, fn in MUTATIONS.items() if inspect.iscoroutinefunction(fn)
+}
🤖 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 `@scripts/adversarial_test/daily_batch.py` around lines 19 - 27, Replace the
duplicated _GEMINI_MUTATION_NAMES set with logic in _estimate_cost that derives
Gemini usage from the coroutine-function entries in MUTATIONS. Preserve
ORIGINAL_LABEL as one call, count coroutine mutations as two calls, and keep
synchronous mutations at one call so newly added async mutations are included
automatically.
🤖 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 `@scripts/adversarial_test/daily_batch.py`:
- Around line 37-44: Update _append_result to determine header writing from the
existing output file’s size, treating a missing or zero-byte file as requiring a
header; preserve append behavior and write the header before the first data row
for non-empty results.csv files.

In `@scripts/adversarial_test/evaluate.py`:
- Around line 130-137: Update write_results_csv to refuse overwriting an
existing output file by default, adding an explicit overwrite parameter and only
using truncating write mode when it is enabled. Add the matching --overwrite CLI
option in the evaluate.py argument parser and pass args.overwrite at every
write_results_csv call, preserving normal writes for new files.

---

Nitpick comments:
In `@scripts/adversarial_test/daily_batch.py`:
- Around line 19-27: Replace the duplicated _GEMINI_MUTATION_NAMES set with
logic in _estimate_cost that derives Gemini usage from the coroutine-function
entries in MUTATIONS. Preserve ORIGINAL_LABEL as one call, count coroutine
mutations as two calls, and keep synchronous mutations at one call so newly
added async mutations are included automatically.

In `@scripts/adversarial_test/evaluate.py`:
- Around line 44-50: Expose the shared workflow helpers under public names:
rename evaluate.py’s _evaluate_one to evaluate_one, daily_batch.py’s _load_done
to load_done, and _append_result to append_result. Update imports and call sites
in scripts/adversarial_test/daily_batch.py lines 30-44 and 10, and
scripts/adversarial_test/import_manual_mutations.py lines 8-9, to use the
renamed symbols; no direct change is needed beyond these renames and references.

In `@scripts/adversarial_test/import_manual_mutations.py`:
- Around line 30-41: Validate each mutation_type in the manual mutation import
loop against the established known mutation-name set before queueing it; skip
unknown names and reject ORIGINAL so invalid keys cannot reach results.csv or
build_summary. Keep valid, non-empty, non-completed mutations queued as before.

In `@scripts/adversarial_test/mutations.py`:
- Around line 1-9: Replace the typing.Callable import in mutations.py with
Callable imported from collections.abc, preserving its existing usage throughout
the module.

In `@scripts/adversarial_test/run.py`:
- Around line 20-45: Define the shared corpus CLI options once in corpus.py
using an argparse parent parser with add_help=False, including --csv,
--per-type, --category-overrides, --keywords, and --min-rule-score with their
existing defaults and help text. Update main in run.py to use that parser via
parents=[...] and remove the duplicated argument definitions, leaving
run.py-specific options unchanged.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8dd6066-beaf-42a5-a1ed-53bdbdfe7c8d

📥 Commits

Reviewing files that changed from the base of the PR and between dd7d952 and 915f8d8.

📒 Files selected for processing (10)
  • .gitignore
  • scripts/adversarial_test/__init__.py
  • scripts/adversarial_test/aggregate.py
  • scripts/adversarial_test/corpus.py
  • scripts/adversarial_test/daily_batch.py
  • scripts/adversarial_test/evaluate.py
  • scripts/adversarial_test/import_manual_mutations.py
  • scripts/adversarial_test/mutations.py
  • scripts/adversarial_test/rate_limit.py
  • scripts/adversarial_test/run.py

Comment on lines +37 to +44
def _append_result(result: EvalResult, out_path: Path) -> None:
is_new = not out_path.exists()
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("a", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
if is_new:
writer.writeheader()
writer.writerow(asdict(result))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Write the header when the file exists but is empty.

is_new is False for a zero-byte results.csv. A crash or an external touch between creation and the first write leaves such a file. _load_done and read_results in scripts/adversarial_test/aggregate.py then read the first data row as the header, and every field lookup fails.

Base the decision on the file size.

🛡️ Proposed fix
 def _append_result(result: EvalResult, out_path: Path) -> None:
-    is_new = not out_path.exists()
     out_path.parent.mkdir(parents=True, exist_ok=True)
+    is_new = not out_path.exists() or out_path.stat().st_size == 0
     with out_path.open("a", encoding="utf-8", newline="") as f:
📝 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.

Suggested change
def _append_result(result: EvalResult, out_path: Path) -> None:
is_new = not out_path.exists()
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("a", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
if is_new:
writer.writeheader()
writer.writerow(asdict(result))
def _append_result(result: EvalResult, out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
is_new = not out_path.exists() or out_path.stat().st_size == 0
with out_path.open("a", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
if is_new:
writer.writeheader()
writer.writerow(asdict(result))
🤖 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 `@scripts/adversarial_test/daily_batch.py` around lines 37 - 44, Update
_append_result to determine header writing from the existing output file’s size,
treating a missing or zero-byte file as requiring a header; preserve append
behavior and write the header before the first data row for non-empty
results.csv files.

Comment on lines +130 to +137
def write_results_csv(results: list[EvalResult], out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
writer.writeheader()
for result in results:
writer.writerow(asdict(result))
logger.info("[Evaluate] 결과 %d건 저장 -> %s", len(results), out_path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

write_results_csv truncates the file that the daily batch workflow accumulates.

out_path.open("w", ...) truncates. The default out_path is output/results.csv, which is the same default path that _append_result in scripts/adversarial_test/daily_batch.py and scripts/adversarial_test/import_manual_mutations.py append to across days.

A single run of evaluate.py or run.py with default arguments therefore deletes every result already collected under the Gemini daily budget. Those results cannot be regenerated cheaply, because each row consumed quota.

Guard the overwrite. Refuse to truncate an existing file unless the caller opts in.

🛡️ Proposed fix: require an explicit overwrite opt-in
-def write_results_csv(results: list[EvalResult], out_path: Path) -> None:
+def write_results_csv(results: list[EvalResult], out_path: Path, overwrite: bool = False) -> None:
     out_path.parent.mkdir(parents=True, exist_ok=True)
+    # daily_batch/import_manual_mutations는 같은 파일에 append로 예산을 며칠에 걸쳐 쌓는다.
+    # 기본 경로를 말없이 덮어쓰면 재생성 비용이 큰 결과가 사라진다.
+    if out_path.exists() and not overwrite:
+        raise FileExistsError(
+            f"{out_path}가 이미 존재합니다. 덮어쓰려면 --overwrite를 지정하세요."
+        )
     with out_path.open("w", encoding="utf-8", newline="") as f:

Add the matching CLI flag:

parser.add_argument("--overwrite", action="store_true", help="기존 results.csv를 덮어쓴다")
...
write_results_csv(results, args.out, overwrite=args.overwrite)
📝 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.

Suggested change
def write_results_csv(results: list[EvalResult], out_path: Path) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
with out_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
writer.writeheader()
for result in results:
writer.writerow(asdict(result))
logger.info("[Evaluate] 결과 %d건 저장 -> %s", len(results), out_path)
def write_results_csv(results: list[EvalResult], out_path: Path, overwrite: bool = False) -> None:
out_path.parent.mkdir(parents=True, exist_ok=True)
# daily_batch/import_manual_mutations는 같은 파일에 append로 예산을 며칠에 걸쳐 쌓는다.
# 기본 경로를 말없이 덮어쓰면 재생성 비용이 큰 결과가 사라진다.
if out_path.exists() and not overwrite:
raise FileExistsError(
f"{out_path}가 이미 존재합니다. 덮어쓰려면 --overwrite를 지정하세요."
)
with out_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=list(EvalResult.__dataclass_fields__))
writer.writeheader()
for result in results:
writer.writerow(asdict(result))
logger.info("[Evaluate] 결과 %d건 저장 -> %s", len(results), out_path)
🤖 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 `@scripts/adversarial_test/evaluate.py` around lines 130 - 137, Update
write_results_csv to refuse overwriting an existing output file by default,
adding an explicit overwrite parameter and only using truncating write mode when
it is enabled. Add the matching --overwrite CLI option in the evaluate.py
argument parser and pass args.overwrite at every write_results_csv call,
preserving normal writes for new files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat New feature or functional additions to the application

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant