LogoSkills

bdd-generate

BDD Feature 파일과 Step Definition 생성

/cc-flutter:bdd:generate — 시나리오로 테스트 자동 만들기#

항목내용
실행 명령/cc-flutter:bdd:generate
분류워크플로우
난이도●●○ 보통
MCP 서버sequential, context7, serena

한마디로#

"사용자가 이렇게 하면 이런 결과가 나와야 한다"는 시나리오를 사람이 읽는 문장으로 적으면, 그걸 실제로 검증하는 자동 테스트 코드로 바꿔주는 명령입니다. 시험문제(시나리오)와 채점표(테스트)를 한 번에 만들어 주는 도구라고 보면 됩니다.

누가·언제 쓰나요#

  • 새로 만든 기능(Feature)이 의도대로 동작하는지 자동으로 검증하고 싶은 개발자
  • 디자인 분석(/cc-flutter:figma:analyze) 과정의 마지막 단계에서 테스트가 자동으로 필요할 때
  • 이미 있는 기능에 뒤늦게 테스트를 붙이고 싶을 때

무엇을 해주나요#

  • 사람이 읽는 시나리오 문서인 .feature 파일(예: community_list.feature, community_detail.feature, community_form.feature)을 만들어 줍니다. 시나리오 제목과 설명은 한국어로 적습니다.
  • 그 시나리오를 실제로 실행하는 테스트 코드인 Step Definition을 자동으로 만들어 줍니다.
  • 이미 만들어 둔 공용 테스트 조각이 있으면 새로 만들지 않고 재사용해서 중복을 줄여 줍니다. step 하나하나에 대해 "재사용했는지 / 새로 만들었는지"와 그 재사용률을 기록으로 남깁니다.

어떻게 쓰나요#

# 기본: community 기능의 시나리오 + 테스트 생성
/cc-flutter:bdd:generate community

# 기존 기획 문서(.claude/docs)에서 가져와 작성하고, Patrol 테스트까지 자동 실행
/cc-flutter:bdd:generate community \
  --from-claude-docs true \
  --run-build true

# 테스트 코드(Step Definition)만 작성
/cc-flutter:bdd:generate community \
  --only-steps true

# 특정 화면만 작성 (목록·상세 화면만)
/cc-flutter:bdd:generate community \
  --screens  " list,detail "
  • feature_name (필수): 대상 기능 이름. 예) community
  • --screens: 어떤 화면을 만들지. 예) "list,detail,form" (목록/상세/입력)
  • --from-claude-docs: 이미 작성된 기획 문서에서 내용을 가져옵니다.
  • --only-steps: 테스트 코드만 만듭니다.
  • --run-build: 작성한 Patrol 테스트를 자동으로 실행합니다 (⛔ 코드 생성 단계는 없습니다 — build.yaml/build_runner를 돌리지 않습니다).

안에서 무슨 일이 벌어지나요#

  1. 새 시나리오를 짜기 전에 먼저 검색 — 이미 만들어 둔 비슷한 테스트 조각이 있는지 찾아보고, 있으면 그대로 재사용합니다(중복 방지). 검색 결과가 0건이면 「없음」으로 넘기지 않고 멈춥니다 — 찾는 위치가 틀렸을 때도 똑같이 0건이 나오기 때문입니다. 그래서 위치와 무관한 「표준 문장 사전」을 함께 대조하고, step별 재사용/신규 결정을 기록에 남깁니다.
  2. 시나리오 작성 — "앱 실행 → 로그인 → 탭 클릭 → 결과 확인" 같은 흐름을 사람이 읽는 문장(한국어 제목 + 영어 단계)으로 정리합니다.
  3. 테스트 코드 생성 — 각 문장에 대응하는 실제 검증 코드(Step Definition)를 만들고, 공용 라이브러리에 있는 것은 가져다 씁니다.
  4. 실행·확인 — 필요하면 코드 생성과 테스트 실행까지 이어서 진행해, 시나리오대로 동작하는지 확인합니다.

