@su-record/vibe 2.7.6 → 2.7.9
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.
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +78 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/update.js +17 -2
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/cli/postinstall/codex-agents.d.ts +12 -0
- package/dist/cli/postinstall/codex-agents.d.ts.map +1 -0
- package/dist/cli/postinstall/codex-agents.js +51 -0
- package/dist/cli/postinstall/codex-agents.js.map +1 -0
- package/dist/cli/postinstall/codex-instruction.d.ts +10 -0
- package/dist/cli/postinstall/codex-instruction.d.ts.map +1 -0
- package/dist/cli/postinstall/codex-instruction.js +56 -0
- package/dist/cli/postinstall/codex-instruction.js.map +1 -0
- package/dist/cli/postinstall/constants.d.ts.map +1 -1
- package/dist/cli/postinstall/constants.js +1 -0
- package/dist/cli/postinstall/constants.js.map +1 -1
- package/dist/cli/postinstall/gemini-agents.d.ts +12 -0
- package/dist/cli/postinstall/gemini-agents.d.ts.map +1 -0
- package/dist/cli/postinstall/gemini-agents.js +80 -0
- package/dist/cli/postinstall/gemini-agents.js.map +1 -0
- package/dist/cli/postinstall/gemini-instruction.d.ts +10 -0
- package/dist/cli/postinstall/gemini-instruction.d.ts.map +1 -0
- package/dist/cli/postinstall/gemini-instruction.js +59 -0
- package/dist/cli/postinstall/gemini-instruction.js.map +1 -0
- package/dist/cli/postinstall/index.d.ts +4 -0
- package/dist/cli/postinstall/index.d.ts.map +1 -1
- package/dist/cli/postinstall/index.js +4 -0
- package/dist/cli/postinstall/index.js.map +1 -1
- package/dist/cli/postinstall/main.d.ts.map +1 -1
- package/dist/cli/postinstall/main.js +34 -1
- package/dist/cli/postinstall/main.js.map +1 -1
- package/dist/cli/postinstall.d.ts +1 -1
- package/dist/cli/postinstall.d.ts.map +1 -1
- package/dist/cli/postinstall.js +1 -1
- package/dist/cli/postinstall.js.map +1 -1
- package/dist/cli/setup/ProjectSetup.d.ts +15 -0
- package/dist/cli/setup/ProjectSetup.d.ts.map +1 -1
- package/dist/cli/setup/ProjectSetup.js +159 -0
- package/dist/cli/setup/ProjectSetup.js.map +1 -1
- package/dist/cli/setup.d.ts +1 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1 -1
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/utils/cli-detector.d.ts +25 -0
- package/dist/cli/utils/cli-detector.d.ts.map +1 -0
- package/dist/cli/utils/cli-detector.js +55 -0
- package/dist/cli/utils/cli-detector.js.map +1 -0
- package/hooks/gemini-hooks.json +73 -0
- package/package.json +1 -1
- package/skills/agents-md/SKILL.md +120 -0
- package/skills/brand-assets/SKILL.md +8 -0
- package/skills/characterization-test/SKILL.md +4 -0
- package/skills/commerce-patterns/SKILL.md +36 -338
- package/skills/commit-push-pr/SKILL.md +21 -64
- package/skills/core-capabilities/SKILL.md +26 -142
- package/skills/e2e-commerce/SKILL.md +37 -284
- package/skills/frontend-design/SKILL.md +12 -31
- package/skills/git-worktree/SKILL.md +34 -146
- package/skills/handoff/SKILL.md +8 -0
- package/skills/parallel-research/SKILL.md +7 -0
- package/skills/priority-todos/SKILL.md +34 -213
- package/skills/seo-checklist/SKILL.md +38 -225
- package/skills/tool-fallback/SKILL.md +53 -143
- package/skills/typescript-advanced-types/SKILL.md +30 -685
- package/skills/ui-ux-pro-max/SKILL.md +40 -220
- package/skills/vercel-react-best-practices/SKILL.md +38 -283
- package/skills/video-production/SKILL.md +35 -206
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [
|
|
4
|
+
{
|
|
5
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/session-start.js"
|
|
6
|
+
}
|
|
7
|
+
],
|
|
8
|
+
"BeforeTool": [
|
|
9
|
+
{
|
|
10
|
+
"tool_name": "shell",
|
|
11
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Bash"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"tool_name": "shell",
|
|
15
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Bash"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"tool_name": "edit",
|
|
19
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Edit"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"tool_name": "edit",
|
|
23
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Edit"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"tool_name": "write_file",
|
|
27
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/sentinel-guard.js Write"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"tool_name": "write_file",
|
|
31
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/pre-tool-guard.js Write"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"AfterTool": [
|
|
35
|
+
{
|
|
36
|
+
"tool_name": "edit",
|
|
37
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/code-check.js"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"tool_name": "write_file",
|
|
41
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/code-check.js"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"tool_name": "edit",
|
|
45
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/post-edit.js"
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"BeforeAgent": [
|
|
49
|
+
{
|
|
50
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/prompt-dispatcher.js"
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"Notification": [
|
|
54
|
+
{
|
|
55
|
+
"matcher": "context_window_80",
|
|
56
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/context-save.js medium"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"matcher": "context_window_90",
|
|
60
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/context-save.js high"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"matcher": "context_window_95",
|
|
64
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/context-save.js critical"
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
"SessionEnd": [
|
|
68
|
+
{
|
|
69
|
+
"command": "VIBE_CLI=gemini node {{VIBE_PATH}}/hooks/scripts/stop-notify.js"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agents-md
|
|
3
|
+
description: "Optimize AGENTS.md / CLAUDE.md by removing discoverable info and keeping only gotchas. Based on Addy Osmani's AGENTS.md principles. Activates on agents.md, claude.md, context file optimization."
|
|
4
|
+
triggers: [agents.md, claude.md, context file, optimize agents, optimize claude]
|
|
5
|
+
priority: 50
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# agents-md — Context File Optimizer
|
|
9
|
+
|
|
10
|
+
AGENTS.md / CLAUDE.md를 최적화한다.
|
|
11
|
+
근거: https://addyosmani.com/blog/agents-md/
|
|
12
|
+
|
|
13
|
+
## 핵심 원칙
|
|
14
|
+
|
|
15
|
+
**한 줄 테스트**: "에이전트가 코드를 읽어서 스스로 알 수 있는가?" → Yes면 삭제.
|
|
16
|
+
|
|
17
|
+
## Step 1: 대상 파일 찾기
|
|
18
|
+
|
|
19
|
+
프로젝트 루트에서 다음 파일을 탐색한다:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Glob: pattern="AGENTS.md"
|
|
23
|
+
Glob: pattern="CLAUDE.md"
|
|
24
|
+
Glob: pattern=".cursorrules"
|
|
25
|
+
Glob: pattern=".github/copilot-instructions.md"
|
|
26
|
+
Glob: pattern=".windsurfrules"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
대상 파일이 없으면 새로 생성할지 사용자에게 확인한다.
|
|
30
|
+
|
|
31
|
+
## Step 2: 현재 내용 분류
|
|
32
|
+
|
|
33
|
+
파일의 각 항목을 다음 기준으로 분류한다:
|
|
34
|
+
|
|
35
|
+
### 삭제 (Discoverable)
|
|
36
|
+
|
|
37
|
+
에이전트가 코드 탐색으로 발견 가능한 정보:
|
|
38
|
+
|
|
39
|
+
| 유형 | 예시 | 발견 경로 |
|
|
40
|
+
|------|------|----------|
|
|
41
|
+
| 디렉토리 구조 | "src/에 컴포넌트가 있다" | `ls`, `Glob` |
|
|
42
|
+
| 기술 스택 | "React + TypeScript 사용" | `package.json`, 파일 확장자 |
|
|
43
|
+
| Phase/진행 테이블 | "Phase 1 ✅, Phase 2 ✅..." | 이력일 뿐, 행동 지침 아님 |
|
|
44
|
+
| 빌드/테스트 커맨드 | "npm test로 테스트 실행" | `package.json` scripts |
|
|
45
|
+
| API 엔드포인트 목록 | "POST /api/users" | 라우터 코드 |
|
|
46
|
+
| 기능별 상세 설명 | "Phase 3에서 서킷브레이커 구현" | 해당 코드 읽으면 파악 가능 |
|
|
47
|
+
| 아키텍처 다이어그램 | ASCII 박스 다이어그램 | 개념적 설명, 실수 방지 효과 없음 |
|
|
48
|
+
|
|
49
|
+
### 유지 (Non-discoverable)
|
|
50
|
+
|
|
51
|
+
에이전트가 코드만으로 알 수 없는 함정과 규칙:
|
|
52
|
+
|
|
53
|
+
| 유형 | 예시 |
|
|
54
|
+
|------|------|
|
|
55
|
+
| 런타임 함정 | "Bun이다, Node가 아니다" (package.json에 명시 안 됨) |
|
|
56
|
+
| 금지 패턴 | "require() 쓰지 말 것", "React 패턴 금지" |
|
|
57
|
+
| SSOT 위치 | "model-registry.ts만 수정할 것, 하드코딩 금지" |
|
|
58
|
+
| 불변 순서/규칙 | "우선순위: A → B → C, 이 순서 변경 금지" |
|
|
59
|
+
| 재시도/폴백 제한 | "최대 2회, 3회 이상 금지" |
|
|
60
|
+
| 도구 선택 | "Zod만 사용, joi/yup 금지" |
|
|
61
|
+
| 네이밍 컨벤션 | 비표준 패턴 (표준이면 삭제) |
|
|
62
|
+
| 프로젝트 한 줄 소개 | 코드만으로 목적 파악 어려운 경우 |
|
|
63
|
+
| 언어/응답 지시 | "한글로 답변" 같은 선호 |
|
|
64
|
+
|
|
65
|
+
### 앵커링 주의
|
|
66
|
+
|
|
67
|
+
기술 이름을 언급하면 에이전트가 해당 기술로 편향된다. "무엇을 쓰지 말라"는 유용하지만, "우리는 X를 쓴다"는 코드에서 이미 보인다면 불필요.
|
|
68
|
+
|
|
69
|
+
## Step 3: 재구성
|
|
70
|
+
|
|
71
|
+
다음 구조로 재작성한다:
|
|
72
|
+
|
|
73
|
+
```markdown
|
|
74
|
+
# {프로젝트명} — {한 줄 소개}
|
|
75
|
+
|
|
76
|
+
{프로젝트가 무엇을 하는지 1-2문장. 코드만으로 목적 파악이 어려운 경우에만.}
|
|
77
|
+
|
|
78
|
+
# Gotchas
|
|
79
|
+
|
|
80
|
+
- **{함정 제목}.** {구체적 금지/규칙 설명}.
|
|
81
|
+
- ...
|
|
82
|
+
|
|
83
|
+
# Naming
|
|
84
|
+
|
|
85
|
+
{비표준 네이밍 패턴이 있을 때만. 표준(camelCase 등)이면 생략.}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
규칙:
|
|
89
|
+
- 섹션은 최대 3개 (소개, Gotchas, Naming)
|
|
90
|
+
- Gotchas 항목은 각각 **볼드 제목 + 구체적 do/don't**
|
|
91
|
+
- "~를 사용한다" 보다 "~를 쓰지 말 것"이 더 유용
|
|
92
|
+
- 전체 50줄 이내 목표
|
|
93
|
+
|
|
94
|
+
## Step 4: CLAUDE.md 분리 (해당 시)
|
|
95
|
+
|
|
96
|
+
CLAUDE.md가 존재하면 Claude 전용 지시만 남긴다:
|
|
97
|
+
|
|
98
|
+
```markdown
|
|
99
|
+
{Claude 전용 지시. 예: "답변은 반드시 한글로 답변할 것."}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
공통 규칙은 AGENTS.md에 둔다. Claude Code는 둘 다 읽는다.
|
|
103
|
+
|
|
104
|
+
## Step 5: 결과 보고
|
|
105
|
+
|
|
106
|
+
```markdown
|
|
107
|
+
## AGENTS.md 최적화 결과
|
|
108
|
+
|
|
109
|
+
| 지표 | Before | After |
|
|
110
|
+
|------|--------|-------|
|
|
111
|
+
| 줄 수 | N | N |
|
|
112
|
+
| 삭제 항목 | - | N개 (discoverable) |
|
|
113
|
+
| 유지 항목 | - | N개 (gotchas) |
|
|
114
|
+
|
|
115
|
+
### 삭제된 항목
|
|
116
|
+
- {항목}: {삭제 이유}
|
|
117
|
+
|
|
118
|
+
### 유지/추가된 항목
|
|
119
|
+
- {항목}: {유지 이유}
|
|
120
|
+
```
|
|
@@ -136,3 +136,11 @@ public/
|
|
|
136
136
|
android-chrome-512x512.png
|
|
137
137
|
site.webmanifest
|
|
138
138
|
```
|
|
139
|
+
|
|
140
|
+
## Done Criteria (K4)
|
|
141
|
+
|
|
142
|
+
- [ ] All required sizes generated (16x16 through 512x512)
|
|
143
|
+
- [ ] Icons work at small sizes (recognizable at 16x16)
|
|
144
|
+
- [ ] No text/letters in icon (illegible at small sizes)
|
|
145
|
+
- [ ] `site.webmanifest` updated with icon paths
|
|
146
|
+
- [ ] Fallback generated if Gemini API unavailable
|
|
@@ -11,6 +11,10 @@ priority: 65
|
|
|
11
11
|
|
|
12
12
|
Lock existing behavior with snapshot/characterization tests before modifying code. This prevents regressions in legacy, complex, or unfamiliar codebases.
|
|
13
13
|
|
|
14
|
+
## Pre-check (K1)
|
|
15
|
+
|
|
16
|
+
> Are you modifying existing code with uncertain behavior? If the code is new (you just wrote it), well-tested, or trivially simple, skip characterization tests and write regular unit tests instead.
|
|
17
|
+
|
|
14
18
|
## When to Use
|
|
15
19
|
|
|
16
20
|
| Scenario | Signal |
|
|
@@ -4,358 +4,56 @@ description: "E-commerce domain patterns - cart, payment, inventory with transac
|
|
|
4
4
|
triggers: [commerce, ecommerce, cart, payment, checkout, inventory, stock, order, pg, toss, stripe]
|
|
5
5
|
priority: 70
|
|
6
6
|
---
|
|
7
|
-
# Commerce Patterns Skill
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
# Commerce Patterns
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## Pre-check (K1)
|
|
12
11
|
|
|
13
|
-
-
|
|
14
|
-
- Payment integration (PG, Stripe, Toss)
|
|
15
|
-
- Inventory/stock management
|
|
16
|
-
- Order processing systems
|
|
12
|
+
> Is this an e-commerce transaction flow? If building simple CRUD without payment/stock management, this skill is not needed.
|
|
17
13
|
|
|
18
|
-
##
|
|
14
|
+
## Gotchas & Traps
|
|
19
15
|
|
|
20
|
-
|
|
16
|
+
These are the non-obvious failure modes LLMs typically miss:
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
interface CartItem {
|
|
25
|
-
productId: string;
|
|
26
|
-
variantId?: string;
|
|
27
|
-
quantity: number;
|
|
28
|
-
price: number; // Snapshot at add time
|
|
29
|
-
originalPrice: number; // For comparison
|
|
30
|
-
addedAt: Date;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface Cart {
|
|
34
|
-
id: string;
|
|
35
|
-
userId?: string; // null for guest
|
|
36
|
-
sessionId: string; // For guest merge
|
|
37
|
-
items: CartItem[];
|
|
38
|
-
couponCode?: string;
|
|
39
|
-
updatedAt: Date;
|
|
40
|
-
expiresAt: Date; // Cart expiration
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
#### Key Patterns
|
|
45
|
-
|
|
46
|
-
| Pattern | Description |
|
|
47
|
-
|---------|-------------|
|
|
48
|
-
| **Guest → User Merge** | Merge localStorage cart on login |
|
|
49
|
-
| **Price Snapshot** | Store price at add time, revalidate at checkout |
|
|
50
|
-
| **Expiration** | Clear abandoned carts after N days |
|
|
51
|
-
| **Validation** | Check stock/price at checkout entry |
|
|
52
|
-
|
|
53
|
-
#### Implementation
|
|
54
|
-
```typescript
|
|
55
|
-
class CartService {
|
|
56
|
-
async addItem(cartId: string, item: AddItemRequest): Promise<Cart> {
|
|
57
|
-
// 1. Validate product exists and in stock
|
|
58
|
-
const product = await this.productService.get(item.productId);
|
|
59
|
-
if (!product || product.stock < item.quantity) {
|
|
60
|
-
throw new OutOfStockError();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 2. Snapshot current price
|
|
64
|
-
const cartItem: CartItem = {
|
|
65
|
-
...item,
|
|
66
|
-
price: product.currentPrice,
|
|
67
|
-
originalPrice: product.originalPrice,
|
|
68
|
-
addedAt: new Date(),
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// 3. Add or update quantity
|
|
72
|
-
return this.cartRepository.upsertItem(cartId, cartItem);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async mergeGuestCart(userId: string, sessionId: string): Promise<Cart> {
|
|
76
|
-
const guestCart = await this.cartRepository.findBySession(sessionId);
|
|
77
|
-
const userCart = await this.cartRepository.findByUser(userId);
|
|
78
|
-
|
|
79
|
-
if (!guestCart) return userCart;
|
|
80
|
-
|
|
81
|
-
// Merge: user cart takes priority for duplicates
|
|
82
|
-
const merged = this.mergeItems(userCart.items, guestCart.items);
|
|
83
|
-
await this.cartRepository.delete(guestCart.id);
|
|
84
|
-
|
|
85
|
-
return this.cartRepository.update(userCart.id, { items: merged });
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### 2. Payment
|
|
91
|
-
|
|
92
|
-
#### State Machine
|
|
93
|
-
```
|
|
94
|
-
PENDING → PROCESSING → AUTHORIZED → CAPTURED → COMPLETED
|
|
95
|
-
↘ FAILED
|
|
96
|
-
↘ CANCELED
|
|
97
|
-
COMPLETED → REFUND_REQUESTED → REFUNDED (partial/full)
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
#### Idempotency Pattern (Critical)
|
|
101
|
-
```typescript
|
|
102
|
-
interface PaymentRequest {
|
|
103
|
-
orderId: string;
|
|
104
|
-
amount: number;
|
|
105
|
-
currency: string;
|
|
106
|
-
idempotencyKey: string; // REQUIRED: `order_${orderId}_${timestamp}`
|
|
107
|
-
paymentMethod: PaymentMethod;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
class PaymentService {
|
|
111
|
-
async processPayment(request: PaymentRequest): Promise<PaymentResult> {
|
|
112
|
-
// 1. Check idempotency - prevent duplicate charges
|
|
113
|
-
const existing = await this.paymentRepository.findByIdempotencyKey(
|
|
114
|
-
request.idempotencyKey
|
|
115
|
-
);
|
|
116
|
-
if (existing) {
|
|
117
|
-
return existing.result; // Return cached result
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// 2. Create payment record (PENDING)
|
|
121
|
-
const payment = await this.paymentRepository.create({
|
|
122
|
-
...request,
|
|
123
|
-
status: 'PENDING',
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
// 3. Call PG adapter
|
|
128
|
-
const pgResult = await this.pgAdapter.authorize(request);
|
|
129
|
-
|
|
130
|
-
// 4. Update status
|
|
131
|
-
return this.paymentRepository.update(payment.id, {
|
|
132
|
-
status: pgResult.success ? 'AUTHORIZED' : 'FAILED',
|
|
133
|
-
pgTransactionId: pgResult.transactionId,
|
|
134
|
-
result: pgResult,
|
|
135
|
-
});
|
|
136
|
-
} catch (error) {
|
|
137
|
-
await this.paymentRepository.update(payment.id, {
|
|
138
|
-
status: 'FAILED',
|
|
139
|
-
error: error.message,
|
|
140
|
-
});
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
#### PG Adapter Pattern
|
|
148
|
-
```typescript
|
|
149
|
-
interface PGAdapter {
|
|
150
|
-
authorize(request: PaymentRequest): Promise<PGResult>;
|
|
151
|
-
capture(transactionId: string): Promise<PGResult>;
|
|
152
|
-
cancel(transactionId: string): Promise<PGResult>;
|
|
153
|
-
refund(transactionId: string, amount?: number): Promise<PGResult>;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Implementations
|
|
157
|
-
class TossPaymentsAdapter implements PGAdapter { /* ... */ }
|
|
158
|
-
class StripeAdapter implements PGAdapter { /* ... */ }
|
|
159
|
-
class PortOneAdapter implements PGAdapter { /* ... */ }
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
#### Webhook Handling
|
|
163
|
-
```typescript
|
|
164
|
-
class PaymentWebhookHandler {
|
|
165
|
-
async handle(event: WebhookEvent): Promise<void> {
|
|
166
|
-
// 1. Verify signature
|
|
167
|
-
if (!this.verifySignature(event)) {
|
|
168
|
-
throw new UnauthorizedError();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// 2. Idempotency check - process each event only once
|
|
172
|
-
const processed = await this.eventStore.find(event.id);
|
|
173
|
-
if (processed) {
|
|
174
|
-
return; // Already processed
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// 3. Process event
|
|
178
|
-
await this.processEvent(event);
|
|
179
|
-
|
|
180
|
-
// 4. Mark as processed
|
|
181
|
-
await this.eventStore.save({
|
|
182
|
-
eventId: event.id,
|
|
183
|
-
processedAt: new Date(),
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### 3. Inventory (Stock Management)
|
|
190
|
-
|
|
191
|
-
#### Reservation Pattern (Two-Phase)
|
|
192
|
-
```typescript
|
|
193
|
-
interface StockReservation {
|
|
194
|
-
id: string;
|
|
195
|
-
productId: string;
|
|
196
|
-
quantity: number;
|
|
197
|
-
orderId: string;
|
|
198
|
-
status: 'RESERVED' | 'COMMITTED' | 'RELEASED';
|
|
199
|
-
expiresAt: Date; // Auto-release if not committed
|
|
200
|
-
createdAt: Date;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
class InventoryService {
|
|
204
|
-
// Phase 1: Reserve stock (at checkout start)
|
|
205
|
-
async reserve(orderId: string, items: OrderItem[]): Promise<void> {
|
|
206
|
-
for (const item of items) {
|
|
207
|
-
// Atomic decrement with check
|
|
208
|
-
const result = await this.db.query(`
|
|
209
|
-
UPDATE products
|
|
210
|
-
SET reserved_stock = reserved_stock + $1
|
|
211
|
-
WHERE id = $2
|
|
212
|
-
AND (available_stock - reserved_stock) >= $1
|
|
213
|
-
RETURNING *
|
|
214
|
-
`, [item.quantity, item.productId]);
|
|
215
|
-
|
|
216
|
-
if (result.rowCount === 0) {
|
|
217
|
-
// Rollback previous reservations
|
|
218
|
-
await this.releaseAll(orderId);
|
|
219
|
-
throw new InsufficientStockError(item.productId);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
await this.reservationRepository.create({
|
|
223
|
-
orderId,
|
|
224
|
-
productId: item.productId,
|
|
225
|
-
quantity: item.quantity,
|
|
226
|
-
status: 'RESERVED',
|
|
227
|
-
expiresAt: addMinutes(new Date(), 15), // 15min hold
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Phase 2: Commit stock (after payment success)
|
|
233
|
-
async commit(orderId: string): Promise<void> {
|
|
234
|
-
const reservations = await this.reservationRepository.findByOrder(orderId);
|
|
235
|
-
|
|
236
|
-
for (const reservation of reservations) {
|
|
237
|
-
await this.db.query(`
|
|
238
|
-
UPDATE products
|
|
239
|
-
SET
|
|
240
|
-
available_stock = available_stock - $1,
|
|
241
|
-
reserved_stock = reserved_stock - $1
|
|
242
|
-
WHERE id = $2
|
|
243
|
-
`, [reservation.quantity, reservation.productId]);
|
|
244
|
-
|
|
245
|
-
await this.reservationRepository.update(reservation.id, {
|
|
246
|
-
status: 'COMMITTED',
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Rollback: Release stock (payment failed or timeout)
|
|
252
|
-
async release(orderId: string): Promise<void> {
|
|
253
|
-
const reservations = await this.reservationRepository.findByOrder(orderId);
|
|
18
|
+
### Payment
|
|
254
19
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
`, [reservation.quantity, reservation.productId]);
|
|
20
|
+
| Trap | Consequence | Prevention |
|
|
21
|
+
|------|-------------|------------|
|
|
22
|
+
| No idempotency key | User double-clicks → charged twice | `idempotencyKey: order_${orderId}_${timestamp}` on every payment request |
|
|
23
|
+
| Webhook not idempotent | Retry delivers duplicate events | Check `eventId` before processing, store processed events |
|
|
24
|
+
| Missing webhook signature verification | Attacker forges payment confirmation | Always verify HMAC signature before processing |
|
|
25
|
+
| No payment state machine | Order stuck in limbo | `PENDING → PROCESSING → AUTHORIZED → CAPTURED → COMPLETED` (+ FAILED, CANCELED, REFUNDED) |
|
|
262
26
|
|
|
263
|
-
|
|
264
|
-
status: 'RELEASED',
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
27
|
+
### Inventory
|
|
271
28
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
WHERE id = $2 AND version = $3 AND stock >= $1
|
|
279
|
-
`, [quantity, productId, expectedVersion]);
|
|
29
|
+
| Trap | Consequence | Prevention |
|
|
30
|
+
|------|-------------|------------|
|
|
31
|
+
| Non-atomic stock decrement | Race condition → negative stock | `UPDATE SET stock = stock - $1 WHERE stock >= $1` (atomic with check) |
|
|
32
|
+
| No reservation TTL | Stock locked forever on abandoned checkout | 15-min reservation + scheduled cleanup job |
|
|
33
|
+
| Commit without reservation | Stock sold twice | Two-phase: RESERVE (checkout start) → COMMIT (payment success) / RELEASE (failure) |
|
|
34
|
+
| Optimistic lock without retry | Fails silently under contention | Use `version` column or `FOR UPDATE` for high-contention products |
|
|
280
35
|
|
|
281
|
-
|
|
282
|
-
await db.query(`
|
|
283
|
-
SELECT * FROM products WHERE id = $1 FOR UPDATE
|
|
284
|
-
`, [productId]);
|
|
36
|
+
### Cart & Pricing
|
|
285
37
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
await lock.unlock();
|
|
292
|
-
}
|
|
293
|
-
```
|
|
38
|
+
| Trap | Consequence | Prevention |
|
|
39
|
+
|------|-------------|------------|
|
|
40
|
+
| No price snapshot | Price changes between add and checkout | Store `price` at add time, revalidate at checkout entry |
|
|
41
|
+
| No guest→user cart merge | Guest loses cart on login | Merge by `sessionId` on login, user cart takes priority for duplicates |
|
|
42
|
+
| No cart expiration | Abandoned carts accumulate forever | TTL-based cleanup (e.g., 7 days) |
|
|
294
43
|
|
|
295
|
-
##
|
|
44
|
+
## Checkout Flow (Reference)
|
|
296
45
|
|
|
297
46
|
```
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
│ │
|
|
302
|
-
│ 1. Cart Validation │
|
|
303
|
-
│ └─ Revalidate prices, check stock availability │
|
|
304
|
-
│ │
|
|
305
|
-
│ 2. Order Creation (PENDING) │
|
|
306
|
-
│ └─ Generate orderId, snapshot cart │
|
|
307
|
-
│ │
|
|
308
|
-
│ 3. Stock Reservation │
|
|
309
|
-
│ └─ Reserve inventory (15min hold) │
|
|
310
|
-
│ │
|
|
311
|
-
│ 4. Payment Processing │
|
|
312
|
-
│ ├─ Success → Order PAID, Stock COMMIT │
|
|
313
|
-
│ └─ Failure → Order FAILED, Stock RELEASE │
|
|
314
|
-
│ │
|
|
315
|
-
│ 5. Order Confirmation │
|
|
316
|
-
│ └─ Send notification, trigger fulfillment │
|
|
317
|
-
│ │
|
|
318
|
-
└─────────────────────────────────────────────────────────────┘
|
|
47
|
+
Cart Validation → Order Creation (PENDING) → Stock Reservation (15min) → Payment
|
|
48
|
+
├─ Success → Order PAID, Stock COMMIT, Send Confirmation
|
|
49
|
+
└─ Failure → Order FAILED, Stock RELEASE
|
|
319
50
|
```
|
|
320
51
|
|
|
321
|
-
##
|
|
322
|
-
|
|
323
|
-
| Bug | Cause | Prevention |
|
|
324
|
-
|-----|-------|------------|
|
|
325
|
-
| **Duplicate payment** | User double-click, webhook retry | Idempotency key |
|
|
326
|
-
| **Negative stock** | Race condition | Atomic update with check |
|
|
327
|
-
| **Price mismatch** | Price changed during checkout | Snapshot + revalidation |
|
|
328
|
-
| **Orphan reservation** | Payment timeout without release | TTL + scheduled cleanup |
|
|
329
|
-
| **Lost webhook** | Network failure | Retry + idempotent handler |
|
|
330
|
-
| **Order state corruption** | Concurrent updates | State machine + versioning |
|
|
331
|
-
|
|
332
|
-
## Integration with /vibe.run
|
|
333
|
-
|
|
334
|
-
During commerce feature implementation:
|
|
52
|
+
## Done Criteria (K4)
|
|
335
53
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
## Checklist
|
|
343
|
-
|
|
344
|
-
### Cart
|
|
345
|
-
- [ ] Guest/user cart merge on login
|
|
346
|
-
- [ ] Price snapshot at add time
|
|
347
|
-
- [ ] Stock validation at checkout
|
|
348
|
-
- [ ] Cart expiration cleanup
|
|
349
|
-
|
|
350
|
-
### Payment
|
|
351
|
-
- [ ] Idempotency key on all requests
|
|
352
|
-
- [ ] Webhook signature verification
|
|
353
|
-
- [ ] Duplicate event handling
|
|
354
|
-
- [ ] Timeout/retry strategy
|
|
355
|
-
- [ ] Refund flow tested
|
|
356
|
-
|
|
357
|
-
### Inventory
|
|
358
|
-
- [ ] Two-phase reservation (reserve → commit/release)
|
|
359
|
-
- [ ] Atomic stock updates
|
|
360
|
-
- [ ] Reservation TTL and cleanup job
|
|
361
|
-
- [ ] Concurrency control tested
|
|
54
|
+
- [ ] Idempotency key on all payment requests
|
|
55
|
+
- [ ] Webhook handler is idempotent (dedup by eventId)
|
|
56
|
+
- [ ] Stock updates are atomic (SQL-level check)
|
|
57
|
+
- [ ] Two-phase reservation implemented with TTL
|
|
58
|
+
- [ ] Prices snapshot at cart add, revalidated at checkout
|
|
59
|
+
- [ ] Payment state machine covers all transitions
|