@walwal-harness/cli 2.0.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/assets/templates/config.json +41 -1
  2. package/bin/init.js +93 -7
  3. package/package.json +5 -2
  4. package/scripts/harness-next.sh +36 -1
  5. package/scripts/harness-user-prompt-submit.sh +106 -0
  6. package/scripts/lib/harness-render-progress.sh +15 -1
  7. package/scripts/scan-project.sh +24 -2
  8. package/skills/brainstorming/SKILL.md +200 -0
  9. package/skills/brainstorming/references/attribution.md +109 -0
  10. package/skills/brainstorming/references/spec-document-reviewer-prompt.md +49 -0
  11. package/skills/brainstorming/references/visual-companion.md +287 -0
  12. package/skills/brainstorming/scripts/frame-template.html +214 -0
  13. package/skills/brainstorming/scripts/helper.js +88 -0
  14. package/skills/brainstorming/scripts/server.cjs +354 -0
  15. package/skills/brainstorming/scripts/start-server.sh +148 -0
  16. package/skills/brainstorming/scripts/stop-server.sh +56 -0
  17. package/skills/dispatcher/SKILL.md +114 -2
  18. package/skills/dispatcher/references/pipeline-definitions.md +53 -5
  19. package/skills/evaluator-functional-flutter/SKILL.md +198 -0
  20. package/skills/evaluator-functional-flutter/references/ia-compliance.md +77 -0
  21. package/skills/evaluator-functional-flutter/references/scoring-rubric.md +132 -0
  22. package/skills/evaluator-functional-flutter/references/static-check-rules.md +99 -0
  23. package/skills/generator-frontend-flutter/SKILL.md +138 -0
  24. package/skills/generator-frontend-flutter/references/anti-patterns.md +288 -0
  25. package/skills/generator-frontend-flutter/references/api-layer-pattern.md +233 -0
  26. package/skills/generator-frontend-flutter/references/i18n-pattern.md +102 -0
  27. package/skills/generator-frontend-flutter/references/riverpod-pattern.md +199 -0
  28. package/skills/planner/SKILL.md +23 -1
  29. package/skills/planner/references/fe-stack-detection.md +131 -0