⚙️ 상세 옵션·실행 명세 (개발자 / AI 에이전트용)

Project Language Rules Summary#

CategoryLanguageNote
Feature title/descriptionKoreanWrite freely
Scenario title/descriptionKoreanWrite freely
Step patternEnglishKorean Usage comment required at top
Step parameter (UI text){'Korean value'}Passed as Dart string
Custom Step functionEnglish camelCaseEnglish/Korean annotation in Usage comment

Triggers#

  • When generating BDD tests for a Feature
  • /cc-flutter:figma:analyze Phase 6
  • When adding BDD tests to existing Features

Context Trigger Pattern#

/cc-flutter:bdd:generate {feature_name} [--options]

Parameters#

ParameterRequiredDescriptionExample
feature_nameFeature명 (snake_case)community
--entity-nameEntity명 (Auto-inferred)Post
--locationFeature Locationapplication
--screensScreen Type"list,detail,form"
--from-claude-docsCopy from .claude/docstrue
--only-stepsWrite Step Definitions onlytrue
--run-buildAuto-run the hand-written Patrol test (⛔ no build/codegen step)true

Project Structure#

⛔ feature 패키지(feature/{location}/{feature_name}/) 안에는 BDD 관련 파일이 전혀 없습니다 — .feature 도, lib/test_steps/ 도, test/src/bdd/ 도, build.yaml 의 builder 블록도 존재하지 않습니다. 전부 앱별 app/{app}/integration_test/ 한 곳에 모입니다:

app/{app}/integration_test/
├── features/                          # .feature 파일 (빌드 대상 아님, 문서)
│   ├── {feature}_list.feature         # 파일은  " 도달 상태(화면) "   단위로 나눈다
│   ├── {feature}_detail.feature
│   └── {feature}_form.feature
├── helpers/screen_batch.dart          # runScreenBatch(...) 배치 러너 (손으로 작성)
├── step/                              # TestDriver 기반 step 함수 (손으로 작성)
│   ├── {feature}_steps.dart
│   └── the_{screen}_screen_is_reset.dart   # 배치용 화면 리셋 (배치 1개당 1)
└── scenarios/                         # Patrol 테스트 (손으로 작성, 유일한 실행 경로)
    ├── {feature}_{screen}_batch_test.dart  # 기본: 같은 Background 시나리오 3+ 를 한 세션에
    └── {feature}_{scenario}_test.dart      # @isolated 또는 2건 이하일 때만

.feature 를 화면 단위로 나누는 이유 — Patrol E2E 비용의 대부분은 도달(앱 기동 → 로그인 → 그 화면까지 이동)이다. 같은 Background 를 공유하는 Scenario 를 한 파일에 모아 두면, 대응하는 Patrol 테스트를 배치 파일 하나로 만들어 도달을 화면당 1회만 지불할 수 있다. 서로 다른 Background 가 필요한 시나리오를 한 .feature 에 섞으면 그 파일은 배치가 불가능해진다. 판정 기준·리셋 계약·실패 의미론의 정본은 plugins/cc-flutter/skills/patrol-bdd-conventions/SKILL.md → "화면 단위 배치".

BDD 레이아웃 SoT — 경로는 한 곳에서만 온다#

위 트리는 비규범(파생 뷰) 이다. BDD 레이아웃의 정본은 plugins/cc-flutter/skills/bdd-testing/SKILL.md → "디렉토리 구조" 하나뿐이다. feature 패키지 하위의 test/features/ · lib/test_steps/ · test/steps/(build_runner 자동 생성)와 build.yaml 의 stepFolderName 설정은 과거 bdd_widget_test/bdd_test_gen/co_test_gen (⛔ 전부 폐지된 코드생성 빌더)로 구성했던 호스트에 남아 있을 수 있는 레거시 잔재다 — 정본과 다르므로 여기 적힌 문자열을 인벤토리 검색에 그대로 쓰면 안 된다.

