spec-gen-cli 1.2.6 → 1.2.8

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 (198) hide show
  1. package/README.md +175 -55
  2. package/dist/api/analyze.d.ts.map +1 -1
  3. package/dist/api/analyze.js +6 -1
  4. package/dist/api/analyze.js.map +1 -1
  5. package/dist/api/audit.d.ts +10 -0
  6. package/dist/api/audit.d.ts.map +1 -0
  7. package/dist/api/audit.js +117 -0
  8. package/dist/api/audit.js.map +1 -0
  9. package/dist/api/generate.d.ts.map +1 -1
  10. package/dist/api/generate.js +10 -1
  11. package/dist/api/generate.js.map +1 -1
  12. package/dist/api/index.d.ts +3 -2
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +1 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/api/run.d.ts.map +1 -1
  17. package/dist/api/run.js +5 -1
  18. package/dist/api/run.js.map +1 -1
  19. package/dist/api/types.d.ts +15 -4
  20. package/dist/api/types.d.ts.map +1 -1
  21. package/dist/cli/commands/analyze.d.ts +3 -0
  22. package/dist/cli/commands/analyze.d.ts.map +1 -1
  23. package/dist/cli/commands/analyze.js +112 -17
  24. package/dist/cli/commands/analyze.js.map +1 -1
  25. package/dist/cli/commands/audit.d.ts +9 -0
  26. package/dist/cli/commands/audit.d.ts.map +1 -0
  27. package/dist/cli/commands/audit.js +98 -0
  28. package/dist/cli/commands/audit.js.map +1 -0
  29. package/dist/cli/commands/drift.d.ts.map +1 -1
  30. package/dist/cli/commands/drift.js +8 -10
  31. package/dist/cli/commands/drift.js.map +1 -1
  32. package/dist/cli/commands/generate.d.ts.map +1 -1
  33. package/dist/cli/commands/generate.js +15 -37
  34. package/dist/cli/commands/generate.js.map +1 -1
  35. package/dist/cli/commands/mcp.d.ts +102 -2
  36. package/dist/cli/commands/mcp.d.ts.map +1 -1
  37. package/dist/cli/commands/mcp.js +134 -2
  38. package/dist/cli/commands/mcp.js.map +1 -1
  39. package/dist/cli/commands/run.d.ts.map +1 -1
  40. package/dist/cli/commands/run.js +9 -47
  41. package/dist/cli/commands/run.js.map +1 -1
  42. package/dist/cli/commands/setup.d.ts +17 -0
  43. package/dist/cli/commands/setup.d.ts.map +1 -0
  44. package/dist/cli/commands/setup.js +201 -0
  45. package/dist/cli/commands/setup.js.map +1 -0
  46. package/dist/cli/commands/verify.d.ts.map +1 -1
  47. package/dist/cli/commands/verify.js +7 -8
  48. package/dist/cli/commands/verify.js.map +1 -1
  49. package/dist/cli/index.js +14 -8
  50. package/dist/cli/index.js.map +1 -1
  51. package/dist/constants.d.ts +14 -0
  52. package/dist/constants.d.ts.map +1 -1
  53. package/dist/constants.js +14 -0
  54. package/dist/constants.js.map +1 -1
  55. package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
  56. package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
  57. package/dist/core/analyzer/ai-config-generator.js +85 -0
  58. package/dist/core/analyzer/ai-config-generator.js.map +1 -0
  59. package/dist/core/analyzer/artifact-generator.d.ts +27 -2
  60. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  61. package/dist/core/analyzer/artifact-generator.js +86 -8
  62. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  63. package/dist/core/analyzer/codebase-digest.d.ts.map +1 -1
  64. package/dist/core/analyzer/codebase-digest.js +12 -11
  65. package/dist/core/analyzer/codebase-digest.js.map +1 -1
  66. package/dist/core/analyzer/env-extractor.d.ts +33 -0
  67. package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
  68. package/dist/core/analyzer/env-extractor.js +196 -0
  69. package/dist/core/analyzer/env-extractor.js.map +1 -0
  70. package/dist/core/analyzer/http-route-parser.d.ts +36 -1
  71. package/dist/core/analyzer/http-route-parser.d.ts.map +1 -1
  72. package/dist/core/analyzer/http-route-parser.js +276 -0
  73. package/dist/core/analyzer/http-route-parser.js.map +1 -1
  74. package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
  75. package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
  76. package/dist/core/analyzer/middleware-extractor.js +195 -0
  77. package/dist/core/analyzer/middleware-extractor.js.map +1 -0
  78. package/dist/core/analyzer/schema-extractor.d.ts +41 -0
  79. package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
  80. package/dist/core/analyzer/schema-extractor.js +229 -0
  81. package/dist/core/analyzer/schema-extractor.js.map +1 -0
  82. package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
  83. package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
  84. package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
  85. package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
  86. package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
  87. package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
  88. package/dist/core/analyzer/ui-component-extractor.js +245 -0
  89. package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
  90. package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
  91. package/dist/core/generator/openspec-format-generator.js +8 -0
  92. package/dist/core/generator/openspec-format-generator.js.map +1 -1
  93. package/dist/core/generator/spec-pipeline.d.ts +9 -0
  94. package/dist/core/generator/spec-pipeline.d.ts.map +1 -1
  95. package/dist/core/generator/spec-pipeline.js +94 -2
  96. package/dist/core/generator/spec-pipeline.js.map +1 -1
  97. package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -1
  98. package/dist/core/generator/stages/stage1-survey.js +43 -0
  99. package/dist/core/generator/stages/stage1-survey.js.map +1 -1
  100. package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -1
  101. package/dist/core/generator/stages/stage2-entities.js +6 -2
  102. package/dist/core/generator/stages/stage2-entities.js.map +1 -1
  103. package/dist/core/generator/stages/stage3-services.d.ts.map +1 -1
  104. package/dist/core/generator/stages/stage3-services.js +9 -2
  105. package/dist/core/generator/stages/stage3-services.js.map +1 -1
  106. package/dist/core/generator/stages/stage4-api.d.ts.map +1 -1
  107. package/dist/core/generator/stages/stage4-api.js +6 -2
  108. package/dist/core/generator/stages/stage4-api.js.map +1 -1
  109. package/dist/core/services/llm-service.d.ts +26 -10
  110. package/dist/core/services/llm-service.d.ts.map +1 -1
  111. package/dist/core/services/llm-service.js +171 -16
  112. package/dist/core/services/llm-service.js.map +1 -1
  113. package/dist/core/services/mcp-handlers/analysis.d.ts +32 -1
  114. package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -1
  115. package/dist/core/services/mcp-handlers/analysis.js +185 -2
  116. package/dist/core/services/mcp-handlers/analysis.js.map +1 -1
  117. package/dist/core/verifier/verification-engine.d.ts +67 -6
  118. package/dist/core/verifier/verification-engine.d.ts.map +1 -1
  119. package/dist/core/verifier/verification-engine.js +316 -90
  120. package/dist/core/verifier/verification-engine.js.map +1 -1
  121. package/dist/types/index.d.ts +70 -1
  122. package/dist/types/index.d.ts.map +1 -1
  123. package/dist/types/pipeline.d.ts +9 -0
  124. package/dist/types/pipeline.d.ts.map +1 -1
  125. package/dist/utils/command-helpers.d.ts +30 -0
  126. package/dist/utils/command-helpers.d.ts.map +1 -1
  127. package/dist/utils/command-helpers.js +69 -1
  128. package/dist/utils/command-helpers.js.map +1 -1
  129. package/examples/bmad/README.md +113 -0
  130. package/examples/bmad/agents/architect.md +226 -0
  131. package/examples/bmad/agents/dev-brownfield.md +69 -0
  132. package/examples/bmad/setup/architect.customize.yaml +14 -0
  133. package/examples/bmad/tasks/implement-story.md +254 -0
  134. package/examples/bmad/tasks/onboarding.md +169 -0
  135. package/examples/bmad/tasks/refactor.md +178 -0
  136. package/examples/bmad/tasks/sprint-planning.md +168 -0
  137. package/examples/bmad/templates/story.md +108 -0
  138. package/examples/cline-workflows/spec-gen-analyze-codebase.md +100 -0
  139. package/examples/cline-workflows/spec-gen-check-spec-drift.md +102 -0
  140. package/examples/cline-workflows/spec-gen-execute-refactor.md +194 -0
  141. package/examples/cline-workflows/spec-gen-implement-feature.md +238 -0
  142. package/examples/cline-workflows/spec-gen-plan-refactor.md +255 -0
  143. package/examples/cline-workflows/spec-gen-refactor-codebase.md +16 -0
  144. package/examples/drift-demo/openspec/config.yaml +14 -0
  145. package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
  146. package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
  147. package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
  148. package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
  149. package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
  150. package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
  151. package/examples/drift-demo/package.json +21 -0
  152. package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
  153. package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
  154. package/examples/drift-demo/src/auth/auth-service.ts +45 -0
  155. package/examples/drift-demo/src/database/connection.ts +27 -0
  156. package/examples/drift-demo/src/index.ts +16 -0
  157. package/examples/drift-demo/src/projects/project-model.ts +15 -0
  158. package/examples/drift-demo/src/projects/project-service.ts +34 -0
  159. package/examples/drift-demo/src/tasks/task-model.ts +37 -0
  160. package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
  161. package/examples/drift-demo/src/tasks/task-service.ts +60 -0
  162. package/examples/drift-demo/src/utils/validation.ts +11 -0
  163. package/examples/drift-demo/tests/auth.test.ts +4 -0
  164. package/examples/drift-demo/tests/tasks.test.ts +4 -0
  165. package/examples/drift-demo/tsconfig.json +10 -0
  166. package/examples/drift-test/run-drift-test.sh +1087 -0
  167. package/examples/gsd/README.md +119 -0
  168. package/examples/gsd/commands/gsd/spec-gen-drift.md +111 -0
  169. package/examples/gsd/commands/gsd/spec-gen-orient.md +191 -0
  170. package/examples/mistral-vibe/README.md +101 -0
  171. package/examples/mistral-vibe/antipatterns-template.md +18 -0
  172. package/examples/mistral-vibe/skills/spec-gen-analyze-codebase/SKILL.md +123 -0
  173. package/examples/mistral-vibe/skills/spec-gen-brainstorm/SKILL.md +379 -0
  174. package/examples/mistral-vibe/skills/spec-gen-debug/SKILL.md +320 -0
  175. package/examples/mistral-vibe/skills/spec-gen-execute-refactor/SKILL.md +210 -0
  176. package/examples/mistral-vibe/skills/spec-gen-generate/SKILL.md +245 -0
  177. package/examples/mistral-vibe/skills/spec-gen-implement-story/SKILL.md +274 -0
  178. package/examples/mistral-vibe/skills/spec-gen-plan-refactor/SKILL.md +251 -0
  179. package/examples/openspec-analysis/README.md +59 -0
  180. package/examples/openspec-analysis/SUMMARY.md +72 -0
  181. package/examples/openspec-analysis/config.json +16 -0
  182. package/examples/openspec-analysis/dependencies.mermaid +35 -0
  183. package/examples/openspec-analysis/dependency-graph.json +12116 -0
  184. package/examples/openspec-analysis/llm-context.json +119 -0
  185. package/examples/openspec-analysis/repo-structure.json +871 -0
  186. package/examples/openspec-cli/README.md +67 -0
  187. package/examples/openspec-cli/openspec/config.yaml +26 -0
  188. package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
  189. package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
  190. package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
  191. package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
  192. package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
  193. package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
  194. package/examples/spec-kit/README.md +104 -0
  195. package/examples/spec-kit/commands/drift.md +87 -0
  196. package/examples/spec-kit/commands/orient.md +138 -0
  197. package/examples/spec-kit/extension.yml +54 -0
  198. package/package.json +3 -6