@@ -0,0 +1,233 @@
1
+ ---
2
+ docmeta:
3
+ id: api-layer-pattern
4
+ title: API Layer Pattern (integrated_data_layer)
5
+ type: output
6
+ createdAt: 2026-04-09T00:00:00Z
7
+ updatedAt: 2026-04-09T00:00:00Z
8
+ source:
9
+ producer: agent
10
+ skillId: harness-generator-frontend-flutter
11
+ inputs:
12
+ - documentId: clue-fe-flutter-api-layer
13
+ uri: ../../../../../moon_web/clue-fe-flutter.skill
14
+ relation: output-from
15
+ sections:
16
+ - sourceRange:
17
+ startLine: 1
18
+ endLine: 187
19
+ targetRange:
20
+ startLine: 32
21
+ endLine: 205
22
+ tags:
23
+ - flutter
24
+ - retrofit
25
+ - json-serializable
26
+ - integrated-data-layer
27
+ ---
28
+
29
+ # API Layer Pattern (integrated_data_layer)
30
+
31
+ 하네스 Flutter Generator는 `api-contract.json` 을 **Source of Truth** 로 삼아
32
+ `integrated_data_layer` 하위에 Dart 타입을 1:1 생성한다.
33
+
34
+ ## 디렉토리 구조
35
+
36
+ ```
37
+ integrated_data_layer/
38
+ ├── lib/
39
+ │ ├── 1_repositories/ # Repository (비즈니스 로직 래퍼)
40
+ │ │ ├── ac_repository.dart
41
+ │ │ ├── visitor_repository.dart
42
+ │ │ ├── s3_repository.dart
43
+ │ │ ├── oauth_repository.dart
44
+ │ │ └── reservation_repository.dart
45
+ │ ├── 2_data_sources/
46
+ │ │ ├── remote/
47
+ │ │ │ ├── rest_api.dart # 모든 Retrofit 엔드포인트
48
+ │ │ │ ├── rest_provider.dart # Dio/RestClient Provider
49
+ │ │ │ ├── request/body/ # Request Body 클래스
50
+ │ │ │ └── response/
51
+ │ │ │ └── abstract/ # 베이스 응답 (ClueResponseImpl 등)
52
+ │ │ └── local/
53
+ │ └── 3_others/
54
+ │ ├── enum/
55
+ │ └── extension/
56
+ ├── test/ # lib/ 미러 구조
57
+ │ ├── 1_repositories/
58
+ │ ├── 2_data_sources/remote/request/body/
59
+ │ └── 2_data_sources/remote/response/
60
+ └── data_layer.dart # 진입점 (dataLayer Provider)
61
+ ```
62
+
63
+ ## 1. api-contract.json → Request Body 변환
64
+
65
+ api-contract.json의 요청 스키마를 `@JsonSerializable(includeIfNull: false)`
66
+ 클래스로 1:1 매핑한다. **필수 지시가 없으면 모든 필드 Nullable.**
67
+
68
+ ```dart
69
+ // lib/2_data_sources/remote/request/body/example_body.dart
70
+ import 'package:json_annotation/json_annotation.dart';
71
+
72
+ part 'example_body.g.dart';
73
+
74
+ @JsonSerializable(includeIfNull: false) // ★ 필수
75
+ class ExampleBody {
76
+ final String? name; // ★ Nullable 기본
77
+ final int? placeId;
78
+ final String? description;
79
+
80
+ const ExampleBody({
81
+ this.name,
82
+ this.placeId,
83
+ this.description,
84
+ });
85
+
86
+ factory ExampleBody.fromJson(Map<String, dynamic> json) =>
87
+ _$ExampleBodyFromJson(json);
88
+ Map<String, dynamic> toJson() => _$ExampleBodyToJson(this);
89
+ }
90
+ ```
91
+
92
+ ## 2. Response 변환
93
+
94
+ 응답 베이스 클래스 계층:
95
+ - `ClueResponseImpl<T>` — 단일 데이터 (code, data, errors)
96
+ - `ClueResponseListImpl<T>` — 목록 (code, data, errors, totalCount)
97
+ - `VisitorResponseImpl<T>` — Visitor 서버 전용
98
+
99
+ ```dart
100
+ // lib/2_data_sources/remote/response/example_response.dart
101
+ import 'package:integrated_data_layer/2_data_sources/remote/response/abstract/clue/clue_response_impl.dart';
102
+ import 'package:json_annotation/json_annotation.dart';
103
+
104
+ part 'example_response.g.dart';
105
+
106
+ @JsonSerializable(explicitToJson: true)
107
+ class ExampleResponse extends ClueResponseImpl<ExampleResponseData> {
108
+ const ExampleResponse({
109
+ required super.code,
110
+ required super.data,
111
+ required super.errors,
112
+ });
113
+
114
+ factory ExampleResponse.fromJson(Map<String, dynamic> json) =>
115
+ _$ExampleResponseFromJson(json);
116
+ Map<String, dynamic> toJson() => _$ExampleResponseToJson(this);
117
+ }
118
+
119
+ @JsonSerializable(explicitToJson: true)
120
+ class ExampleResponseData {
121
+ final int? id; // ★ Nullable 기본
122
+ final String? name;
123
+
124
+ const ExampleResponseData({this.id, this.name});
125
+
126
+ factory ExampleResponseData.fromJson(Map<String, dynamic> json) =>
127
+ _$ExampleResponseDataFromJson(json);
128
+ Map<String, dynamic> toJson() => _$ExampleResponseDataToJson(this);
129
+ }
130
+ ```
131
+
132
+ **기존 응답 재사용 원칙**: api-contract.json 의 서로 다른 엔드포인트가
133
+ 동일한 응답 구조를 가지면 새 Response 클래스를 만들지 않는다.
134
+
135
+ ## 3. rest_api.dart 엔드포인트 추가
136
+
137
+ ```dart
138
+ // rest_api.dart 내부 — RestClient 클래스에 추가
139
+ @GET("/examples/{exampleId}")
140
+ Future<ExampleResponse> getExample(
141
+ @Path("exampleId") int exampleId,
142
+ );
143
+
144
+ @POST("/examples")
145
+ Future<ExampleResponse> createExample(
146
+ @Body() ExampleBody requestBody,
147
+ );
148
+
149
+ @PUT("/examples/{exampleId}")
150
+ Future<ExampleResponse> updateExample(
151
+ @Path("exampleId") int exampleId,
152
+ @Body() ExampleBody requestBody,
153
+ );
154
+
155
+ @DELETE("/examples/{exampleId}")
156
+ Future<ClueDefaultResponse> deleteExample(
157
+ @Path("exampleId") int exampleId,
158
+ );
159
+ ```
160
+
161
+ api-contract.json의 path, method, param 위치를 **그대로** 옮긴다.
162
+ 경로 변수 추가/제거 금지 — 계약이 Source of Truth.
163
+
164
+ ## 4. Repository 래퍼 추가
165
+
166
+ ```dart
167
+ // 1_repositories/ac_repository.dart 내부에 메서드 추가
168
+ Future<ExampleResponse> getExample({required int exampleId}) {
169
+ return _restClient.getExample(exampleId);
170
+ }
171
+ ```
172
+
173
+ Repository는 **순수 래퍼** — 비즈니스 로직 없이 Retrofit 호출을 전달만 한다.
174
+ 도메인 분기, 에러 매핑은 VM 계층에서 수행.
175
+
176
+ ## 5. 호출 패턴 (VM에서)
177
+
178
+ ```dart
179
+ // VM 내부
180
+ final result = await ref.read(dataLayer).ac.getExample(exampleId: 123);
181
+ if (result.code == 200) {
182
+ // 성공 처리
183
+ }
184
+ ```
185
+
186
+ ## 6. TC 작성 (필수)
187
+
188
+ **모든 Request Body, Response에 왕복 테스트 필수.**
189
+ Evaluator가 test 디렉토리를 점검한다.
190
+
191
+ ```dart
192
+ // test/2_data_sources/remote/request/body/example_body_test.dart
193
+ import 'dart:convert';
194
+ import 'package:flutter_test/flutter_test.dart';
195
+ import 'package:integrated_data_layer/2_data_sources/remote/request/body/example_body.dart';
196
+
197
+ void main() {
198
+ group("example body test", () {
199
+ test("/fromJson & toJson", () {
200
+ ExampleBody body1 = const ExampleBody(
201
+ name: 'test',
202
+ placeId: 1,
203
+ );
204
+
205
+ ExampleBody body2 = ExampleBody.fromJson(body1.toJson());
206
+
207
+ expect(body1.name, 'test');
208
+ expect(body1.placeId, 1);
209
+
210
+ var body1Data = jsonEncode(body1.toJson());
211
+ var body2Data = jsonEncode(body2.toJson());
212
+ expect(body1Data == body2Data, true);
213
+ });
214
+
215
+ test("/null fields excluded", () {
216
+ ExampleBody body = const ExampleBody(name: 'test');
217
+ var json = body.toJson();
218
+ expect(json.containsKey('placeId'), false); // includeIfNull: false 검증
219
+ });
220
+ });
221
+ }
222
+ ```
223
+
224
+ ## 7. 코드 생성
225
+
226
+ 모든 Request/Response 파일 추가/수정 후 **반드시** 실행:
227
+
228
+ ```bash
229
+ cd <integrated_data_layer 경로>
230
+ flutter pub run build_runner build --delete-conflicting-outputs
231
+ ```
232
+
233
+ 생성된 `*.g.dart` 파일은 커밋한다 (CI에서 재생성하지 않음).
@@ -0,0 +1,102 @@
1
+ ---
2
+ docmeta:
3
+ id: i18n-pattern
4
+ title: 다국어 (i18n) ARB 패턴
5
+ type: output
6
+ createdAt: 2026-04-09T00:00:00Z
7
+ updatedAt: 2026-04-09T00:00:00Z
8
+ source:
9
+ producer: agent
10
+ skillId: harness-generator-frontend-flutter
11
+ inputs:
12
+ - documentId: clue-fe-flutter-i18n
13
+ uri: ../../../../../moon_web/clue-fe-flutter.skill
14
+ relation: output-from
15
+ sections:
16
+ - sourceRange:
17
+ startLine: 1
18
+ endLine: 60
19
+ targetRange:
20
+ startLine: 32
21
+ endLine: 110
22
+ tags:
23
+ - flutter
24
+ - i18n
25
+ - arb
26
+ - l10n
27
+ ---
28
+
29
+ # 다국어 (i18n) ARB 패턴
30
+
31
+ ## ARB 파일 기반 l10n
32
+
33
+ ### 파일 경로
34
+
35
+ - 영어: `lib/l10n/app_en.arb`
36
+ - 한국어: `lib/l10n/app_ko.arb`
37
+ - 일본어: `lib/l10n/app_ja.arb`
38
+
39
+ 프로젝트에 따라 `assets/strings/en.json` 등 다른 경로를 쓸 수 있다.
40
+ `AGENTS.md` / sprint-contract.md 의 기존 컨벤션을 우선한다.
41
+
42
+ ### 작업 순서
43
+
44
+ 1. 문자열 언어 판별 (예: "취소" → ko, "Cancel" → en)
45
+ 2. 해당 언어 arb 파일에 **키 존재 여부 확인** (중복 방지)
46
+ 3. 없으면 모든 언어 파일에 동시 추가 (`app_en.arb`, `app_ko.arb`, `app_ja.arb`)
47
+ 4. 코드 치환 → `LocaleAssist().of.키이름`
48
+
49
+ ### 사용법
50
+
51
+ ```dart
52
+ // 기본 문자열
53
+ Text(
54
+ LocaleAssist().of.cancel,
55
+ style: MyTextStyle.size15.w500.xFF9CA3AF,
56
+ )
57
+
58
+ // 문자열 내 스타일 별도 구현 (ClueText 등 프로젝트 커스텀 위젯 사용)
59
+ ClueText(
60
+ "${LocaleAssist().of.all} (${count})",
61
+ style: MyTextStyle.size16.w500,
62
+ targetList: [
63
+ TargetModel(
64
+ text: "(${count})",
65
+ style: MyTextStyle.size16.w500.xFF6682FF,
66
+ ),
67
+ ],
68
+ )
69
+ ```
70
+
71
+ ### ARB 키 네이밍 규칙
72
+
73
+ ```json
74
+ {
75
+ "cancel": "취소",
76
+ "confirm": "확인",
77
+ "doorOpen": "문 열기",
78
+ "networkError": "네트워크 오류가 발생했습니다"
79
+ }
80
+ ```
81
+
82
+ - **camelCase** 사용
83
+ - 모든 arb 파일(en, ko, ja)에 **동일 키 동시 추가**
84
+ - 기존 키 검색 후 중복 방지 (`grep -rn '"cancel"' lib/l10n/`)
85
+ - 플레이스홀더 필요 시 ICU MessageFormat 사용
86
+
87
+ ## 필수 원칙
88
+
89
+ - 영문 전용 표기 지시가 없는 한 **모든 사용자 노출 문자열은 다국어 처리**
90
+ - 하드코딩된 문자열 사용 금지 (`Text('취소')` ✗)
91
+ - 새 페이지 추가 시 관련 문자열 **일괄 등록** — 스프린트 중 누락 방지
92
+ - 모든 arb 파일의 키 집합은 동일해야 한다 (Evaluator가 diff 검사)
93
+
94
+ ## Self-Check
95
+
96
+ ```bash
97
+ # 하드코딩된 한글 탐지
98
+ grep -rn "Text('[가-힣]" lib/ui/
99
+
100
+ # 누락 키 탐지 (en 기준)
101
+ python3 -c "import json; en=set(json.load(open('lib/l10n/app_en.arb')).keys()); ko=set(json.load(open('lib/l10n/app_ko.arb')).keys()); print('missing in ko:', en - ko); print('missing in en:', ko - en)"
102
+ ```
@@ -0,0 +1,199 @@
1
+ ---
2
+ docmeta:
3
+ id: riverpod-pattern
4
+ title: Riverpod 상태관리 패턴 (Page + VM)
5
+ type: output
6
+ createdAt: 2026-04-09T00:00:00Z
7
+ updatedAt: 2026-04-09T00:00:00Z
8
+ source:
9
+ producer: agent
10
+ skillId: harness-generator-frontend-flutter
11
+ inputs:
12
+ - documentId: clue-fe-flutter-riverpod
13
+ uri: ../../../../../moon_web/clue-fe-flutter.skill
14
+ relation: output-from
15
+ sections:
16
+ - sourceRange:
17
+ startLine: 1
18
+ endLine: 149
19
+ targetRange:
20
+ startLine: 32
21
+ endLine: 180
22
+ tags:
23
+ - flutter
24
+ - riverpod
25
+ - state-management
26
+ - notifier-provider
27
+ ---
28
+
29
+ # Riverpod 상태관리 패턴 (Page + VM)
30
+
31
+ ## 기본 구조: Page + VM 쌍
32
+
33
+ 모든 화면은 `xxx_page.dart` + `xxx_page_vm.dart` 쌍으로 구성.
34
+ 이는 테스트 가능성과 UI/로직 분리를 위한 강제 규약.
35
+
36
+ ### VM (ViewModel)
37
+
38
+ ```dart
39
+ // example_page_vm.dart
40
+ import 'package:equatable/equatable.dart';
41
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
42
+ import 'package:integrated_data_layer/data_layer.dart';
43
+
44
+ final pExampleProvider =
45
+ NotifierProvider<ExampleNotifier, ExampleState>(ExampleNotifier.new);
46
+
47
+ class ExampleNotifier extends Notifier<ExampleState> {
48
+ @override
49
+ ExampleState build() {
50
+ return const ExampleState();
51
+ }
52
+
53
+ Future<void> loadData() async {
54
+ state = state.copyWith(isLoading: true);
55
+ try {
56
+ final result = await ref.read(dataLayer).ac.getExample(exampleId: 1);
57
+ if (result.code == 200) {
58
+ state = state.copyWith(
59
+ isLoading: false,
60
+ data: result.data,
61
+ );
62
+ } else {
63
+ state = state.copyWith(
64
+ isLoading: false,
65
+ error: 'code=${result.code}',
66
+ );
67
+ }
68
+ } catch (e) {
69
+ state = state.copyWith(isLoading: false, error: e.toString());
70
+ }
71
+ }
72
+ }
73
+
74
+ class ExampleState extends Equatable {
75
+ final bool isLoading;
76
+ final dynamic data;
77
+ final String? error;
78
+
79
+ const ExampleState({
80
+ this.isLoading = false,
81
+ this.data,
82
+ this.error,
83
+ });
84
+
85
+ ExampleState copyWith({
86
+ bool? isLoading,
87
+ dynamic data,
88
+ String? error,
89
+ }) {
90
+ return ExampleState(
91
+ isLoading: isLoading ?? this.isLoading,
92
+ data: data ?? this.data,
93
+ error: error ?? this.error,
94
+ );
95
+ }
96
+
97
+ @override
98
+ List<Object?> get props => [isLoading, data, error];
99
+ }
100
+ ```
101
+
102
+ ### Page
103
+
104
+ ```dart
105
+ // example_page.dart
106
+ import 'package:flutter/material.dart';
107
+ import 'package:flutter_riverpod/flutter_riverpod.dart';
108
+ import 'example_page_vm.dart';
109
+
110
+ class ExamplePage extends ConsumerStatefulWidget {
111
+ const ExamplePage({super.key});
112
+
113
+ @override
114
+ ConsumerState<ExamplePage> createState() => _ExamplePageState();
115
+ }
116
+
117
+ class _ExamplePageState extends ConsumerState<ExamplePage> {
118
+ @override
119
+ void initState() {
120
+ super.initState();
121
+ // 초기 데이터 로드는 addPostFrameCallback 으로
122
+ WidgetsBinding.instance.addPostFrameCallback((_) {
123
+ ref.read(pExampleProvider.notifier).loadData();
124
+ });
125
+ }
126
+
127
+ @override
128
+ Widget build(BuildContext context) {
129
+ final state = ref.watch(pExampleProvider); // watch로 상태 구독
130
+
131
+ if (state.isLoading) {
132
+ return const Center(child: CircularProgressIndicator());
133
+ }
134
+ if (state.error != null) {
135
+ return Center(child: Text(state.error!));
136
+ }
137
+ // ... 정상 상태 렌더링
138
+ return const SizedBox.shrink();
139
+ }
140
+ }
141
+ ```
142
+
143
+ ## Family 패턴 (다중 자식 컴포넌트)
144
+
145
+ 리스트 아이템처럼 동일 타입 인스턴스가 여러 개 필요할 때:
146
+
147
+ ```dart
148
+ // relay_tile_vm.dart
149
+ final pRelayItemProvider = NotifierProvider.family<
150
+ RelayItemNotifier,
151
+ RelayItemState,
152
+ ({int ioId, String topic})>(
153
+ () => RelayItemNotifier(),
154
+ );
155
+
156
+ class RelayItemNotifier
157
+ extends FamilyNotifier<RelayItemState, ({int ioId, String topic})> {
158
+ @override
159
+ RelayItemState build(({int ioId, String topic}) arg) {
160
+ ref.onDispose(() {
161
+ // 정리 로직 (스트림 구독 해제 등)
162
+ });
163
+ return RelayItemState(ioId: arg.ioId, topic: arg.topic);
164
+ }
165
+
166
+ Future<void> doAction() async {
167
+ final result = await ref.read(dataLayer).ac.someMethod(id: state.ioId);
168
+ state = state.copyWith(/* ... */);
169
+ }
170
+ }
171
+ ```
172
+
173
+ 사용:
174
+ ```dart
175
+ // Page/Widget에서
176
+ final itemState = ref.watch(
177
+ pRelayItemProvider((ioId: item.ioId, topic: item.topic)),
178
+ );
179
+ ```
180
+
181
+ ## 핵심 규칙
182
+
183
+ | 규칙 | 설명 |
184
+ |------|------|
185
+ | `ref.watch` | UI rebuild이 필요한 곳 (build 메서드 내) |
186
+ | `ref.read` | 일회성 호출 (이벤트 핸들러, initState 콜백) |
187
+ | `dataLayer` 접근 | VM 내에서 `ref.read(dataLayer).repository.method()` |
188
+ | State 불변성 | Equatable 상속 + copyWith 패턴 |
189
+ | Provider 네이밍 | `p` 접두사 + PascalCase + Provider (예: `pHomePageProvider`) |
190
+ | 초기 로드 | `addPostFrameCallback` 으로 첫 프레임 이후 호출 |
191
+ | 에러/로딩 | State에 `isLoading`, `error` 필수 포함 (UI 3가지 상태 처리) |
192
+
193
+ ## 금지
194
+
195
+ - `bridges/` 사용 금지 — BLOC 기반 레거시
196
+ - StatefulWidget 내 직접 API 호출 — 반드시 VM 경유
197
+ - `setState` 로 API 응답 상태 관리 — Riverpod 상태로 대체
198
+ - `ref.read`를 build 메서드 내에서 사용 (→ `ref.watch`)
199
+ - VM 내 UI 의존 코드 (Navigator, ScaffoldMessenger 등) — UI 콜백으로 분리
@@ -26,7 +26,18 @@ disable-model-invocation: true
26
26
  1. `AGENTS.md` 읽기