레이아웃은 더 이상 호스트마다 설정 가능하지 않다 — build.yaml/bdd_options.yaml 로 위치를 바꾸는 경로 자체가 없어졌다. step 인벤토리 위치는 고정값 하나다:

$STEP_DIR = app/{app}/integration_test/step

{app} 은 대상 앱 디렉토리명이다(예: app/kobic). 여러 앱이 있으면 앱마다 각자의 integration_test/step/ 을 갖는다 — feature 패키지나 build.yaml 을 거쳐 해석할 필요가 없다.


공유 Step Import (build.yaml 없음)#

build.yaml 빌더 설정은 없습니다. bdd_widget_test/bdd_test_gen/co_test_gen (|dual_test_gen) 어떤 빌더도 이 파이프라인에 존재하지 않습니다 — .feature 는 문서이고, scenarios/*_test.dart 는 그 문서를 사람이 그대로 옮겨 적은 실행 파일입니다.

공유 step은 package/test_driver/lib/shared_steps.dart 의 barrel export를 시나리오 파일 상단에서 직접 import해서 씁니다:

// app/{app}/integration_test/scenarios/{feature}_{scenario}_test.dart
import 'package:test_driver/shared_steps.dart';
import 'package:test_driver/test_driver.dart';

shared_steps.dart 의 export 목록에 있는 이름(iTapTheWidget / iEnterInTheWidget / theWidgetShouldBeDisplayed 등, 전체 목록은 bdd-canonical-steps 참조)은 이렇게 바로 호출할 수 있습니다. Feature 전용 도메인 step은 같은 앱의 integration_test/step/{feature}_steps.dart 에 손으로 작성해 상대 경로로 import합니다.


Step Definition Writing Rules#

Core Rules#

  1. Function name: English camelCase (e.g., iSeeClassListScreen)
  2. Usage comment: English Step Pattern + Korean purpose description
  3. Parameters: Korean UI text is passed in {'Korean Value'} format
  4. First parameter is TestDriver (⛔ not WidgetTester — BDD steps target hand-written Patrol E2E only, so a WidgetTester-first-param step is either non-BDD widget-test code or a leftover from the retired bdd_widget_test scaffold)
  5. Location: app/{app}/integration_test/step/{feature}_steps.dart — hand-written, never generated

Step Definition Template#

// app/{app}/integration_test/step/{feature}_steps.dart
import 'package:test_driver/test_driver.dart';

// =============================================================================
// Given Steps — compound domain step (Mock/DI setup not covered by a canonical shared step)
// =============================================================================

/// Usage: And user is logged in as {email}
/// 용도: {email} 계정으로 로그인된 상태를 설정
Future<void> userIsLoggedInAs(TestDriver driver, String email) async {
  // 테스트용 로그인 상태 주입
}

// =============================================================================
// Then Steps — domain-specific screen assertion
// =============================================================================

/// Usage: And I see class list screen
/// 용도: 수업 목록 화면이 표시되는지 확인
Future<void> iSeeClassListScreen(TestDriver driver) async {
  await driver.expectVisible(K.classListView);
}

탭·표시 확인처럼 단일 액션 + 단일 Key 로 끝나는 step은 이렇게 로컬로 새로 만들지 말고 package:test_driver/shared_steps.dart 의 canonical shared step(iTapTheWidget, iTapTheText, theTextShouldBeDisplayed 등)을 재사용하세요 — 로컬 step 파일은 위 예시처럼 Mock 설정이나 복합 동작에만 씁니다 (기준: bdd-canonical-steps).


Feature File Writing Guide#

Core Rules#

  1. Feature/Scenario title: Korean
  2. Step Pattern: Write in English (the Pattern the Package matches), and must attach Korean comments.
  3. Parameters: {'Korean Value'} Format
  4. Multi-field forms: put field values in a Given data table, submit with a single high-level When (domain step) — do not stack one When/And per field. See "Examples: Data Table 폼 제출 패턴" below.

Examples: List Screen#

Feature: 수업 등록
  학생이 원하는 수업을 검색하고 등록할 수 있는 기능을 테스트한다.
  등록 완료 후 마이페이지에서 등록된 수업을 확인할 수 있어야 한다.

  Background:
    # 앱 실행 및 로그인 상태 설정
    Given the app is running # 앱이 실행됨
    And user is logged in as { ' student@school.com ' } # 주어진 이메일로 로그인 상태임

  @smoke
  Scenario: 수업 목록 조회
    사용자가 수업 탭을 선택하면 현재 등록 가능한 수업 목록이 표시된다.

    # 수업 탭으로 이동
    When I tap { ' 수업 ' } text #  ' 수업 '   텍스트를 탭함
    # 수업 목록 화면 확인
    Then I see class list screen # 수업 목록 화면이 표시되는지 확인
    And I see at least {3} class items # 최소 3개의 수업 항목이 표시되는지 확인

  @enrollment @payment
  Scenario: 유료 수업 등록
    사용자가 유료 수업을 선택하고 결제를 완료하면 등록이 완료된다.

    # 수업 선택
    Given I am on class list screen # 수업 목록 화면에 있음
    When I tap { ' 고급 수학 ' } text #  ' 고급 수학 '   텍스트를 탭함
    # 등록 버튼 클릭
    And I tap enroll button # 등록 버튼을 탭함
    # 결제 진행
    And I complete payment with { ' 카드 ' } #  ' 카드 '   결제 진행
    # 등록 완료 확인
    Then I see { ' 등록이 완료되었습니다 ' } text #  ' 등록이 완료되었습니다 '   텍스트가 표시됨
    And class { ' 고급 수학 ' } appears in my enrolled list # 내 등록 내역에서  ' 고급 수학 ' 이 나타남

Examples: Form Screen#

Feature: 숙제 제출
  학생이 숙제를 확인하고 제출할 수 있는 기능을 테스트한다.

  Background:
    Given the app is running # 앱이 실행됨
    And user is logged in as { ' student@school.com ' } # 주어진 이메일로 로그인 상태임

  @homework @submit
  Scenario: 숙제 파일 제출
    학생이 숙제에 파일을 첨부하고 제출하면 제출 완료 상태가 된다.

    Given I am on class list screen # 수업 목록 화면에 있음
    When I tap { ' 숙제 ' } text #  ' 숙제 '   텍스트를 탭함
    And I tap first homework item # 첫 번째 숙제 항목을 탭함
    And I attach file { ' answer.pdf ' } #  ' answer.pdf '   파일을 첨부함
    And I tap submit button # 제출 버튼을 탭함
    Then I see { ' 제출 완료 ' } text #  ' 제출 완료 '   텍스트가 표시됨
    And homework status is { ' 제출됨 ' } # 숙제 상태가  ' 제출됨 '

Examples: Data Table 폼 제출 패턴#

필드 3개 이상인 폼은 값을 Given 데이터 테이블로 두고, When은 하나의 도메인 step으로 제출합니다:

Feature: 커뮤니티 글 작성
  학생이 여러 항목을 입력해 커뮤니티 글을 작성하고 등록할 수 있는 기능을 테스트한다.

  Background:
    Given the app is running # 앱이 실행됨
    And user is logged in as { ' student@school.com ' } # 주어진 이메일로 로그인 상태임

  @community @create
  Scenario: 여러 항목을 입력해 글 작성
    학생이 제목·내용·카테고리를 입력하고 등록하면 글 작성이 완료된다.

    Given user wants to write a post with the following details: # 다음 정보로 글을 작성하려 함
      | 제목   | 오늘의 학습 후기        |
      | 내용   | 수업이 유익했습니다      |
      | 카테고리 | 자유게시판             |
    When I submit the post # 글을 등록함
    Then I see { ' 등록되었습니다 ' } text #  ' 등록되었습니다 '   텍스트가 표시됨

Using Predefined Steps#

bdd_widget_test 패키지가 기본 제공하던 find.text/Icons.check 기반 스텝들은 더 이상 쓰이지 않습니다 — BDD 시나리오는 위젯 테스트를 생성하지 않으므로 그 패키지 자체가 BDD 워크플로우에서 폐지됐습니다. 대신 TestDriver 기반 canonical 공유 step(iTapTheWidget, theWidgetShouldBeDisplayed, iEnterInTheWidget 등)을 재사용하세요. 표준 목록은 plugins/cc-flutter/skills/bdd-canonical-steps/SKILL.md 참조.


여러 앱 간 공유 (bdd_options.yaml 없음)#

bdd_options.yaml 기반 externalSteps 설정은 폐지되었습니다 — 코드 생성기가 없으므로 등록할 대상 자체가 없습니다. 여러 앱이 같은 공유 step을 쓰려면 각 앱의 integration_test/scenarios/*_test.dart 에서 동일하게 직접 import합니다:

import 'package:test_driver/shared_steps.dart';

Setup — hooks.dart 없음#

hooks.dart(WidgetTester 기반 setUp/tearDown) 패턴은 옛 위젯 테스트 코드생성 파이프라인의 산물이라 더 이상 없습니다. Patrol 시나리오의 공통 setup은 각 시나리오 파일 앞부분에서 theAppIsInitialized($) / ensureLoggedIn($) 같은 setup step을 호출하는 방식으로 대신합니다(구현은 app/{app}/integration_test/helpers/app_initializer.dart 등에 손으로 작성). 상세 패턴은 patrol-bdd-conventions 참조.


Domain Step Library Advantages#

AdvantageDescription
ReusabilityShare same steps across multiple feature files
MaintenanceChanges to step logic in one place reflect everywhere
ConsistencyUnified domain terminology and test patterns
Multi-appShare across apps via direct import 'package:test_driver/shared_steps.dart' (no build config)
Auto-completeIDE provides hints based on Usage comments

MCP Integration#

TaskMCP ServerPurpose
Scenario structuringSequentialSystematic scenario design
Pattern referenceContext7TestDriver / Patrol BDD pattern docs
Code generationSerenaStep Definition Generation

Examples#

Basic Usage#

/cc-flutter:bdd:generate community

Copy from .claude/docs + Run#

/cc-flutter:bdd:generate community \
  --from-claude-docs true \
  --run-build true

Write Step Definitions Only#

/cc-flutter:bdd:generate community \
  --only-steps true

Generate Specific Screens Only#

/cc-flutter:bdd:generate community \
  --screens  " list,detail "

Test Execution#

dart run build_runner build(BDD용)와 melos run test:bdd/test:bdd:select/test:bdd:full 은 모두 폐지된 코드생성 파이프라인 명령입니다 — BDD(Gherkin) 시나리오에는 빌드 단계가 없습니다.

cd app/{app}
# Patrol E2E 실행 (BDD 시나리오의 유일한 실행 경로)
# 배치 파일 1= 그 화면의 시나리오 전부 (도달 1)
patrol test --target integration_test/scenarios/{feature}_{screen}_batch_test.dart
# @isolated 시나리오는 파일 1= 시나리오 1개
patrol test --target integration_test/scenarios/{feature}_{scenario}_test.dart

# 전수 순차 실행
./integration_test/run_all_scenarios.sh

비-BDD 일반 위젯/유닛 테스트는 그대로 melos run test / flutter test 로 실행합니다(변경 없음).


Core Rules Summary#

  1. Step patterns in English: Given/When/Then steps in .feature files must be in English
  2. Function names in English camelCase: iSeeClassListScreen, iTapEnrollButton
  3. Usage comment required: /// Usage: And I tap enroll button + /// Purpose: Tap the enroll button
  4. Korean parameters: Pass in {'Korean value'} format
  5. Reuse shared steps: Utilize package:test_driver/shared_steps.dart
  6. No build.yaml / externalSteps: shared steps are imported directly (import 'package:test_driver/shared_steps.dart';) — there is no code generator to configure
  7. Use tags: Classification tags like @smoke, @navigation
  8. Step 재사용 우선: 새 step 작성 전 반드시 기존 step 인벤토리 검색 (아래 참조). 글롭은 "BDD 레이아웃 SoT" 절차의 고정 경로(app/{app}/integration_test/step)를 쓰고, 인벤토리 0건은 통과가 아니라 unknown(STOP) 이며, 경로 비의존 2차 소스인 Canonical Step Dictionary를 반드시 함께 대조한 뒤 step별 재사용/신규 결정을 로그로 남긴다
  9. Multi-field forms → Data Table + 단일 When: 필드 3개 이상 폼은 Given 데이터 테이블로 값을 두고 When 하나(도메인 step)로 제출 — "Examples: Data Table 폼 제출 패턴" 참조
  10. 화면 단위 배치: .feature 는 도달 상태(화면) 단위로 나누고, 같은 Background 를 공유하는 Scenario 가 3건 이상이면 Patrol 쪽은 배치 파일 1개로 모은다. 배치 불가 시나리오(로그아웃·결제·@journey·콜드스타트)에는 @isolated 를 단다. 배치는 실행 세션을 합치는 것이지 Scenario 를 합치는 것이 아니다 — 1 Scenario = 1 행위 검증은 그대로다

Step 재사용 필수 절차 (Pre-Generation Check)#

.feature 파일 작성 전 반드시 실행:

# 0) $STEP_DIR 은 고정값이다 — build.yaml/bdd_options.yaml 해석 불필요(그런 설정 자체가 없다)
STEP_DIR= " app/{app}/integration_test/step " 

 # 1) 고정된 글롭으로 인벤토리 전수 스캔 — find 의 exit code 를 파이프로 가리지 않는다
find . -path  " */$STEP_DIR/*.dart "   >   /tmp/step_inventory.txt; FIND_RC=$?
INV_COUNT=$(wc -l  <   /tmp/step_inventory.txt | tr -d  '   ' )

# 1b) 0건일 때만 의미 있는 corroboration: $STEP_DIR 이 실재하는 디렉토리인가?
#     (실재를 확인하지 못한 0건은  " 없음 "   이 아니라 unknown 이다)
DIR_EXISTS=$(find . -type d -path  " */$STEP_DIR "   | wc -l | tr -d  '   ' )