@@ -0,0 +1,1087 @@
1
+ #!/bin/bash
2
+ #
3
+ # End-to-end drift detection test
4
+ #
5
+ # Creates a temporary git repo with specs and source files, then simulates
6
+ # code changes that should be caught by spec-gen drift.
7
+ #
8
+ # Usage: ./run-drift-test.sh [path-to-spec-gen-binary]
9
+ #
10
+ # If no binary path is given, uses npx from the parent project.
11
+
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ SPECGEN="${1:-node ${SCRIPT_DIR}/../../dist/cli/index.js}"
16
+ TMPDIR_BASE="${TMPDIR:-/tmp}"
17
+ TEST_DIR="${TMPDIR_BASE}/spec-gen-drift-e2e-$$"
18
+
19
+ # Colors
20
+ RED='\033[0;31m'
21
+ GREEN='\033[0;32m'
22
+ YELLOW='\033[0;33m'
23
+ NC='\033[0m' # No Color
24
+
25
+ pass_count=0
26
+ fail_count=0
27
+
28
+ pass() {
29
+ echo -e " ${GREEN}✓${NC} $1"
30
+ pass_count=$((pass_count + 1))
31
+ }
32
+
33
+ fail() {
34
+ echo -e " ${RED}✗${NC} $1"
35
+ echo -e " ${RED}$2${NC}"
36
+ fail_count=$((fail_count + 1))
37
+ }
38
+
39
+ cleanup() {
40
+ rm -rf "$TEST_DIR"
41
+ }
42
+ trap cleanup EXIT
43
+
44
+ echo ""
45
+ echo "=== spec-gen drift: End-to-End Test ==="
46
+ echo "Test directory: $TEST_DIR"
47
+ echo ""
48
+
49
+ # ============================================================================
50
+ # SETUP: Create the brownfield repo with existing specs
51
+ # ============================================================================
52
+
53
+ echo "--- Setup: Creating brownfield repo ---"
54
+
55
+ mkdir -p "$TEST_DIR"
56
+ cd "$TEST_DIR"
57
+
58
+ # Init git
59
+ git init -b main > /dev/null 2>&1
60
+ git config user.email "test@example.com"
61
+ git config user.name "Test User"
62
+
63
+ # Create .spec-gen config
64
+ mkdir -p .spec-gen
65
+ cat > .spec-gen/config.json << 'EOF'
66
+ {
67
+ "version": "1.0.0",
68
+ "projectType": "nodejs",
69
+ "openspecPath": "openspec",
70
+ "analysis": { "maxFiles": 500, "includePatterns": [], "excludePatterns": [] },
71
+ "generation": { "model": "claude-sonnet-4-20250514", "domains": "auto" },
72
+ "createdAt": "2025-01-01T00:00:00.000Z",
73
+ "lastRun": "2025-01-15T00:00:00.000Z"
74
+ }
75
+ EOF
76
+
77
+ # Create source files
78
+ mkdir -p src/auth src/user src/payments src/utils
79
+
80
+ cat > src/auth/auth-service.ts << 'EOF'
81
+ export class AuthService {
82
+ async login(email: string, password: string) {
83
+ // Validate credentials
84
+ const user = await this.findUser(email);
85
+ if (!user || !this.checkPassword(password, user.hashedPassword)) {
86
+ throw new Error('Invalid credentials');
87
+ }
88
+ return this.generateToken(user);
89
+ }
90
+
91
+ async register(email: string, password: string) {
92
+ const existing = await this.findUser(email);
93
+ if (existing) throw new Error('User already exists');
94
+ return this.createUser(email, password);
95
+ }
96
+
97
+ private findUser(email: string) { return null; }
98
+ private checkPassword(pw: string, hash: string) { return true; }
99
+ private generateToken(user: any) { return 'token'; }
100
+ private createUser(email: string, pw: string) { return {}; }
101
+ }
102
+ EOF
103
+
104
+ cat > src/auth/jwt.ts << 'EOF'
105
+ export function signToken(payload: object): string {
106
+ return 'signed-token';
107
+ }
108
+
109
+ export function verifyToken(token: string): object | null {
110
+ return { userId: '123' };
111
+ }
112
+ EOF
113
+
114
+ cat > src/user/user-model.ts << 'EOF'
115
+ export interface User {
116
+ id: string;
117
+ email: string;
118
+ name: string;
119
+ createdAt: Date;
120
+ }
121
+
122
+ export interface UserProfile extends User {
123
+ bio?: string;
124
+ avatarUrl?: string;
125
+ }
126
+ EOF
127
+
128
+ cat > src/user/user-service.ts << 'EOF'
129
+ import { User } from './user-model';
130
+
131
+ export class UserService {
132
+ async getUser(id: string): Promise<User | null> {
133
+ return null;
134
+ }
135
+
136
+ async updateUser(id: string, data: Partial<User>): Promise<User> {
137
+ return {} as User;
138
+ }
139
+
140
+ async deleteUser(id: string): Promise<void> {
141
+ // soft delete
142
+ }
143
+ }
144
+ EOF
145
+
146
+ cat > src/payments/payment-handler.ts << 'EOF'
147
+ export class PaymentHandler {
148
+ async processPayment(amount: number, currency: string) {
149
+ if (amount <= 0) throw new Error('Invalid amount');
150
+ return { transactionId: 'tx_123', status: 'completed' };
151
+ }
152
+
153
+ async refund(transactionId: string) {
154
+ return { status: 'refunded' };
155
+ }
156
+ }
157
+ EOF
158
+
159
+ cat > src/utils/format.ts << 'EOF'
160
+ export function formatCurrency(amount: number, currency: string): string {
161
+ return `${currency} ${amount.toFixed(2)}`;
162
+ }
163
+ EOF
164
+
165
+ cat > package.json << 'EOF'
166
+ {
167
+ "name": "drift-test-app",
168
+ "version": "1.0.0",
169
+ "dependencies": {}
170
+ }
171
+ EOF
172
+
173
+ # Create OpenSpec specs (the "global spec")
174
+ mkdir -p openspec/specs/overview openspec/specs/auth openspec/specs/user openspec/specs/payments openspec/specs/architecture
175
+
176
+ cat > openspec/config.yaml << 'EOF'
177
+ schema: spec-driven
178
+ context: |
179
+ A sample Node.js application with authentication, user management, and payments.
180
+
181
+ Tech stack: TypeScript, Node.js
182
+ Architecture: Layered (service-oriented)
183
+
184
+ spec-gen:
185
+ generatedAt: "2025-01-15T00:00:00.000Z"
186
+ domains:
187
+ - auth
188
+ - user
189
+ - payments
190
+ EOF
191
+
192
+ cat > openspec/specs/overview/spec.md << 'EOF'
193
+ # System Overview
194
+
195
+ > Generated by spec-gen v1.0.0 on 2025-01-15
196
+
197
+ ## Purpose
198
+
199
+ A sample application with authentication, user management, and payment processing.
200
+
201
+ ## Domains
202
+
203
+ - **auth**: Authentication and authorization (login, register, JWT)
204
+ - **user**: User management (CRUD, profiles)
205
+ - **payments**: Payment processing (charges, refunds)
206
+ EOF
207
+
208
+ cat > openspec/specs/auth/spec.md << 'EOF'
209
+ # Auth Specification
210
+
211
+ > Generated by spec-gen v1.0.0 on 2025-01-15
212
+ > Source files: src/auth/auth-service.ts, src/auth/jwt.ts
213
+
214
+ ## Purpose
215
+
216
+ Manages authentication and authorization, including user login, registration, and JWT token management.
217
+
218
+ ## Requirements
219
+
220
+ ### Requirement: UserLogin
221
+
222
+ The system SHALL authenticate users via email and password, returning a JWT token on success.
223
+
224
+ #### Scenario: ValidLogin
225
+ - **GIVEN** a registered user with valid credentials
226
+ - **WHEN** the user submits their email and password
227
+ - **THEN** the system returns a signed JWT token
228
+
229
+ #### Scenario: InvalidCredentials
230
+ - **GIVEN** invalid credentials
231
+ - **WHEN** the user attempts to login
232
+ - **THEN** the system throws an 'Invalid credentials' error
233
+
234
+ ### Requirement: UserRegistration
235
+
236
+ The system SHALL allow new users to register with email and password.
237
+
238
+ #### Scenario: DuplicateEmail
239
+ - **GIVEN** an email that is already registered
240
+ - **WHEN** a registration is attempted
241
+ - **THEN** the system throws a 'User already exists' error
242
+
243
+ ### Requirement: JWTManagement
244
+
245
+ The system SHALL sign and verify JWT tokens for session management.
246
+
247
+ ## Technical Notes
248
+
249
+ - **Implementation**: `src/auth/auth-service.ts`, `src/auth/jwt.ts`
250
+ - **Dependencies**: user domain
251
+ EOF
252
+
253
+ cat > openspec/specs/user/spec.md << 'EOF'
254
+ # User Specification
255
+
256
+ > Generated by spec-gen v1.0.0 on 2025-01-15
257
+ > Source files: src/user/user-model.ts, src/user/user-service.ts
258
+
259
+ ## Purpose
260
+
261
+ Manages user data, profiles, and user lifecycle operations.
262
+
263
+ ## Requirements
264
+
265
+ ### Requirement: UserCRUD
266
+
267
+ The system SHALL support Create, Read, Update, and Delete operations for users.
268
+
269
+ #### Scenario: GetUser
270
+ - **GIVEN** a valid user ID
271
+ - **WHEN** the user is requested
272
+ - **THEN** the system returns the user record or null
273
+
274
+ #### Scenario: DeleteUser
275
+ - **GIVEN** a valid user ID
276
+ - **WHEN** the user is deleted
277
+ - **THEN** the system performs a soft delete
278
+
279
+ ### Requirement: UserProfile
280
+
281
+ The system SHALL support optional profile fields (bio, avatarUrl) via the UserProfile interface.
282
+
283
+ ## Technical Notes
284
+
285
+ - **Implementation**: `src/user/user-model.ts`, `src/user/user-service.ts`
286
+ - **Dependencies**: none
287
+ EOF
288
+
289
+ cat > openspec/specs/payments/spec.md << 'EOF'
290
+ # Payments Specification
291
+
292
+ > Generated by spec-gen v1.0.0 on 2025-01-15
293
+ > Source files: src/payments/payment-handler.ts
294
+
295
+ ## Purpose
296
+
297
+ Handles payment processing including charges and refunds.
298
+
299
+ ## Requirements
300
+
301
+ ### Requirement: ProcessPayment
302
+
303
+ The system SHALL process payments with amount and currency validation.
304
+
305
+ #### Scenario: ValidPayment
306
+ - **GIVEN** a positive amount and valid currency
307
+ - **WHEN** a payment is processed
308
+ - **THEN** the system returns a transaction ID with status 'completed'
309
+
310
+ #### Scenario: InvalidAmount
311
+ - **GIVEN** an amount of zero or negative
312
+ - **WHEN** a payment is attempted
313
+ - **THEN** the system throws an 'Invalid amount' error
314
+
315
+ ### Requirement: Refunds
316
+
317
+ The system SHALL support refunding a previously completed transaction.
318
+
319
+ ## Technical Notes
320
+
321
+ - **Implementation**: `src/payments/payment-handler.ts`
322
+ - **Dependencies**: none
323
+ EOF
324
+
325
+ cat > openspec/specs/architecture/spec.md << 'EOF'
326
+ # Architecture Specification
327
+
328
+ > Generated by spec-gen v1.0.0 on 2025-01-15
329
+
330
+ ## Purpose
331
+
332
+ Describes the system structure and architectural patterns.
333
+
334
+ ## Structure
335
+
336
+ The application follows a layered architecture:
337
+ - **Services**: Business logic (auth-service, user-service, payment-handler)
338
+ - **Models**: Data structures (user-model)
339
+ - **Utils**: Shared helpers (format)
340
+
341
+ ## Requirements
342
+
343
+ ### Requirement: LayeredArchitecture
344
+
345
+ The system SHALL separate concerns into service, model, and utility layers.
346
+
347
+ ## Technical Notes
348
+
349
+ - **Implementation**: `src/`
350
+ - **Dependencies**: none
351
+ EOF
352
+
353
+ # Commit the baseline
354
+ git add -A
355
+ git commit -m "Initial commit: baseline app with specs" > /dev/null 2>&1
356
+
357
+ pass "Brownfield repo created with baseline specs"
358
+
359
+ # ============================================================================
360
+ # TEST 1: No drift on clean baseline
361
+ # ============================================================================
362
+
363
+ echo ""
364
+ echo "--- Test 1: No drift on baseline (no changes) ---"
365
+
366
+ output=$($SPECGEN drift --json 2>&1) || true
367
+ total=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['total'])" 2>/dev/null || echo "FAIL")
368
+ has_drift=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['hasDrift'])" 2>/dev/null || echo "FAIL")
369
+
370
+ if [ "$total" = "0" ] && [ "$has_drift" = "False" ]; then
371
+ pass "No drift detected on clean baseline"
372
+ else
373
+ fail "Expected no drift on baseline" "Got total=$total, hasDrift=$has_drift"
374
+ fi
375
+
376
+ # ============================================================================
377
+ # TEST 2: Gap detection — modify source without updating spec
378
+ # ============================================================================
379
+
380
+ echo ""
381
+ echo "--- Test 2: Gap detection — source changed, spec not updated ---"
382
+
383
+ git checkout -b feature/add-mfa > /dev/null 2>&1
384
+
385
+ # Significantly modify auth-service without updating the auth spec
386
+ cat >> src/auth/auth-service.ts << 'APPEND'
387
+
388
+ async verifyMFA(userId: string, code: string): Promise<boolean> {
389
+ // New MFA verification logic
390
+ if (!code || code.length !== 6) {
391
+ throw new Error('Invalid MFA code');
392
+ }
393
+ const storedSecret = await this.getMFASecret(userId);
394
+ return this.validateTOTP(storedSecret, code);
395
+ }
396
+
397
+ async enableMFA(userId: string): Promise<{ secret: string; qrCode: string }> {
398
+ const secret = this.generateSecret();
399
+ await this.storeMFASecret(userId, secret);
400
+ return { secret, qrCode: this.generateQRCode(secret) };
401
+ }
402
+
403
+ private getMFASecret(userId: string) { return 'secret'; }
404
+ private validateTOTP(secret: string, code: string) { return true; }
405
+ private generateSecret() { return 'newsecret'; }
406
+ private storeMFASecret(userId: string, secret: string) { }
407
+ private generateQRCode(secret: string) { return 'qr://...'; }
408
+ APPEND
409
+
410
+ git add -A
411
+ git commit -m "Add MFA support to auth service" > /dev/null 2>&1
412
+
413
+ output=$($SPECGEN drift --json 2>&1) || true
414
+ gaps=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['gaps'])" 2>/dev/null || echo "FAIL")
415
+
416
+ if [ "$gaps" != "0" ] && [ "$gaps" != "FAIL" ]; then
417
+ pass "Gap detected: auth source changed, spec not updated (gaps=$gaps)"
418
+ else
419
+ fail "Expected gap for auth-service.ts change" "Got gaps=$gaps"
420
+ fi
421
+
422
+ # Reset to main for next test
423
+ git checkout main > /dev/null 2>&1
424
+
425
+ # ============================================================================
426
+ # TEST 3: Stale spec — delete a source file referenced by spec
427
+ # ============================================================================
428
+
429
+ echo ""
430
+ echo "--- Test 3: Stale spec — delete file referenced by spec ---"
431
+
432
+ git checkout -b feature/remove-jwt > /dev/null 2>&1
433
+
434
+ # Delete jwt.ts which is referenced in the auth spec
435
+ rm src/auth/jwt.ts
436
+ git add -A
437
+ git commit -m "Remove JWT module (switching to external lib)" > /dev/null 2>&1
438
+
439
+ output=$($SPECGEN drift --json 2>&1) || true
440
+ stale=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['stale'])" 2>/dev/null || echo "FAIL")
441
+
442
+ if [ "$stale" != "0" ] && [ "$stale" != "FAIL" ]; then
443
+ pass "Stale spec detected: jwt.ts deleted but still referenced (stale=$stale)"
444
+ else
445
+ fail "Expected stale issue for deleted jwt.ts" "Got stale=$stale"
446
+ fi
447
+
448
+ git checkout main > /dev/null 2>&1
449
+
450
+ # ============================================================================
451
+ # TEST 4: Uncovered file — add new file with no spec domain
452
+ # ============================================================================
453
+
454
+ echo ""
455
+ echo "--- Test 4: Uncovered file — new file with no spec domain ---"
456
+
457
+ git checkout -b feature/add-notifications > /dev/null 2>&1
458
+
459
+ # Add a new domain's files that have no spec coverage
460
+ mkdir -p src/notifications
461
+ cat > src/notifications/notification-service.ts << 'EOF'
462
+ export class NotificationService {
463
+ async sendEmail(to: string, subject: string, body: string) {
464
+ // Send email notification
465
+ return { sent: true };
466
+ }
467
+
468
+ async sendPush(userId: string, message: string) {
469
+ // Send push notification
470
+ return { sent: true };
471
+ }
472
+
473
+ async sendSMS(phone: string, message: string) {
474
+ // Send SMS notification
475
+ return { sent: true };
476
+ }
477
+ }
478
+ EOF
479
+
480
+ git add -A
481
+ git commit -m "Add notification service" > /dev/null 2>&1
482
+
483
+ output=$($SPECGEN drift --json 2>&1) || true
484
+ uncovered=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['uncovered'])" 2>/dev/null || echo "FAIL")
485
+
486
+ if [ "$uncovered" != "0" ] && [ "$uncovered" != "FAIL" ]; then
487
+ pass "Uncovered file detected: notification-service.ts has no spec (uncovered=$uncovered)"
488
+ else
489
+ fail "Expected uncovered issue for notification-service.ts" "Got uncovered=$uncovered"
490
+ fi
491
+
492
+ git checkout main > /dev/null 2>&1
493
+
494
+ # ============================================================================
495
+ # TEST 5: No drift when spec is updated alongside code
496
+ # ============================================================================
497
+
498
+ echo ""
499
+ echo "--- Test 5: No drift when code AND spec are both updated ---"
500
+
501
+ git checkout -b feature/update-payments > /dev/null 2>&1
502
+
503
+ # Modify payments source AND update the spec
504
+ cat >> src/payments/payment-handler.ts << 'APPEND'
505
+
506
+ async createSubscription(planId: string, userId: string) {
507
+ return { subscriptionId: 'sub_123', status: 'active' };
508
+ }
509
+ APPEND
510
+
511
+ # Also update the spec
512
+ cat >> openspec/specs/payments/spec.md << 'APPEND'
513
+
514
+ ### Requirement: Subscriptions
515
+
516
+ The system SHALL support creating recurring subscriptions.
517
+
518
+ #### Scenario: CreateSubscription
519
+ - **GIVEN** a valid plan ID and user ID
520
+ - **WHEN** a subscription is created
521
+ - **THEN** the system returns a subscription ID with status 'active'
522
+ APPEND
523
+
524
+ git add -A
525
+ git commit -m "Add subscription support with updated spec" > /dev/null 2>&1
526
+
527
+ output=$($SPECGEN drift --json 2>&1) || true
528
+ gaps=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['gaps'])" 2>/dev/null || echo "FAIL")
529
+
530
+ if [ "$gaps" = "0" ]; then
531
+ pass "No gap for payments: code and spec both updated"
532
+ else
533
+ fail "Expected no gap when spec was also updated" "Got gaps=$gaps"
534
+ fi
535
+
536
+ git checkout main > /dev/null 2>&1
537
+
538
+ # ============================================================================
539
+ # TEST 6: Domain filter — only show issues for specific domain
540
+ # ============================================================================
541
+
542
+ echo ""
543
+ echo "--- Test 6: Domain filter — only check specific domain ---"
544
+
545
+ git checkout -b feature/multi-domain-changes > /dev/null 2>&1
546
+
547
+ # Modify both auth AND user without updating either spec
548
+ echo "// auth change" >> src/auth/auth-service.ts
549
+ echo "// user change" >> src/user/user-service.ts
550
+
551
+ git add -A
552
+ git commit -m "Changes to both auth and user" > /dev/null 2>&1
553
+
554
+ output=$($SPECGEN drift --json --domains auth 2>&1) || true
555
+ all_auth=$(echo "$output" | python3 -c "
556
+ import sys, json
557
+ data = json.load(sys.stdin)
558
+ domains = [i['domain'] for i in data['issues']]
559
+ print('OK' if all(d == 'auth' for d in domains) else 'MIXED')
560
+ " 2>/dev/null || echo "FAIL")
561
+
562
+ if [ "$all_auth" = "OK" ]; then
563
+ pass "Domain filter works: only auth issues shown"
564
+ else
565
+ fail "Expected only auth domain issues" "Got: $all_auth"
566
+ fi
567
+
568
+ git checkout main > /dev/null 2>&1
569
+
570
+ # ============================================================================
571
+ # TEST 7: fail-on threshold — error only
572
+ # ============================================================================
573
+
574
+ echo ""
575
+ echo "--- Test 7: fail-on threshold ---"
576
+
577
+ git checkout -b feature/small-change > /dev/null 2>&1
578
+
579
+ # Small change (1 line) — should be info-level
580
+ echo "// minor fix" >> src/auth/jwt.ts
581
+
582
+ git add -A
583
+ git commit -m "Minor fix" > /dev/null 2>&1
584
+
585
+ # fail-on error: should NOT fail for info-level issues
586
+ exit_code_error=0
587
+ $SPECGEN drift --fail-on error > /dev/null 2>&1 || exit_code_error=$?
588
+
589
+ # fail-on info: SHOULD fail for info-level issues
590
+ exit_code_info=0
591
+ $SPECGEN drift --fail-on info > /dev/null 2>&1 || exit_code_info=$?
592
+
593
+ if [ "$exit_code_error" = "0" ]; then
594
+ pass "fail-on error: exits 0 for info-level issues"
595
+ else
596
+ fail "fail-on error: should exit 0 for info-level issues" "Got exit code $exit_code_error"
597
+ fi
598
+
599
+ if [ "$exit_code_info" != "0" ]; then
600
+ pass "fail-on info: exits non-zero for info-level issues"
601
+ else
602
+ fail "fail-on info: should exit non-zero for info-level issues" "Got exit code $exit_code_info"
603
+ fi
604
+
605
+ git checkout main > /dev/null 2>&1
606
+
607
+ # ============================================================================
608
+ # TEST 8: JSON output is valid JSON
609
+ # ============================================================================
610
+
611
+ echo ""
612
+ echo "--- Test 8: JSON output validity ---"
613
+
614
+ git checkout -b feature/json-test > /dev/null 2>&1
615
+ echo "// change" >> src/auth/auth-service.ts
616
+ git add -A
617
+ git commit -m "Test change" > /dev/null 2>&1
618
+
619
+ output=$($SPECGEN drift --json 2>&1) || true
620
+ valid_json=$(echo "$output" | python3 -c "
621
+ import sys, json
622
+ try:
623
+ data = json.load(sys.stdin)
624
+ required = ['timestamp', 'baseRef', 'totalChangedFiles', 'specRelevantFiles', 'issues', 'summary', 'hasDrift', 'duration', 'mode']
625
+ if all(k in data for k in required):
626
+ print('VALID')
627
+ else:
628
+ print('MISSING_KEYS')
629
+ except:
630
+ print('INVALID')
631
+ " 2>/dev/null || echo "FAIL")
632
+
633
+ if [ "$valid_json" = "VALID" ]; then
634
+ pass "JSON output is valid with all required fields"
635
+ else
636
+ fail "JSON output validation failed" "Got: $valid_json"
637
+ fi
638
+
639
+ git checkout main > /dev/null 2>&1
640
+
641
+ # ============================================================================
642
+ # TEST 9: Hook install/uninstall
643
+ # ============================================================================
644
+
645
+ echo ""
646
+ echo "--- Test 9: Pre-commit hook management ---"
647
+
648
+ $SPECGEN drift --install-hook 2>&1 > /dev/null || true
649
+
650
+ if [ -f .git/hooks/pre-commit ] && grep -q "spec-gen-drift-hook" .git/hooks/pre-commit; then
651
+ pass "Pre-commit hook installed with marker"
652
+ else
653
+ fail "Pre-commit hook not installed" "Expected .git/hooks/pre-commit with spec-gen marker"
654
+ fi
655
+
656
+ $SPECGEN drift --uninstall-hook 2>&1 > /dev/null || true
657
+
658
+ if [ ! -f .git/hooks/pre-commit ] || ! grep -q "spec-gen-drift-hook" .git/hooks/pre-commit 2>/dev/null; then
659
+ pass "Pre-commit hook uninstalled"
660
+ else
661
+ fail "Pre-commit hook not uninstalled" "Marker still present in .git/hooks/pre-commit"
662
+ fi
663
+
664
+ # ============================================================================
665
+ # TEST 10: Verbose output includes detailed messages
666
+ # ============================================================================
667
+
668
+ echo ""
669
+ echo "--- Test 10: Verbose output ---"
670
+
671
+ git checkout -b feature/verbose-test > /dev/null 2>&1
672
+ cat >> src/auth/auth-service.ts << 'APPEND'
673
+ // More significant changes for verbose testing
674
+ export function newAuthHelper() { return true; }
675
+ export function anotherAuthHelper() { return false; }
676
+ APPEND
677
+
678
+ git add -A
679
+ git commit -m "More auth changes" > /dev/null 2>&1
680
+
681
+ output=$($SPECGEN drift --verbose 2>&1) || true
682
+
683
+ if echo "$output" | grep -qi "suggestion\|review\|spec"; then
684
+ pass "Verbose output includes detailed information"
685
+ else
686
+ # The verbose flag shows suggestions inline, check for basic output
687
+ if echo "$output" | grep -qi "gap\|drift\|auth"; then
688
+ pass "Verbose output includes issue details"
689
+ else
690
+ fail "Verbose output missing details" "Output: $(echo "$output" | head -5)"
691
+ fi
692
+ fi
693
+
694
+ git checkout main > /dev/null 2>&1
695
+
696
+ # ============================================================================
697
+ # TEST 11: --max-files limits analyzed files
698
+ # ============================================================================
699
+
700
+ echo ""
701
+ echo "--- Test 11: --max-files limits analyzed files ---"
702
+
703
+ git checkout -b feature/max-files-test > /dev/null 2>&1
704
+
705
+ # Make changes to 3 different files
706
+ echo "// auth change for max-files" >> src/auth/auth-service.ts
707
+ echo "// user change for max-files" >> src/user/user-service.ts
708
+ echo "// payment change for max-files" >> src/payments/payment-handler.ts
709
+
710
+ git add -A
711
+ git commit -m "Changes to 3 files" > /dev/null 2>&1
712
+
713
+ output=$($SPECGEN drift --max-files 1 --json 2>&1) || true
714
+ total_files=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['totalChangedFiles'])" 2>/dev/null || echo "FAIL")
715
+ issue_count=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['total'])" 2>/dev/null || echo "FAIL")
716
+
717
+ # totalChangedFiles should report ALL changed files (3), but issues should be limited
718
+ # because only 1 file was analyzed due to --max-files
719
+ if [ "$total_files" = "3" ] && [ "$issue_count" -le 1 ]; then
720
+ pass "--max-files 1 limits to 1 analyzed file (total=3, issues=$issue_count)"
721
+ else
722
+ fail "Expected 3 total files but limited analysis" "Got totalChangedFiles=$total_files, issues=$issue_count"
723
+ fi
724
+
725
+ git checkout main > /dev/null 2>&1
726
+
727
+ # ============================================================================
728
+ # TEST 12: --files filter to specific files
729
+ # ============================================================================
730
+
731
+ echo ""
732
+ echo "--- Test 12: --files filter to specific files ---"
733
+
734
+ git checkout -b feature/files-filter-test > /dev/null 2>&1
735
+
736
+ echo "// auth change for filter" >> src/auth/auth-service.ts
737
+ echo "// user change for filter" >> src/user/user-service.ts
738
+
739
+ git add -A
740
+ git commit -m "Changes to auth and user" > /dev/null 2>&1
741
+
742
+ output=$($SPECGEN drift --files src/auth/auth-service.ts --json 2>&1) || true
743
+ total_files=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['totalChangedFiles'])" 2>/dev/null || echo "FAIL")
744
+
745
+ if [ "$total_files" = "1" ]; then
746
+ pass "--files filters to specific file"
747
+ else
748
+ fail "Expected 1 file with --files filter" "Got totalChangedFiles=$total_files"
749
+ fi
750
+
751
+ git checkout main > /dev/null 2>&1
752
+
753
+ # ============================================================================
754
+ # TEST 13: Orphaned spec detection
755
+ # ============================================================================
756
+
757
+ echo ""
758
+ echo "--- Test 13: Orphaned spec detection ---"
759
+
760
+ git checkout -b feature/orphaned-test > /dev/null 2>&1
761
+
762
+ # Delete jwt.ts (referenced in auth spec) and make another change
763
+ rm src/auth/jwt.ts
764
+ echo "// trigger change" >> src/auth/auth-service.ts
765
+
766
+ git add -A
767
+ git commit -m "Remove jwt.ts" > /dev/null 2>&1
768
+
769
+ output=$($SPECGEN drift --json 2>&1) || true
770
+ orphaned=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['summary']['orphanedSpecs'])" 2>/dev/null || echo "FAIL")
771
+
772
+ if [ "$orphaned" != "0" ] && [ "$orphaned" != "FAIL" ]; then
773
+ pass "Orphaned spec detected: jwt.ts no longer on disk (orphaned=$orphaned)"
774
+ else
775
+ fail "Expected orphaned spec for deleted jwt.ts" "Got orphanedSpecs=$orphaned"
776
+ fi
777
+
778
+ git checkout main > /dev/null 2>&1
779
+
780
+ # ============================================================================
781
+ # TEST 14: --base explicit ref
782
+ # ============================================================================
783
+
784
+ echo ""
785
+ echo "--- Test 14: --base with explicit commit ref ---"
786
+
787
+ git checkout -b feature/base-test > /dev/null 2>&1
788
+
789
+ echo "// first change" >> src/auth/auth-service.ts
790
+ git add -A
791
+ git commit -m "First change" > /dev/null 2>&1
792
+
793
+ MIDPOINT_SHA=$(git rev-parse HEAD)
794
+
795
+ echo "// second change" >> src/auth/auth-service.ts
796
+ git add -A
797
+ git commit -m "Second change" > /dev/null 2>&1
798
+
799
+ # Compare against midpoint (only second change)
800
+ output=$($SPECGEN drift --base "$MIDPOINT_SHA" --json 2>&1) || true
801
+ total_files=$(echo "$output" | python3 -c "import sys,json; print(json.load(sys.stdin)['totalChangedFiles'])" 2>/dev/null || echo "FAIL")
802
+
803
+ if [ "$total_files" = "1" ]; then
804
+ pass "--base with SHA limits comparison to one commit of changes"
805
+ else
806
+ fail "Expected 1 changed file from midpoint" "Got totalChangedFiles=$total_files"
807
+ fi
808
+
809
+ git checkout main > /dev/null 2>&1
810
+
811
+ # ============================================================================
812
+ # TEST 15: CHANGELOG.md / README.md should not cause false positives
813
+ # ============================================================================
814
+
815
+ echo ""
816
+ echo "--- Test 15: Markdown files should not trigger drift ---"
817
+
818
+ git checkout -b feature/md-test > /dev/null 2>&1
819
+
820
+ echo "# v2.0 Release" >> CHANGELOG.md
821
+ echo "# Updated README" >> README.md
822
+ echo "# Contributing Guide" > CONTRIBUTING.md
823
+ # Also add a real code change to have something to compare
824
+ echo "// source change" >> src/auth/auth-service.ts
825
+
826
+ git add -A
827
+ git commit -m "Update docs and auth" > /dev/null 2>&1
828
+
829
+ output=$($SPECGEN drift --json 2>&1) || true
830
+ # CHANGELOG, README, CONTRIBUTING should NOT appear as issues
831
+ has_changelog=$(echo "$output" | python3 -c "
832
+ import sys, json
833
+ data = json.load(sys.stdin)
834
+ md_issues = [i for i in data['issues'] if i['filePath'].endswith('.md')]
835
+ print('CLEAN' if len(md_issues) == 0 else 'DIRTY')
836
+ " 2>/dev/null || echo "FAIL")
837
+
838
+ if [ "$has_changelog" = "CLEAN" ]; then
839
+ pass "CHANGELOG.md/README.md/CONTRIBUTING.md not flagged as drift"
840
+ else
841
+ fail "Markdown files incorrectly flagged as drift" "Got: $has_changelog"
842
+ fi
843
+
844
+ git checkout main > /dev/null 2>&1
845
+
846
+ # ============================================================================
847
+ # TEST 16: Quiet mode should produce minimal output
848
+ # ============================================================================
849
+
850
+ echo ""
851
+ echo "--- Test 16: Quiet mode minimal output ---"
852
+
853
+ git checkout -b feature/quiet-test > /dev/null 2>&1
854
+
855
+ cat >> src/auth/auth-service.ts << 'APPEND'
856
+ export function newBigFeature() {
857
+ // 30+ lines of significant change
858
+ const data = Array.from({ length: 30 }, (_, i) => `item_${i}`);
859
+ return data.filter(d => d.startsWith('item_1'));
860
+ }
861
+ APPEND
862
+
863
+ git add -A
864
+ git commit -m "Add big feature" > /dev/null 2>&1
865
+
866
+ output=$($SPECGEN -q drift 2>&1) || true
867
+ line_count=$(echo "$output" | wc -l | tr -d ' ')
868
+
869
+ # Quiet mode should produce very few lines (just the summary line)
870
+ if [ "$line_count" -le 3 ]; then
871
+ pass "Quiet mode produces minimal output ($line_count lines)"
872
+ else
873
+ fail "Quiet mode too verbose" "Got $line_count lines: $(echo "$output" | head -3)"
874
+ fi
875
+
876
+ git checkout main > /dev/null 2>&1
877
+
878
+ # ============================================================================
879
+ # TEST 17: Pre-commit hook blocks real commit
880
+ # ============================================================================
881
+
882
+ echo ""
883
+ echo "--- Test 17: Pre-commit hook blocks commit with drift ---"
884
+
885
+ git checkout -b feature/hook-test > /dev/null 2>&1
886
+
887
+ # Install the hook
888
+ $SPECGEN drift --install-hook 2>&1 > /dev/null || true
889
+
890
+ # Make a code change without updating spec
891
+ echo "// hook test change" >> src/auth/auth-service.ts
892
+ git add -A
893
+
894
+ # Try to commit — hook should block it
895
+ hook_commit_result=0
896
+ git commit -m "This should be blocked" > /dev/null 2>&1 || hook_commit_result=$?
897
+
898
+ if [ "$hook_commit_result" -ne 0 ]; then
899
+ pass "Pre-commit hook blocked commit with drift (exit code $hook_commit_result)"
900
+ else
901
+ fail "Pre-commit hook did not block commit" "Commit succeeded when it should have failed"
902
+ fi
903
+
904
+ # Verify no commit was made
905
+ latest_msg=$(git log -1 --format=%s)
906
+ if [ "$latest_msg" != "This should be blocked" ]; then
907
+ pass "No commit was created (last commit: '$latest_msg')"
908
+ else
909
+ fail "Commit was created despite hook" "Found commit: $latest_msg"
910
+ fi
911
+
912
+ # Uninstall hook for remaining tests
913
+ $SPECGEN drift --uninstall-hook 2>&1 > /dev/null || true
914
+
915
+ # Commit with --no-verify to clean up
916
+ git commit -m "Hook test change" --no-verify > /dev/null 2>&1
917
+
918
+ git checkout main > /dev/null 2>&1
919
+
920
+ # ============================================================================
921
+ # TEST 18: Multiple domains changed simultaneously
922
+ # ============================================================================
923
+
924
+ echo ""
925
+ echo "--- Test 18: Multiple domains changed simultaneously ---"
926
+
927
+ git checkout -b feature/multi-domain > /dev/null 2>&1
928
+
929
+ echo "// auth change" >> src/auth/auth-service.ts
930
+ echo "// user change" >> src/user/user-service.ts
931
+ echo "// payment change" >> src/payments/payment-handler.ts
932
+
933
+ git add -A
934
+ git commit -m "Changes across three domains" > /dev/null 2>&1
935
+
936
+ output=$($SPECGEN drift --json 2>&1) || true
937
+ domains_hit=$(echo "$output" | python3 -c "
938
+ import sys, json
939
+ data = json.load(sys.stdin)
940
+ domains = set(i['domain'] for i in data['issues'] if i['domain'])
941
+ print(len(domains))
942
+ " 2>/dev/null || echo "FAIL")
943
+
944
+ if [ "$domains_hit" = "3" ]; then
945
+ pass "Detected drift across all 3 domains"
946
+ elif [ "$domains_hit" != "FAIL" ] && [ "$domains_hit" -ge 2 ]; then
947
+ pass "Detected drift across $domains_hit domains"
948
+ else
949
+ fail "Expected drift across multiple domains" "Got domains=$domains_hit"
950
+ fi
951
+
952
+ git checkout main > /dev/null 2>&1
953
+
954
+ # ============================================================================
955
+ # TEST 19: Binary/generated files should be ignored
956
+ # ============================================================================
957
+
958
+ echo ""
959
+ echo "--- Test 19: Binary and generated files ignored ---"
960
+
961
+ git checkout -b feature/binary-test > /dev/null 2>&1
962
+
963
+ # Create "binary" files that should be skipped
964
+ echo "fake binary content" > logo.png
965
+ echo "fake font" > font.woff2
966
+ mkdir -p src/types
967
+ echo "// generated" > src/types/index.d.ts
968
+
969
+ git add -A
970
+ git commit -m "Add binary and generated files" > /dev/null 2>&1
971
+
972
+ output=$($SPECGEN drift --json 2>&1) || true
973
+ binary_issues=$(echo "$output" | python3 -c "
974
+ import sys, json
975
+ data = json.load(sys.stdin)
976
+ bad = [i for i in data['issues'] if i['filePath'].endswith(('.png', '.woff2', '.d.ts'))]
977
+ print(len(bad))
978
+ " 2>/dev/null || echo "FAIL")
979
+
980
+ if [ "$binary_issues" = "0" ]; then
981
+ pass "Binary and generated files correctly ignored"
982
+ else
983
+ fail "Binary/generated files incorrectly flagged" "Got $binary_issues issues for binary files"
984
+ fi
985
+
986
+ git checkout main > /dev/null 2>&1
987
+
988
+ # ============================================================================
989
+ # TEST 20: Info-level severity with --fail-on info
990
+ # ============================================================================
991
+
992
+ echo ""
993
+ echo "--- Test 20: Info-level issues with --fail-on info ---"
994
+
995
+ git checkout -b feature/info-test > /dev/null 2>&1
996
+
997
+ # Small change (1-2 lines on a spec-covered file) should be info-level gap
998
+ # jwt.ts is in the auth spec; a 1-line comment = 1 addition = info severity
999
+ echo "// minor fix" >> src/auth/jwt.ts
1000
+
1001
+ git add -A
1002
+ git commit -m "Tiny jwt change" > /dev/null 2>&1
1003
+
1004
+ # Verify it detects info-level severity
1005
+ output=$($SPECGEN drift --json 2>&1) || true
1006
+ severity=$(echo "$output" | python3 -c "
1007
+ import sys, json
1008
+ data = json.load(sys.stdin)
1009
+ sevs = [i['severity'] for i in data['issues'] if i['kind'] == 'gap']
1010
+ print(sevs[0] if sevs else 'NONE')
1011
+ " 2>/dev/null || echo "FAIL")
1012
+
1013
+ exit_code_info=0
1014
+ $SPECGEN drift --fail-on info > /dev/null 2>&1 || exit_code_info=$?
1015
+
1016
+ exit_code_error=0
1017
+ $SPECGEN drift --fail-on error > /dev/null 2>&1 || exit_code_error=$?
1018
+
1019
+ # info-level gap: fail-on info should exit 1, fail-on error should exit 0
1020
+ if [ "$severity" = "info" ] && [ "$exit_code_info" -ne 0 ] && [ "$exit_code_error" -eq 0 ]; then
1021
+ pass "Info-level gap: --fail-on info exits 1, --fail-on error exits 0 (severity=$severity)"
1022
+ elif [ "$exit_code_info" -ne 0 ] && [ "$exit_code_error" -eq 0 ]; then
1023
+ pass "Threshold works: --fail-on info exits 1, --fail-on error exits 0 (severity=$severity)"
1024
+ else
1025
+ fail "Info-level threshold not working" "severity=$severity, fail-on info=$exit_code_info, fail-on error=$exit_code_error"
1026
+ fi
1027
+
1028
+ git checkout main > /dev/null 2>&1
1029
+
1030
+ echo "--- Test 21: --use-llm without API key shows clear error ---"
1031
+ git checkout feature/add-mfa > /dev/null 2>&1
1032
+
1033
+ # Ensure no API keys are set for this test
1034
+ unset ANTHROPIC_API_KEY 2>/dev/null || true
1035
+ unset OPENAI_API_KEY 2>/dev/null || true
1036
+
1037
+ output=$(eval $SPECGEN drift --use-llm 2>&1) || true
1038
+ exit_code=$?
1039
+
1040
+ if echo "$output" | grep -q "No LLM API key found"; then
1041
+ pass "--use-llm without API key shows clear error message"
1042
+ else
1043
+ fail "--use-llm should show API key error" "output: $output"
1044
+ fi
1045
+
1046
+ if echo "$output" | grep -q "ANTHROPIC_API_KEY\|OPENAI_API_KEY"; then
1047
+ pass "--use-llm error includes env var names"
1048
+ else
1049
+ fail "--use-llm error should mention env var names" "output: $output"
1050
+ fi
1051
+
1052
+ git checkout main > /dev/null 2>&1
1053
+
1054
+ echo "--- Test 22: --use-llm with --json without API key exits non-zero ---"
1055
+ git checkout feature/add-mfa > /dev/null 2>&1
1056
+
1057
+ unset ANTHROPIC_API_KEY 2>/dev/null || true
1058
+ unset OPENAI_API_KEY 2>/dev/null || true
1059
+
1060
+ if eval $SPECGEN drift --use-llm --json > /dev/null 2>&1; then
1061
+ fail "--use-llm --json should exit non-zero without API key" "exit_code=0"
1062
+ else
1063
+ pass "--use-llm --json without API key exits non-zero"
1064
+ fi
1065
+
1066
+ git checkout main > /dev/null 2>&1
1067
+
1068
+ # ============================================================================
1069
+ # RESULTS
1070
+ # ============================================================================
1071
+
1072
+ echo ""
1073
+ echo "=== Results ==="
1074
+ echo -e " ${GREEN}Passed: $pass_count${NC}"
1075
+ if [ "$fail_count" -gt 0 ]; then
1076
+ echo -e " ${RED}Failed: $fail_count${NC}"
1077
+ else
1078
+ echo -e " Failed: 0"
1079
+ fi
1080
+ echo ""
1081
+
1082
+ if [ "$fail_count" -gt 0 ]; then
1083
+ exit 1
1084
+ else
1085
+ echo -e "${GREEN}All drift detection tests passed!${NC}"
1086
+ exit 0
1087
+ fi