27
27
  2. `.harness/gotchas/planner.md` 읽기 — **과거 실수 반복 금지**
28
28
  3. `.harness/progress.json` 읽기
29
- 4. `.harness/actions/pipeline.json` 읽기 — `planner_mode` 확인
29
+ 4. `.harness/actions/pipeline.json` 읽기 — `planner_mode`, `fe_stack` 확인
30
+ 5. `.harness/actions/scan-result.json` 읽기 — `tech_stack.fe_stack` 확인 (없으면 `react` 기본)
31
+ 6. **Brainstorm Spec 우선 로드** — `.harness/actions/brainstorm-spec.md` 가 존재하면
32
+ **이 파일이 PRD 대체 입력**. Brainstormer 가 이미 사용자와 대화하여 확정한
33
+ 결과이므로 **승인된 결정을 뒤엎지 않는다**. 없으면 사용자의 원본 요청 텍스트를 입력으로 사용.
34
+ - brainstorm-spec.md 에 `## Open Questions` 섹션이 있으면 Planner 가 해소 (API 계약으로 확정)
35
+ - brainstorm-spec.md 의 `## 7. 주요 컴포넌트 / 엔티티` → `feature-list.json` 초기 feature 목록 시드
36
+ - brainstorm-spec.md 의 `## 5. 선택된 접근법` / `## 6. 아키텍처 스케치` → MSA 서비스 분할 베이스
37
+ 7. **FE Stack 확정** → [FE Stack 결정 가이드](references/fe-stack-detection.md)
38
+ - `pubspec.yaml` + `flutter:` 키 → `fe_stack = "flutter"`
39
+ - 혼재/불명확 → 사용자에게 단 한 번 질문
40
+ - 확정 후 `pipeline.json.fe_stack` 갱신 (없으면 생성)
30
41
 