# 2) 유사 step 검색 — 아래 tri-state 판정을 통과한 뒤에만 의미가 있다
grep -i  " {keyword} "   /tmp/step_inventory.txt | xargs -n1 basename | sort -u

범용 Key 기반 공유 Step (/cc-flutter:bdd:refactor-steps 참조):

Step 패턴공유 step 함수대체 대상
I tap the {'key'} widgetiTapTheWidgetI tap the X button 118개
I tap the {'text'} textiTapTheText텍스트 기반 탭
I enter {'val'} in the {'key'} widgetiEnterInTheWidgetI enter/select 67개
the {'key'} widget should be displayedtheWidgetShouldBeDisplayedthe X should be displayed 304개
the {'text'} text should be displayedtheTextShouldBeDisplayed텍스트 검증
I wait for {'N'} secondsiWaitForSeconds대기 step

{'key'} = Widget Key 문자열 (K 클래스 상수와 일치):

결과: 기존 step과 매칭되면 새 step 파일 생성 스킵, 기존 파일 재사용.

Step 인벤토리 tri-state — 0건은 통과가 아니다#

인벤토리 판정은 pass / fail / undetermined 세 값이고 undetermined 의 기본값은 fail 이다 (plugins/cc-dev/rules/orchestration-graph.md → "3. Gate Contract — verdict 는 tri-state 다"). 판정 함수는 plugins/cc-dev/rules/zenhub-conventions.md → "Child Enumeration Contract (fail-closed)" 의 openChildrenStatus() open | none | unknown 모양을 그대로 빌린다.

