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.
- package/README.md +175 -55
- package/dist/api/analyze.d.ts.map +1 -1
- package/dist/api/analyze.js +6 -1
- package/dist/api/analyze.js.map +1 -1
- package/dist/api/audit.d.ts +10 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +117 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/generate.d.ts.map +1 -1
- package/dist/api/generate.js +10 -1
- package/dist/api/generate.js.map +1 -1
- package/dist/api/index.d.ts +3 -2
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/run.d.ts.map +1 -1
- package/dist/api/run.js +5 -1
- package/dist/api/run.js.map +1 -1
- package/dist/api/types.d.ts +15 -4
- package/dist/api/types.d.ts.map +1 -1
- package/dist/cli/commands/analyze.d.ts +3 -0
- package/dist/cli/commands/analyze.d.ts.map +1 -1
- package/dist/cli/commands/analyze.js +112 -17
- package/dist/cli/commands/analyze.js.map +1 -1
- package/dist/cli/commands/audit.d.ts +9 -0
- package/dist/cli/commands/audit.d.ts.map +1 -0
- package/dist/cli/commands/audit.js +98 -0
- package/dist/cli/commands/audit.js.map +1 -0
- package/dist/cli/commands/drift.d.ts.map +1 -1
- package/dist/cli/commands/drift.js +8 -10
- package/dist/cli/commands/drift.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +15 -37
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts +102 -2
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +134 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +9 -47
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/setup.d.ts +17 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +201 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/verify.d.ts.map +1 -1
- package/dist/cli/commands/verify.js +7 -8
- package/dist/cli/commands/verify.js.map +1 -1
- package/dist/cli/index.js +14 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +14 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/analyzer/ai-config-generator.d.ts +54 -0
- package/dist/core/analyzer/ai-config-generator.d.ts.map +1 -0
- package/dist/core/analyzer/ai-config-generator.js +85 -0
- package/dist/core/analyzer/ai-config-generator.js.map +1 -0
- package/dist/core/analyzer/artifact-generator.d.ts +27 -2
- package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
- package/dist/core/analyzer/artifact-generator.js +86 -8
- package/dist/core/analyzer/artifact-generator.js.map +1 -1
- package/dist/core/analyzer/codebase-digest.d.ts.map +1 -1
- package/dist/core/analyzer/codebase-digest.js +12 -11
- package/dist/core/analyzer/codebase-digest.js.map +1 -1
- package/dist/core/analyzer/env-extractor.d.ts +33 -0
- package/dist/core/analyzer/env-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/env-extractor.js +196 -0
- package/dist/core/analyzer/env-extractor.js.map +1 -0
- package/dist/core/analyzer/http-route-parser.d.ts +36 -1
- package/dist/core/analyzer/http-route-parser.d.ts.map +1 -1
- package/dist/core/analyzer/http-route-parser.js +276 -0
- package/dist/core/analyzer/http-route-parser.js.map +1 -1
- package/dist/core/analyzer/middleware-extractor.d.ts +29 -0
- package/dist/core/analyzer/middleware-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/middleware-extractor.js +195 -0
- package/dist/core/analyzer/middleware-extractor.js.map +1 -0
- package/dist/core/analyzer/schema-extractor.d.ts +41 -0
- package/dist/core/analyzer/schema-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/schema-extractor.js +229 -0
- package/dist/core/analyzer/schema-extractor.js.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts +17 -0
- package/dist/core/analyzer/spec-snapshot-generator.d.ts.map +1 -0
- package/dist/core/analyzer/spec-snapshot-generator.js +201 -0
- package/dist/core/analyzer/spec-snapshot-generator.js.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts +43 -0
- package/dist/core/analyzer/ui-component-extractor.d.ts.map +1 -0
- package/dist/core/analyzer/ui-component-extractor.js +245 -0
- package/dist/core/analyzer/ui-component-extractor.js.map +1 -0
- package/dist/core/generator/openspec-format-generator.d.ts.map +1 -1
- package/dist/core/generator/openspec-format-generator.js +8 -0
- package/dist/core/generator/openspec-format-generator.js.map +1 -1
- package/dist/core/generator/spec-pipeline.d.ts +9 -0
- package/dist/core/generator/spec-pipeline.d.ts.map +1 -1
- package/dist/core/generator/spec-pipeline.js +94 -2
- package/dist/core/generator/spec-pipeline.js.map +1 -1
- package/dist/core/generator/stages/stage1-survey.d.ts.map +1 -1
- package/dist/core/generator/stages/stage1-survey.js +43 -0
- package/dist/core/generator/stages/stage1-survey.js.map +1 -1
- package/dist/core/generator/stages/stage2-entities.d.ts.map +1 -1
- package/dist/core/generator/stages/stage2-entities.js +6 -2
- package/dist/core/generator/stages/stage2-entities.js.map +1 -1
- package/dist/core/generator/stages/stage3-services.d.ts.map +1 -1
- package/dist/core/generator/stages/stage3-services.js +9 -2
- package/dist/core/generator/stages/stage3-services.js.map +1 -1
- package/dist/core/generator/stages/stage4-api.d.ts.map +1 -1
- package/dist/core/generator/stages/stage4-api.js +6 -2
- package/dist/core/generator/stages/stage4-api.js.map +1 -1
- package/dist/core/services/llm-service.d.ts +26 -10
- package/dist/core/services/llm-service.d.ts.map +1 -1
- package/dist/core/services/llm-service.js +171 -16
- package/dist/core/services/llm-service.js.map +1 -1
- package/dist/core/services/mcp-handlers/analysis.d.ts +32 -1
- package/dist/core/services/mcp-handlers/analysis.d.ts.map +1 -1
- package/dist/core/services/mcp-handlers/analysis.js +185 -2
- package/dist/core/services/mcp-handlers/analysis.js.map +1 -1
- package/dist/core/verifier/verification-engine.d.ts +67 -6
- package/dist/core/verifier/verification-engine.d.ts.map +1 -1
- package/dist/core/verifier/verification-engine.js +316 -90
- package/dist/core/verifier/verification-engine.js.map +1 -1
- package/dist/types/index.d.ts +70 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/pipeline.d.ts +9 -0
- package/dist/types/pipeline.d.ts.map +1 -1
- package/dist/utils/command-helpers.d.ts +30 -0
- package/dist/utils/command-helpers.d.ts.map +1 -1
- package/dist/utils/command-helpers.js +69 -1
- package/dist/utils/command-helpers.js.map +1 -1
- package/examples/bmad/README.md +113 -0
- package/examples/bmad/agents/architect.md +226 -0
- package/examples/bmad/agents/dev-brownfield.md +69 -0
- package/examples/bmad/setup/architect.customize.yaml +14 -0
- package/examples/bmad/tasks/implement-story.md +254 -0
- package/examples/bmad/tasks/onboarding.md +169 -0
- package/examples/bmad/tasks/refactor.md +178 -0
- package/examples/bmad/tasks/sprint-planning.md +168 -0
- package/examples/bmad/templates/story.md +108 -0
- package/examples/cline-workflows/spec-gen-analyze-codebase.md +100 -0
- package/examples/cline-workflows/spec-gen-check-spec-drift.md +102 -0
- package/examples/cline-workflows/spec-gen-execute-refactor.md +194 -0
- package/examples/cline-workflows/spec-gen-implement-feature.md +238 -0
- package/examples/cline-workflows/spec-gen-plan-refactor.md +255 -0
- package/examples/cline-workflows/spec-gen-refactor-codebase.md +16 -0
- package/examples/drift-demo/openspec/config.yaml +14 -0
- package/examples/drift-demo/openspec/specs/architecture/spec.md +30 -0
- package/examples/drift-demo/openspec/specs/auth/spec.md +71 -0
- package/examples/drift-demo/openspec/specs/database/spec.md +33 -0
- package/examples/drift-demo/openspec/specs/overview/spec.md +20 -0
- package/examples/drift-demo/openspec/specs/projects/spec.md +55 -0
- package/examples/drift-demo/openspec/specs/tasks/spec.md +78 -0
- package/examples/drift-demo/package.json +21 -0
- package/examples/drift-demo/src/auth/auth-middleware.ts +30 -0
- package/examples/drift-demo/src/auth/auth-routes.ts +29 -0
- package/examples/drift-demo/src/auth/auth-service.ts +45 -0
- package/examples/drift-demo/src/database/connection.ts +27 -0
- package/examples/drift-demo/src/index.ts +16 -0
- package/examples/drift-demo/src/projects/project-model.ts +15 -0
- package/examples/drift-demo/src/projects/project-service.ts +34 -0
- package/examples/drift-demo/src/tasks/task-model.ts +37 -0
- package/examples/drift-demo/src/tasks/task-routes.ts +53 -0
- package/examples/drift-demo/src/tasks/task-service.ts +60 -0
- package/examples/drift-demo/src/utils/validation.ts +11 -0
- package/examples/drift-demo/tests/auth.test.ts +4 -0
- package/examples/drift-demo/tests/tasks.test.ts +4 -0
- package/examples/drift-demo/tsconfig.json +10 -0
- package/examples/drift-test/run-drift-test.sh +1087 -0
- package/examples/gsd/README.md +119 -0
- package/examples/gsd/commands/gsd/spec-gen-drift.md +111 -0
- package/examples/gsd/commands/gsd/spec-gen-orient.md +191 -0
- package/examples/mistral-vibe/README.md +101 -0
- package/examples/mistral-vibe/antipatterns-template.md +18 -0
- package/examples/mistral-vibe/skills/spec-gen-analyze-codebase/SKILL.md +123 -0
- package/examples/mistral-vibe/skills/spec-gen-brainstorm/SKILL.md +379 -0
- package/examples/mistral-vibe/skills/spec-gen-debug/SKILL.md +320 -0
- package/examples/mistral-vibe/skills/spec-gen-execute-refactor/SKILL.md +210 -0
- package/examples/mistral-vibe/skills/spec-gen-generate/SKILL.md +245 -0
- package/examples/mistral-vibe/skills/spec-gen-implement-story/SKILL.md +274 -0
- package/examples/mistral-vibe/skills/spec-gen-plan-refactor/SKILL.md +251 -0
- package/examples/openspec-analysis/README.md +59 -0
- package/examples/openspec-analysis/SUMMARY.md +72 -0
- package/examples/openspec-analysis/config.json +16 -0
- package/examples/openspec-analysis/dependencies.mermaid +35 -0
- package/examples/openspec-analysis/dependency-graph.json +12116 -0
- package/examples/openspec-analysis/llm-context.json +119 -0
- package/examples/openspec-analysis/repo-structure.json +871 -0
- package/examples/openspec-cli/README.md +67 -0
- package/examples/openspec-cli/openspec/config.yaml +26 -0
- package/examples/openspec-cli/openspec/specs/architecture/spec.md +178 -0
- package/examples/openspec-cli/openspec/specs/artifact-graph/spec.md +143 -0
- package/examples/openspec-cli/openspec/specs/cli/spec.md +138 -0
- package/examples/openspec-cli/openspec/specs/overview/spec.md +60 -0
- package/examples/openspec-cli/openspec/specs/parsing/spec.md +123 -0
- package/examples/openspec-cli/openspec/specs/validation/spec.md +108 -0
- package/examples/spec-kit/README.md +104 -0
- package/examples/spec-kit/commands/drift.md +87 -0
- package/examples/spec-kit/commands/orient.md +138 -0
- package/examples/spec-kit/extension.yml +54 -0
- 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
|