31
42
  ## Outputs (4개)
32
43
 
@@ -42,6 +53,17 @@ disable-model-invocation: true
42
53
  - **full**: MSA 서비스 분할 + 전체 설계 (FULLSTACK, BE-ONLY)
43
54
  - **light**: OpenAPI → api-contract.json 변환 + FE 설계만 (FE-ONLY)
44
55
 
56
+ ## fe_stack (FE 파이프라인 분기)
57
+
58
+ `pipeline.json.fe_stack`은 FE Generator/Evaluator 선택을 결정한다:
59
+
60
+ | 값 | FE Generator | FE Evaluator | 비고 |
61
+ |----|--------------|--------------|------|
62
+ | `react` (기본) | `generator-frontend` | `evaluator-functional` + `evaluator-visual` | Vercel/Next.js/Tailwind |
63
+ | `flutter` | `generator-frontend-flutter` | `evaluator-functional-flutter` | Riverpod + integrated_data_layer, Eval-Visual 생략 |
64
+
65
+ **Planner는 `pipeline.json`에 `fe_stack`을 반드시 기록해야 한다.** Dispatcher가 이 값으로 `next_agent`를 라우팅한다.
66
+
45
67
  ## Process
46
68
 
47
69
  1. 사양서 작성 → [plan 템플릿](references/plan-template.md)