stepInventoryStatus()found | none | unknown

조건행동
foundFIND_RC == 0 && INV_COUNT > 0통과 — 검색 결과를 재사용 근거로 쓴다
noneFIND_RC == 0 && INV_COUNT == 0 && DIR_EXISTS > 0통과 — 그린필드로 인정하고 전 step 을 create 로 기록
unknown그 밖 전부 — FIND_RC != 0 · $STEP_DIR 해석 실패 · INV_COUNT == 0 && DIR_EXISTS == 0STOP — 도구/설정 실패다. .feature 도 step 도 생성하지 않는다
  • 0건은 「매칭되는 step 이 없다」의 증거가 절대 아니다. 잘못된 글롭도 똑같이 0건을 낸다. 그래서 none$STEP_DIR 디렉토리 실재를 먼저 확인한 뒤에만 주장할 수 있고, 확인하지 못하면 unknown 이다 — orchestration-graph.md → "3.2 이름 붙은 fail-open 5형" 의 empty-set pass.
  • find/grep 를 파이프 뒤에 두고 그 exit code 로 판정하지 않는다 — 같은 표의 pipe-masked exit code. 위 스니펫이 FIND_RC 를 따로 잡아 두는 이유다.
  • unknown 을 "경고 후 계속" 으로 완화하지 않는다. undet:warn 은 위 SoT §3.1 세 경우에만 허용되고 이 게이트는 어디에도 해당하지 않는다 — 막을 대상(중복 폭발)이 그대로 남아 있다.

