@designfever/web-review-kit 0.1.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.
@@ -0,0 +1,336 @@
1
+ # df-sheet adapter implementation plan
2
+
3
+ ## Goal
4
+
5
+ `dfSheetAdapter` connects df-web-review-kit QA items to df-sheet issues.
6
+
7
+ The first production pilot is Lexus. Keep the adapter local and project-tested
8
+ before exposing it as a package export.
9
+
10
+ ## Current decision
11
+
12
+ - Use `issues.id` as the canonical QA item id.
13
+ - Add only one df-sheet field: `issues.review_metadata jsonb null`.
14
+ - Store human-readable QA content in `description`.
15
+ - Store restore data in `review_metadata`.
16
+ - Store screenshots through df-sheet `/api/upload`, then attach the returned URL
17
+ to `issues.attachments`.
18
+ - Use a project/page scoped integration token, not an open unauthenticated API.
19
+ - Publish the adapter only after the Lexus pilot validates create, list, update,
20
+ delete, upload, and deep-link restore.
21
+
22
+ ## Required df-sheet changes
23
+
24
+ ### Database
25
+
26
+ Add a nullable JSON field to `issues`.
27
+
28
+ ```sql
29
+ alter table issues
30
+ add column if not exists review_metadata jsonb null;
31
+ ```
32
+
33
+ ### API
34
+
35
+ Pass `review_metadata` through the existing issue APIs.
36
+
37
+ - `POST /api/issues`
38
+ - accept `body.review_metadata`
39
+ - insert it into `issues.review_metadata`
40
+ - `PATCH /api/issues/:id`
41
+ - accept `body.review_metadata`
42
+ - update `issues.review_metadata`
43
+ - `GET /api/issues`
44
+ - include `review_metadata` in list responses
45
+ - `GET /api/issues/:id`
46
+ - include `review_metadata` in detail responses
47
+
48
+ Do not hide adapter metadata in `description`. df-sheet uses Tiptap JSON there,
49
+ so a hidden JSON block would be fragile.
50
+
51
+ ### Auth
52
+
53
+ Add an integration token path for adapter calls.
54
+
55
+ Recommended rules:
56
+
57
+ - token is sent with `Authorization: Bearer <token>`
58
+ - token is scoped to one `project_id`
59
+ - token is scoped to one `page_id`
60
+ - token can only create/list/read/update/delete issues in that scope
61
+ - token can upload attachments for those issues
62
+
63
+ Do not expose issue CRUD without auth.
64
+
65
+ ## Adapter config
66
+
67
+ ```ts
68
+ type DfSheetAdapterOptions = {
69
+ baseUrl: string;
70
+ projectId: string;
71
+ pageId: string;
72
+ token: string;
73
+ issueType?: string;
74
+ issuePriority?: "low" | "medium" | "high" | "urgent";
75
+ };
76
+ ```
77
+
78
+ Example usage during the Lexus pilot:
79
+
80
+ ```ts
81
+ createWebReviewKit({
82
+ projectId: "lexus-official-v2026",
83
+ adapter: createDfSheetAdapter({
84
+ baseUrl: "https://df-sheet.vercel.app",
85
+ projectId: "<df-sheet-project-id>",
86
+ pageId: "<df-sheet-page-id>",
87
+ token: "<integration-token>",
88
+ issueType: "task",
89
+ issuePriority: "medium",
90
+ }),
91
+ });
92
+ ```
93
+
94
+ If the target project has no backend proxy, assume the browser can see this
95
+ token. Keep the token project/page scoped so leakage impact is limited.
96
+
97
+ ## Adapter contract changes
98
+
99
+ Current adapter contract:
100
+
101
+ ```ts
102
+ interface WebReviewKitAdapter {
103
+ list(query: ReviewItemQuery): Promise<ReviewItem[]>;
104
+ create(item: ReviewItem): Promise<ReviewItem>;
105
+ update(id: string, patch: Partial<Omit<ReviewItem, "id" | "createdAt">>): Promise<ReviewItem>;
106
+ remove(id: string): Promise<void>;
107
+ }
108
+ ```
109
+
110
+ Add `get(id)` for deep-link restore:
111
+
112
+ ```ts
113
+ interface WebReviewKitAdapter {
114
+ get(id: string): Promise<ReviewItem | null>;
115
+ list(query: ReviewItemQuery): Promise<ReviewItem[]>;
116
+ create(item: ReviewItem): Promise<ReviewItem>;
117
+ update(id: string, patch: Partial<Omit<ReviewItem, "id" | "createdAt">>): Promise<ReviewItem>;
118
+ remove(id: string): Promise<void>;
119
+ }
120
+ ```
121
+
122
+ `localAdapter` should implement `get(id)` from local storage.
123
+
124
+ ## Status mapping
125
+
126
+ df-web-review-kit:
127
+
128
+ - `todo`
129
+ - `doing`
130
+ - `review`
131
+ - `hold`
132
+ - `done`
133
+
134
+ df-sheet:
135
+
136
+ - `todo`
137
+ - `in_progress`
138
+ - `review`
139
+ - `on_hold`
140
+ - `done`
141
+
142
+ Mapping:
143
+
144
+ ```ts
145
+ const toDfSheetStatus = {
146
+ todo: "todo",
147
+ doing: "in_progress",
148
+ review: "review",
149
+ hold: "on_hold",
150
+ done: "done",
151
+ } as const;
152
+
153
+ const fromDfSheetStatus = {
154
+ todo: "todo",
155
+ in_progress: "doing",
156
+ review: "review",
157
+ on_hold: "hold",
158
+ done: "done",
159
+ } as const;
160
+ ```
161
+
162
+ ## Review metadata shape
163
+
164
+ The metadata must be enough to rebuild a `ReviewItem` from a df-sheet issue.
165
+
166
+ ```ts
167
+ type DfSheetReviewMetadata = {
168
+ schema: "df-web-review-kit";
169
+ version: 1;
170
+ reviewProjectId: string;
171
+ routeKey: string;
172
+ pageUrl: string;
173
+ originalUrl?: string;
174
+ normalizedPath: string;
175
+ scope?: ReviewItemScope;
176
+ kind: ReviewItemKind;
177
+ viewport: ViewportSize;
178
+ devicePixelRatio?: number;
179
+ scroll?: {
180
+ x: number;
181
+ y: number;
182
+ };
183
+ anchor?: DomAnchor;
184
+ marker?: ReviewMarker;
185
+ selection?: ReviewSelection;
186
+ reviewUrl?: string;
187
+ };
188
+ ```
189
+
190
+ Avoid storing screenshot data URLs in metadata. Use `attachments`.
191
+
192
+ ## Issue mapping
193
+
194
+ ### ReviewItem to Issue
195
+
196
+ ```ts
197
+ {
198
+ project_id: options.projectId,
199
+ page_id: options.pageId,
200
+ title: item.title || makeIssueTitle(item),
201
+ description: makeIssueDescription(item),
202
+ status: toDfSheetStatus[normalizeReviewItemStatus(item.status)],
203
+ priority: options.issuePriority ?? "medium",
204
+ type: options.issueType ?? "task",
205
+ attachments: item.screenshot ? [uploadedScreenshot] : [],
206
+ links: makeReviewUrl(item),
207
+ review_metadata: makeReviewMetadata(item),
208
+ }
209
+ ```
210
+
211
+ ### Issue to ReviewItem
212
+
213
+ ```ts
214
+ {
215
+ id: issue.id,
216
+ externalIssueId: issue.id,
217
+ projectId: metadata.reviewProjectId,
218
+ routeKey: metadata.routeKey,
219
+ pageUrl: metadata.pageUrl,
220
+ originalUrl: metadata.originalUrl,
221
+ normalizedPath: metadata.normalizedPath,
222
+ scope: metadata.scope,
223
+ kind: metadata.kind,
224
+ title: issue.title,
225
+ comment: extractComment(issue.description),
226
+ status: fromDfSheetStatus[issue.status],
227
+ viewport: metadata.viewport,
228
+ devicePixelRatio: metadata.devicePixelRatio,
229
+ scroll: metadata.scroll,
230
+ anchor: metadata.anchor,
231
+ marker: metadata.marker,
232
+ selection: metadata.selection,
233
+ screenshot: undefined,
234
+ createdAt: issue.created_at,
235
+ updatedAt: issue.updated_at ?? issue.created_at,
236
+ }
237
+ ```
238
+
239
+ ## Deep link flow
240
+
241
+ Target URL:
242
+
243
+ ```txt
244
+ /review?target=/path&w=390&h=720&item=<issue.id>
245
+ ```
246
+
247
+ Flow:
248
+
249
+ 1. `ReviewShell` reads `item` from the query string.
250
+ 2. It calls `adapter.get(itemId)`.
251
+ 3. The adapter fetches `GET /api/issues/:id`.
252
+ 4. It maps the df-sheet issue to `ReviewItem`.
253
+ 5. `ReviewShell` restores target route and viewport from the item metadata.
254
+ 6. It reloads the target iframe.
255
+ 7. It highlights the restored item after the target document is ready.
256
+
257
+ The query params `target`, `w`, and `h` are hints for early shell rendering.
258
+ `adapter.get(item)` is the source of truth.
259
+
260
+ ## List sync
261
+
262
+ MVP behavior:
263
+
264
+ - load list on review shell mount
265
+ - reload after create/update/delete
266
+ - reload when target route changes
267
+ - reload on manual refresh
268
+ - optionally reload when the window regains focus
269
+
270
+ Do not start with realtime sync. Add Supabase realtime later only if the Lexus
271
+ pilot shows that multiple reviewers need live updates.
272
+
273
+ ## Delete behavior
274
+
275
+ Use the existing df-sheet delete API for the pilot:
276
+
277
+ ```txt
278
+ DELETE /api/issues/:id
279
+ ```
280
+
281
+ If teams want QA history preserved later, add an archive/hidden status in
282
+ df-sheet instead of hard delete.
283
+
284
+ ## Implementation phases
285
+
286
+ ### Phase 1: contract
287
+
288
+ - add `adapter.get(id)`
289
+ - update `localAdapter`
290
+ - update review shell deep-link restore to use adapter data instead of local
291
+ storage only
292
+
293
+ ### Phase 2: df-sheet adapter
294
+
295
+ - add `src/adapters/df-sheet.ts`
296
+ - implement `createDfSheetAdapter(options)`
297
+ - implement issue/status/metadata mapping helpers
298
+ - implement screenshot upload helper
299
+ - keep the adapter unexported or internally linked during Lexus pilot
300
+
301
+ ### Phase 3: Lexus pilot
302
+
303
+ - configure Lexus review shell with df-sheet adapter options
304
+ - test create/list/update/delete
305
+ - test screenshot attachment upload
306
+ - test `/review?...&item=<issue.id>` restore
307
+ - test status changes from both review-kit and df-sheet
308
+
309
+ ### Phase 4: package export
310
+
311
+ After the Lexus pilot is stable:
312
+
313
+ - export as `@designfever/web-review-kit/adapters/df-sheet`
314
+ - include adapter files in package build
315
+ - update README with installation/config examples
316
+ - publish npm package
317
+
318
+ ## Verification checklist
319
+
320
+ - `pnpm typecheck`
321
+ - `pnpm build`
322
+ - create text QA from Lexus review page
323
+ - create capture QA with screenshot
324
+ - list reload shows created issues
325
+ - status update maps both directions
326
+ - delete removes item from list
327
+ - deep link restores route, viewport, and highlighted QA
328
+ - df-sheet issue detail shows title, description, status, attachment, and link
329
+
330
+ ## Open questions
331
+
332
+ - Lexus df-sheet `project_id`
333
+ - Lexus df-sheet `page_id`
334
+ - integration token creation and rotation flow
335
+ - whether df-sheet should expose an issue type dedicated to QA
336
+ - whether delete should become archive after the pilot
@@ -0,0 +1,222 @@
1
+ # df-sheet adapter next plan
2
+
3
+ ## 목적
4
+
5
+ df-sheet를 `df-web-review-kit`의 remote destination으로 계속 사용할 때의 역할과 필요한 df-sheet 쪽 변경을 분리해서 정리한다.
6
+
7
+ Supabase 실험 문서는 adapter 구조 검증용이고, 이 문서는 df-sheet 제품/운영 흐름 기준이다.
8
+
9
+ ## 현재 위치
10
+
11
+ 현재 `dfSheetAdapter`는 review-kit item을 df-sheet issue로 보낸다.
12
+
13
+ 현재 동작:
14
+
15
+ - `create`: local QA item을 df-sheet issue로 등록
16
+ - `list`: df-sheet issue 목록을 review item으로 변환
17
+ - `get`: issue detail을 review item으로 변환
18
+ - `update/remove`: review-kit 안에서는 read-only로 막음
19
+ - restore data: `issues.review_metadata`
20
+ - review link: issue metadata/link에 `/review?...&item=<issueId>` 저장
21
+
22
+ 현재 shell adapter에서는 df-sheet config에 `updateStatus`와 `remove`를 넣지 않는다. 따라서 df-sheet source에서는 상태 변경 dropdown은 readonly badge로만 둘 수 있고, 삭제 버튼도 숨긴다.
23
+
24
+ ## 권장 역할
25
+
26
+ df-sheet는 source of record가 되어야 한다. review-kit은 다음 역할까지만 한다.
27
+
28
+ - local에서 만든 QA를 df-sheet issue로 등록
29
+ - df-sheet issue를 현재 page/viewport에서 restore
30
+ - issue status를 표시
31
+ - 필요 시 status update만 좁게 허용
32
+
33
+ review-kit이 df-sheet의 전체 issue editor가 되면 안 된다.
34
+
35
+ ## 상태 변경 정책
36
+
37
+ generic `update`는 넣지 않는다.
38
+
39
+ df-sheet 상태를 review-kit에서 바꾸고 싶다면 `updateStatus`만 추가한다.
40
+
41
+ ```ts
42
+ {
43
+ label: 'df-sheet',
44
+ get,
45
+ list,
46
+ create,
47
+ statusOptions: DF_SHEET_STATUS_OPTIONS,
48
+ updateStatus: async ({ id, status, statusOption, statusIndex }) => {
49
+ return dfSheet.updateIssueStatus(id, {
50
+ status,
51
+ statusIndex,
52
+ statusLabel: statusOption.label,
53
+ });
54
+ },
55
+ }
56
+ ```
57
+
58
+ 상태 기준:
59
+
60
+ - 저장 identity: `statusOption.value`
61
+ - 표시명: `statusOption.label`
62
+ - 외부 매핑 보조값: `statusIndex`
63
+
64
+ df-sheet의 실제 status vocabulary가 다르면 adapter에서 mapping한다.
65
+
66
+ ```ts
67
+ const DF_SHEET_STATUS_OPTIONS = [
68
+ { value: 'todo', label: '작업전' },
69
+ { value: 'doing', label: '작업중' },
70
+ { value: 'review', label: '검토 필요' },
71
+ { value: 'hold', label: '보류' },
72
+ { value: 'done', label: '완료' },
73
+ ] as const;
74
+ ```
75
+
76
+ ## df-sheet API 제안
77
+
78
+ 현재 issue CRUD API를 그대로 넓히기보다 review-kit 전용 endpoint를 두는 쪽이 안전하다.
79
+
80
+ 추천 endpoint:
81
+
82
+ ```txt
83
+ GET /api/review-kit/issues
84
+ GET /api/review-kit/issues/:id
85
+ POST /api/review-kit/issues
86
+ PATCH /api/review-kit/issues/:id/status
87
+ ```
88
+
89
+ 선택 endpoint:
90
+
91
+ ```txt
92
+ DELETE /api/review-kit/issues/:id
93
+ ```
94
+
95
+ 요청은 integration token 기준으로 project/page scope를 제한한다.
96
+
97
+ ```http
98
+ Authorization: Bearer <review-kit-token>
99
+ ```
100
+
101
+ Token scope:
102
+
103
+ - `project_id`
104
+ - `page_id`
105
+ - allowed actions: `create`, `read`, optional `update_status`, optional `delete`
106
+
107
+ 브라우저에서 직접 token을 쓰는 경우 누출을 전제로 작게 scope를 잡는다. production에서는 backend proxy 또는 df-sheet domain cookie auth를 우선 검토한다.
108
+
109
+ ## DB 필드
110
+
111
+ `issues.review_metadata jsonb`는 유지한다.
112
+
113
+ ```sql
114
+ alter table issues
115
+ add column if not exists review_metadata jsonb null;
116
+ ```
117
+
118
+ `review_metadata`에 들어가는 값:
119
+
120
+ - review item id
121
+ - review number
122
+ - route/normalized path
123
+ - viewport
124
+ - scroll
125
+ - anchor
126
+ - marker
127
+ - selection
128
+ - original comment
129
+ - review link
130
+ - schema/source/version
131
+
132
+ 사람이 읽어야 하는 내용은 `title`/`description`에 둔다. restore에 필요한 구조 데이터는 `review_metadata`에 둔다.
133
+
134
+ ## create flow
135
+
136
+ 1. local item 선택
137
+ 2. review-kit이 df-sheet `POST /api/review-kit/issues` 호출
138
+ 3. df-sheet가 issue 생성
139
+ 4. df-sheet가 issue id/url 반환
140
+ 5. remote 등록 성공 시 local item은 삭제하고, 실패 시에만 `syncSubmission`으로 실패 상태를 기록
141
+
142
+ remote issue id를 local id로 덮어쓰지 않는다. 대신 remote 등록 payload에서는 사람이 볼 번호로 `reviewNumber`를 보낸다.
143
+
144
+ ## list/get flow
145
+
146
+ `list`는 route 기준으로 좁게 가져온다.
147
+
148
+ ```txt
149
+ project_id=<df-sheet-project-id>
150
+ page_id=<df-sheet-page-id>
151
+ review_source=df-web-review-kit
152
+ review_route_key=<target route>
153
+ ```
154
+
155
+ 응답에는 `review_metadata`가 반드시 포함되어야 한다. metadata가 없거나 source/schema가 맞지 않으면 adapter는 review item으로 변환하지 않는다.
156
+
157
+ ## status update flow
158
+
159
+ df-sheet에서 status 변경을 허용하려면 issue 전체 patch가 아니라 status-only endpoint를 쓴다.
160
+
161
+ ```txt
162
+ PATCH /api/review-kit/issues/:id/status
163
+ ```
164
+
165
+ Body:
166
+
167
+ ```json
168
+ {
169
+ "status": "doing"
170
+ }
171
+ ```
172
+
173
+ Response는 updated issue와 `review_metadata`를 포함한다.
174
+
175
+ review-kit adapter는 이 응답을 `ReviewItem`으로 변환한다.
176
+
177
+ ## delete policy
178
+
179
+ 초기에는 df-sheet delete를 review-kit에서 열지 않는 편이 낫다.
180
+
181
+ 이유:
182
+
183
+ - remote issue 삭제는 팀 공유 데이터에 직접 영향이 크다.
184
+ - review-kit list에서 실수 클릭으로 삭제될 수 있다.
185
+ - df-sheet 내부 권한/감사 로그와 맞춰야 한다.
186
+
187
+ 필요하면 `remove`를 adapter에 넣고, UI는 확인 modal을 추가한다. 현재 shell의 local 삭제 UX와 같은 즉시 삭제 방식으로 remote delete를 열면 안 된다.
188
+
189
+ ## package 관점
190
+
191
+ df-sheet adapter를 package에 포함할 때도 df-sheet 전용 타입/설정은 adapter 내부에 묶는다.
192
+
193
+ `ReviewShellAdapter`는 계속 storage-agnostic이어야 한다.
194
+
195
+ 좋은 방향:
196
+
197
+ - `dfSheetAdapter(options)`는 core `WebReviewKitAdapter` 구현
198
+ - `dfSheetShellAdapter(options)` 또는 helper는 shell-facing adapter config 생성
199
+ - page/review에서는 helper를 조립해서 넘김
200
+
201
+ 예시:
202
+
203
+ ```ts
204
+ const dfSheet = dfSheetAdapter(options);
205
+
206
+ const REVIEW_ADAPTERS = [
207
+ localReviewShellAdapter(local),
208
+ dfSheetReviewShellAdapter(dfSheet, {
209
+ pageId: options.pageId,
210
+ statusOptions: DF_SHEET_STATUS_OPTIONS,
211
+ canUpdateStatus: false,
212
+ }),
213
+ ];
214
+ ```
215
+
216
+ ## 남은 결정
217
+
218
+ - df-sheet에서 review-kit 전용 endpoint를 새로 만들지, 기존 `/api/issues`를 확장할지.
219
+ - status update를 review-kit에서 허용할지.
220
+ - integration token을 cookie auth와 병행할지.
221
+ - df-sheet issue status vocabulary와 review-kit status value를 1:1로 맞출지.
222
+ - remote delete를 허용할지.