@@ -0,0 +1,131 @@
1
+ ---
2
+ docmeta:
3
+ id: fe-stack-detection
4
+ title: FE Stack Detection — Planner Responsibility
5
+ type: output
6
+ createdAt: 2026-04-09T00:00:00Z
7
+ updatedAt: 2026-04-09T00:00:00Z
8
+ source:
9
+ producer: agent
10
+ skillId: harness-planner
11
+ inputs:
12
+ - documentId: clue-fe-flutter-skill
13
+ uri: ../../../../../moon_web/clue-fe-flutter.skill
14
+ relation: output-from
15
+ sections:
16
+ - sourceRange:
17
+ startLine: 1
18
+ endLine: 103
19
+ targetRange:
20
+ startLine: 79
21
+ endLine: 94
22
+ - documentId: harness-planner-skill
23
+ uri: ../SKILL.md
24
+ relation: output-from
25
+ sections:
26
+ - sourceRange:
27
+ startLine: 26
28
+ endLine: 56
29
+ targetRange:
30
+ startLine: 27
31
+ endLine: 78
32
+ tags:
33
+ - planner
34
+ - fe-stack
35
+ - flutter
36
+ - pipeline
37
+ ---
38
+
39
+ # FE Stack Detection — Planner Responsibility
40
+
41
+ Planner는 Sprint 1 착수 전 반드시 `fe_stack`을 확정하고 `pipeline.json`에 기록해야 한다.
42
+
43
+ ## 1. 감지 순서 (자동)
44
+
45
+ ```
46
+ 1. .harness/actions/scan-result.json 의 tech_stack.fe_stack 읽기
47
+ 2. 값이 "flutter" 또는 "react" 면 → 그대로 사용
48
+ 3. 값이 없거나 "unknown" 이면 → 아래 추가 감지
49
+ ```
50
+
51
+ ### 추가 감지 (fallback)
52
+
53
+ | 시그널 | 판정 |
54
+ |--------|------|
55
+ | 루트 `pubspec.yaml` + `flutter:` 키 존재 | `flutter` |
56
+ | 하위 경로(예: `mobile/`, `apps/mobile/`, `clue_mobile_app/`)에 pubspec.yaml | `flutter` |
57
+ | `package.json` 에 `react`, `next`, `vite` 등 | `react` |
58
+ | `lib/` + `.dart` 파일 다수 | `flutter` |
59
+ | 둘 다 존재 (Flutter + Web 혼재) | **사용자 질문 필수** |
60
+
61
+ ## 2. 사용자 질문 (불명확 시 단 한 번)
62
+
63
+ ```
64
+ 프론트엔드 스택을 확인합니다:
65
+
66
+ (A) React / Next.js / Vite 기반 Web
67
+ (B) Flutter (Dart, 모바일/데스크톱)
68
+
69
+ 현재 감지된 시그널:
70
+ - pubspec.yaml: [있음/없음]
71
+ - package.json: [있음/없음]
72
+ - Next.js config: [있음/없음]
73
+
74
+ 어떤 스택으로 진행하시겠습니까? (A/B)
75
+ ```
76
+
77
+ 혼재 프로젝트(Web + Flutter mobile)라면 사용자에게 **현 스프린트의 타깃**을 묻는다 — 파이프라인은 한 번에 하나의 `fe_stack`만 취급한다.
78
+
79
+ ## 3. pipeline.json 갱신
80
+
81
+ `fe_stack` 확정 후 `pipeline.json`에 반드시 추가:
82
+
83
+ ```json
84
+ {
85
+ "pipeline": "FULLSTACK",
86
+ "planner_mode": "full",
87
+ "fe_stack": "flutter",
88
+ "agents_active": [
89
+ "planner",
90
+ "generator-backend",
91
+ "generator-frontend-flutter",
92
+ "evaluator-functional-flutter"
93
+ ],
94
+ "agents_skipped": [
95
+ "generator-frontend",
96
+ "evaluator-functional",
97
+ "evaluator-visual"
98
+ ],
99
+ "evaluator_mode": "flutter-native",
100
+ "notes": "Flutter 앱 — Playwright 대신 flutter analyze/test + 정적 anti-pattern 검증"
101
+ }
102
+ ```
103
+
104
+ ### fe_stack → 파이프라인 매핑
105
+
106
+ | pipeline | fe_stack | agents_active 예시 |
107
+ |----------|----------|-------------------|
108
+ | FULLSTACK | react | planner, generator-backend, generator-frontend, evaluator-functional, evaluator-visual |
109
+ | FULLSTACK | flutter | planner, generator-backend, generator-frontend-flutter, evaluator-functional-flutter |
110
+ | FE-ONLY | react | planner, generator-frontend, evaluator-functional, evaluator-visual |
111
+ | FE-ONLY | flutter | planner, generator-frontend-flutter, evaluator-functional-flutter |
112
+ | BE-ONLY | (무관) | planner, generator-backend, evaluator-functional |
113
+
114
+ ## 4. Flutter 선택 시 추가 작업
115
+
116
+ Flutter로 확정되면 Planner는:
117
+
118
+ 1. **AGENTS.md IA-MAP** 의 `[FE]` 섹션을 Flutter 구조로 바꿔야 한다.
119
+ - `apps/web/` → `lib/ui/pages/`, `lib/ui/component/`
120
+ - `libs/shared-dto/` 대신 `integrated_data_layer/` 경로 등록
121
+ - 소유자: `→ Generator-Frontend-Flutter`
122
+
123
+ 2. **api-contract.json** 은 언어 중립적이어야 한다 — Planner는 TypeScript 타입이 아니라 **스키마 JSON** 으로만 작성. Flutter Generator가 Retrofit/JsonSerializable로 변환한다.
124
+
125
+ 3. **feature-list.json** 의 `layer: "frontend"` feature들은 `fe_stack: "flutter"` 태그를 달아서 Evaluator가 구분하도록 한다.
126
+
127
+ ## 5. 금지
128
+
129
+ - 파이프라인 실행 도중 `fe_stack` 변경 금지 — 스프린트 경계에서만 가능
130
+ - React/Flutter 코드 혼재 생성 금지 — Generator는 하나의 stack만 담당
131
+ - 사용자가 명시적으로 한 스택을 지시했는데 감지 결과로 다른 스택을 강제하지 말 것