Canonical Step Dictionary 대조 — 경로 비의존 2차 소스#

글롭 검색은 경로에 의존하므로 단독으로는 "없음" 의 근거가 못 된다. step 을 새로 만들기 전에 두 소스를 모두 대조한다:

#소스성질실패 시
1$STEP_DIR 인벤토리 (위 tri-state)경로 의존unknown → STOP
2plugins/cc-flutter/skills/bdd-canonical-steps/SKILL.md → "공유 Step (test_driver 패키지)" · "금지 변형"경로 비의존 — 파일 배치와 무관사전을 읽지 못하면 STOP
  • 사전의 금지 변형에 걸리는 표현은 매칭 실패가 아니다. 표준 표현으로 정규화한 뒤 다시 대조한다. 정규화 전 텍스트로 0건이 나온 것을 "없음" 으로 읽는 것이 중복 폭발의 실제 경로다.
  • 2번이 create 를 막으면 1번이 0건이어도 새 step 파일을 만들지 않는다. 두 소스가 엇갈리면 사전이 이긴다(경로 비의존이므로).
  • 위 "범용 Key 기반 공유 Step" 표는 그 사전의 발췌(비규범) 다. 어긋나면 사전이 정본이다.

재사용/신규 결정 로그 — 재사용률은 산출해서 남긴다#

step 하나당 한 줄을 .claude/docs/{feature}/bdd/reuse-decisions.md내구 기록한다. 콘솔 출력은 내구 기록이 아니다.

step사전 표준으로 정규화된 step 텍스트
decisionreuse | create
evidencereuse → 재사용한 파일 경로 / create → 두 소스 모두 미매칭임을 보인 근거
sourceinventory | dictionary | both
undecided잔여 미판정 step 수

종료 시 마지막 줄에 reuse ratio = reuse / (reuse + create)분자·분모와 함께 출력한다 (비율만 적으면 검증이 불가능하다). create 가 하나라도 있으면 그 사유 전량을 같은 파일에 남긴다.

흐름 선언 (규범) — Pre-Generation Check#

표기법·게이트 계약·루프 계약은 plugins/cc-dev/rules/orchestration-graph.md 를 따른다. 아래 블록이 이 절차의 유일한 규범 선언이고, 위 산문·표는 모두 그 파생 뷰다(어긋나면 블록이 이긴다). 기질은 inline / sequential — 이 절차는 병렬화하지 않는다.

BG0    ACT   $STEP_DIR 고정 (app/{app}/integration_test/step, 해석 불필요)  writes:none
BG1    GATE  stepInventoryStatus() !==  " unknown "     verdict:stepInventoryStatus()  undet:fail  fail:HALT
BG2    ACT   Canonical Step Dictionary 로드         extern:true
BG3    LOOP  step 정규화 + 재사용/신규 판정          contract:L-bdd.generate-step-reuse-precheck  tier:standard
BG4    GATE  결정 로그 전수 기록(내구)              verdict:decisionLog.rows === normalizedSteps.count  undet:fail  fail:BG3
BG5    ACT   .feature + Step Definition 생성        writes:{featureDir}/**,$STEP_DIR/**  own:lead

BG0 -- >   BG1 -- >   BG3 -- >   BG4 -- >   BG5
BG2 -- >   BG3                        # hard prereq — 사전 없이 판정하지 않는다(경로 비의존 2차 소스)
BG4 == >   BG3   on:decisionLog.incomplete  bound:1  invalidates:BG3

BG1fail:HALT 는 종단 sink 다 — 0건을 SKIP 으로 흘려보내는 엣지는 없다.

L-bdd.generate-step-reuse-precheck — 값은 이 자리에 있다(SoT 는 계약만 갖는다):

inv:      매 판정의 진입·종료 양쪽에서 $STEP_DIR 은 정본 고정값(`app/{app}/integration_test/step`)
          하나뿐(재하드코딩 금지)이고 stepInventoryStatus(){found, none} 이며 사전이 로드된 상태다
prog:     undecided = normalizedSteps − decisionLog.rows, 매 iteration 강한 감소
          no-prog: 같은 step 을 같은 키워드로 재검색 금지 → Rung 2 (/cc-dev:unstuck)
term:     undecided == 0 (모든 step 이 reuse | create 중 하나로 확정)
budget:   step 당 조회 2(인벤토리 1 + 사전 1) / 시나리오 파일당 재진입 1회
exhaust:  미판정 step 을 create 로 기본 처리하지 않는다 —
          BLOCKED( ' step_reuse_undecided ' ) 로 멈추고 미판정 목록을 출력 (경고 후 계속 금지)
resume:   $STEP_DIR 재스캔 + .claude/docs/{feature}/bdd/reuse-decisions.md 를 읽어 위치 판정.
          같은 step 의 reuse 기록 재기입은 멱등
log:      step 당 한 줄  " step | reuse: < 파일 >   | create: < 사유 >   | undecided= < 잔여 > "   ·
          종료 시 reuse ratio 를 분자·분모와 함께, create 사유 전량과 함께 내구 기록

Reference Agents#

  • Detailed implementation rules: ${CLAUDE_PLUGIN_ROOT}/agents/bdd-scenario-agent.md
  • Step 재사용 분석: ${CLAUDE_PLUGIN_ROOT}/agents/bdd-step-reuse-agent.md
  • Step 리팩토링: /cc-flutter:bdd:refactor-steps
  • 레이아웃 정본: plugins/cc-flutter/skills/bdd-testing/SKILL.md → "디렉토리 구조"
  • Step 표준 표현 정본(경로 비의존): plugins/cc-flutter/skills/bdd-canonical-steps/SKILL.md

bdd-step-reuse-agent.md, refactor-steps.md 도 동일한 고정 경로 (app/{app}/integration_test/step)를 쓴다 — 인벤토리를 실행할 때 $STEP_DIR 을 문서마다 다시 해석할 필요는 없다. feature 패키지 경로(*/test/src/bdd/step/*.dart)가 어딘가에 남아 있다면 그건 레거시 잔재이지 정본이 아니다.