@dzhechkov/skills-idea2prd 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +71 -0
  3. package/bin/cli.js +5 -0
  4. package/package.json +49 -0
  5. package/sources.json +25 -0
  6. package/src/cli.js +108 -0
  7. package/src/commands/doctor.js +340 -0
  8. package/src/commands/init.js +168 -0
  9. package/src/commands/list.js +146 -0
  10. package/src/commands/remove.js +182 -0
  11. package/src/commands/update.js +170 -0
  12. package/src/utils.js +154 -0
  13. package/templates/.claude/commands/idea2prd-manual.md +35 -0
  14. package/templates/.claude/skills/explore/SKILL.md +218 -0
  15. package/templates/.claude/skills/explore/references/questioning-techniques.md +151 -0
  16. package/templates/.claude/skills/explore/references/task-brief-templates.md +355 -0
  17. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +418 -0
  18. package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +658 -0
  19. package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +544 -0
  20. package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +560 -0
  21. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +662 -0
  22. package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +720 -0
  23. package/templates/.claude/skills/idea2prd-manual/SKILL.md +695 -0
  24. package/templates/.claude/skills/idea2prd-manual/references/adr-catalog.md +288 -0
  25. package/templates/.claude/skills/idea2prd-manual/references/c4-model.md +277 -0
  26. package/templates/.claude/skills/idea2prd-manual/references/completion-checklist-template.md +446 -0
  27. package/templates/.claude/skills/idea2prd-manual/references/ddd-patterns.md +261 -0
  28. package/templates/.claude/skills/idea2prd-manual/references/fitness-functions-catalog.md +414 -0
  29. package/templates/.claude/skills/idea2prd-manual/references/pseudocode-style.md +404 -0
  30. package/templates/.claude/skills/idea2prd-manual/scripts/ai_context_builder.py +491 -0
  31. package/templates/.claude/skills/idea2prd-manual/scripts/c4_generator.py +311 -0
  32. package/templates/.claude/skills/idea2prd-manual/scripts/fitness_validator.py +451 -0
  33. package/templates/.claude/skills/idea2prd-manual/scripts/pseudocode_generator.py +430 -0
  34. package/templates/.claude/skills/problem-solver-enhanced/SKILL.md +565 -0
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Architecture Fitness Functions Validator for idea2prd skills.
4
+ Validates architecture compliance against defined fitness functions.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import List, Dict, Callable, Optional, Any
9
+ from enum import Enum
10
+ from datetime import datetime
11
+ import json
12
+ import re
13
+
14
+
15
+ class FitnessCategory(Enum):
16
+ STRUCTURAL = "structural"
17
+ ADR_COMPLIANCE = "adr_compliance"
18
+ PERFORMANCE = "performance"
19
+ SECURITY = "security"
20
+ DATA_INTEGRITY = "data_integrity"
21
+ OPERATIONAL = "operational"
22
+
23
+
24
+ class FitnessStatus(Enum):
25
+ PASS = "pass"
26
+ FAIL = "fail"
27
+ WARN = "warn"
28
+ SKIP = "skip"
29
+
30
+
31
+ @dataclass
32
+ class FitnessResult:
33
+ """Result of a single fitness function evaluation."""
34
+ function_id: str
35
+ name: str
36
+ category: FitnessCategory
37
+ status: FitnessStatus
38
+ current_value: Any
39
+ threshold: Any
40
+ message: str
41
+ details: Dict = field(default_factory=dict)
42
+ timestamp: datetime = field(default_factory=datetime.now)
43
+
44
+ def to_dict(self) -> dict:
45
+ return {
46
+ "id": self.function_id,
47
+ "name": self.name,
48
+ "category": self.category.value,
49
+ "status": self.status.value,
50
+ "current": self.current_value,
51
+ "threshold": self.threshold,
52
+ "message": self.message,
53
+ "details": self.details,
54
+ "timestamp": self.timestamp.isoformat()
55
+ }
56
+
57
+
58
+ @dataclass
59
+ class FitnessFunction:
60
+ """Definition of an architecture fitness function."""
61
+ id: str
62
+ name: str
63
+ category: FitnessCategory
64
+ description: str
65
+ rule: str
66
+ threshold: Any
67
+ validator: Callable[..., FitnessResult]
68
+ related_adr: Optional[str] = None
69
+ related_nfr: Optional[str] = None
70
+ frequency: str = "every_commit"
71
+ tools: List[str] = field(default_factory=list)
72
+
73
+ def validate(self, context: Dict) -> FitnessResult:
74
+ """Execute the fitness function validation."""
75
+ return self.validator(self, context)
76
+
77
+
78
+ class FitnessFunctionRegistry:
79
+ """Registry for managing fitness functions."""
80
+
81
+ def __init__(self):
82
+ self.functions: Dict[str, FitnessFunction] = {}
83
+
84
+ def register(self, func: FitnessFunction):
85
+ """Register a fitness function."""
86
+ self.functions[func.id] = func
87
+
88
+ def get(self, func_id: str) -> Optional[FitnessFunction]:
89
+ """Get a fitness function by ID."""
90
+ return self.functions.get(func_id)
91
+
92
+ def get_by_category(self, category: FitnessCategory) -> List[FitnessFunction]:
93
+ """Get all fitness functions in a category."""
94
+ return [f for f in self.functions.values() if f.category == category]
95
+
96
+ def validate_all(self, context: Dict) -> List[FitnessResult]:
97
+ """Run all registered fitness functions."""
98
+ results = []
99
+ for func in self.functions.values():
100
+ try:
101
+ result = func.validate(context)
102
+ results.append(result)
103
+ except Exception as e:
104
+ results.append(FitnessResult(
105
+ function_id=func.id,
106
+ name=func.name,
107
+ category=func.category,
108
+ status=FitnessStatus.FAIL,
109
+ current_value=None,
110
+ threshold=func.threshold,
111
+ message=f"Validation error: {str(e)}",
112
+ details={"error": str(e)}
113
+ ))
114
+ return results
115
+
116
+
117
+ # =============================================================================
118
+ # Standard Fitness Function Validators
119
+ # =============================================================================
120
+
121
+ def validate_bounded_context_independence(func: FitnessFunction, context: Dict) -> FitnessResult:
122
+ """
123
+ FF-001: Validate that bounded contexts don't directly access each other's databases.
124
+ """
125
+ violations = context.get("cross_context_db_access", [])
126
+ violation_count = len(violations)
127
+
128
+ return FitnessResult(
129
+ function_id=func.id,
130
+ name=func.name,
131
+ category=func.category,
132
+ status=FitnessStatus.PASS if violation_count == 0 else FitnessStatus.FAIL,
133
+ current_value=violation_count,
134
+ threshold=func.threshold,
135
+ message=f"Found {violation_count} cross-context DB access violations" if violations else "No violations found",
136
+ details={"violations": violations}
137
+ )
138
+
139
+
140
+ def validate_aggregate_size(func: FitnessFunction, context: Dict) -> FitnessResult:
141
+ """
142
+ FF-002: Validate that aggregates don't exceed entity limit.
143
+ """
144
+ aggregates = context.get("aggregates", {})
145
+ violations = []
146
+
147
+ for agg_name, entity_count in aggregates.items():
148
+ if entity_count > func.threshold:
149
+ violations.append({
150
+ "aggregate": agg_name,
151
+ "entity_count": entity_count,
152
+ "limit": func.threshold
153
+ })
154
+
155
+ return FitnessResult(
156
+ function_id=func.id,
157
+ name=func.name,
158
+ category=func.category,
159
+ status=FitnessStatus.PASS if not violations else FitnessStatus.FAIL,
160
+ current_value=max(aggregates.values()) if aggregates else 0,
161
+ threshold=func.threshold,
162
+ message=f"{len(violations)} aggregates exceed size limit" if violations else "All aggregates within limits",
163
+ details={"violations": violations, "aggregates": aggregates}
164
+ )
165
+
166
+
167
+ def validate_api_compliance(func: FitnessFunction, context: Dict) -> FitnessResult:
168
+ """
169
+ FF-003: Validate API endpoints follow defined style (REST/GraphQL).
170
+ """
171
+ total_endpoints = context.get("total_endpoints", 0)
172
+ compliant_endpoints = context.get("compliant_endpoints", 0)
173
+
174
+ if total_endpoints == 0:
175
+ return FitnessResult(
176
+ function_id=func.id,
177
+ name=func.name,
178
+ category=func.category,
179
+ status=FitnessStatus.SKIP,
180
+ current_value=0,
181
+ threshold=func.threshold,
182
+ message="No endpoints to validate"
183
+ )
184
+
185
+ compliance_rate = (compliant_endpoints / total_endpoints) * 100
186
+
187
+ return FitnessResult(
188
+ function_id=func.id,
189
+ name=func.name,
190
+ category=func.category,
191
+ status=FitnessStatus.PASS if compliance_rate >= func.threshold else FitnessStatus.FAIL,
192
+ current_value=compliance_rate,
193
+ threshold=func.threshold,
194
+ message=f"API compliance: {compliance_rate:.1f}%",
195
+ details={
196
+ "total_endpoints": total_endpoints,
197
+ "compliant_endpoints": compliant_endpoints,
198
+ "non_compliant": context.get("non_compliant_endpoints", [])
199
+ }
200
+ )
201
+
202
+
203
+ def validate_auth_coverage(func: FitnessFunction, context: Dict) -> FitnessResult:
204
+ """
205
+ FF-004: Validate all non-public endpoints require authentication.
206
+ """
207
+ protected_endpoints = context.get("protected_endpoints", 0)
208
+ total_private_endpoints = context.get("total_private_endpoints", 0)
209
+
210
+ if total_private_endpoints == 0:
211
+ return FitnessResult(
212
+ function_id=func.id,
213
+ name=func.name,
214
+ category=func.category,
215
+ status=FitnessStatus.SKIP,
216
+ current_value=100,
217
+ threshold=func.threshold,
218
+ message="No private endpoints to validate"
219
+ )
220
+
221
+ coverage = (protected_endpoints / total_private_endpoints) * 100
222
+
223
+ return FitnessResult(
224
+ function_id=func.id,
225
+ name=func.name,
226
+ category=func.category,
227
+ status=FitnessStatus.PASS if coverage >= func.threshold else FitnessStatus.FAIL,
228
+ current_value=coverage,
229
+ threshold=func.threshold,
230
+ message=f"Auth coverage: {coverage:.1f}%",
231
+ details={
232
+ "protected": protected_endpoints,
233
+ "total_private": total_private_endpoints,
234
+ "unprotected": context.get("unprotected_endpoints", [])
235
+ }
236
+ )
237
+
238
+
239
+ def validate_test_coverage(func: FitnessFunction, context: Dict) -> FitnessResult:
240
+ """
241
+ FF-005: Validate test coverage meets threshold.
242
+ """
243
+ coverage = context.get("test_coverage", 0)
244
+
245
+ return FitnessResult(
246
+ function_id=func.id,
247
+ name=func.name,
248
+ category=func.category,
249
+ status=FitnessStatus.PASS if coverage >= func.threshold else FitnessStatus.FAIL,
250
+ current_value=coverage,
251
+ threshold=func.threshold,
252
+ message=f"Test coverage: {coverage:.1f}%",
253
+ details=context.get("coverage_details", {})
254
+ )
255
+
256
+
257
+ def validate_dependency_direction(func: FitnessFunction, context: Dict) -> FitnessResult:
258
+ """
259
+ FF-006: Validate dependencies flow inward (Infrastructure → Application → Domain).
260
+ """
261
+ violations = context.get("dependency_violations", [])
262
+
263
+ return FitnessResult(
264
+ function_id=func.id,
265
+ name=func.name,
266
+ category=func.category,
267
+ status=FitnessStatus.PASS if not violations else FitnessStatus.FAIL,
268
+ current_value=len(violations),
269
+ threshold=func.threshold,
270
+ message=f"Found {len(violations)} dependency direction violations" if violations else "Dependencies flow correctly",
271
+ details={"violations": violations}
272
+ )
273
+
274
+
275
+ # =============================================================================
276
+ # Fitness Function Factory
277
+ # =============================================================================
278
+
279
+ def create_standard_fitness_functions() -> FitnessFunctionRegistry:
280
+ """Create a registry with standard fitness functions."""
281
+ registry = FitnessFunctionRegistry()
282
+
283
+ # FF-001: Bounded Context Independence
284
+ registry.register(FitnessFunction(
285
+ id="FF-001",
286
+ name="Bounded Context Independence",
287
+ category=FitnessCategory.STRUCTURAL,
288
+ description="No direct database access across bounded contexts",
289
+ rule="Bounded contexts must communicate via events or APIs, not direct DB access",
290
+ threshold=0,
291
+ validator=validate_bounded_context_independence,
292
+ tools=["dependency-cruiser", "custom linter"]
293
+ ))
294
+
295
+ # FF-002: Aggregate Size Limit
296
+ registry.register(FitnessFunction(
297
+ id="FF-002",
298
+ name="Aggregate Size Limit",
299
+ category=FitnessCategory.STRUCTURAL,
300
+ description="Aggregates should have ≤7 entities",
301
+ rule="Each aggregate must contain no more than 7 entities",
302
+ threshold=7,
303
+ validator=validate_aggregate_size,
304
+ tools=["AST analysis", "custom script"]
305
+ ))
306
+
307
+ # FF-003: API Style Compliance
308
+ registry.register(FitnessFunction(
309
+ id="FF-003",
310
+ name="API Style Compliance",
311
+ category=FitnessCategory.ADR_COMPLIANCE,
312
+ description="All endpoints follow defined API style",
313
+ rule="100% of endpoints must comply with chosen API style (REST/GraphQL)",
314
+ threshold=100,
315
+ validator=validate_api_compliance,
316
+ related_adr="ADR-003",
317
+ tools=["Spectral", "GraphQL linter"]
318
+ ))
319
+
320
+ # FF-004: Authentication Coverage
321
+ registry.register(FitnessFunction(
322
+ id="FF-004",
323
+ name="Authentication Coverage",
324
+ category=FitnessCategory.SECURITY,
325
+ description="All non-public endpoints require authentication",
326
+ rule="100% of private endpoints must require authentication",
327
+ threshold=100,
328
+ validator=validate_auth_coverage,
329
+ related_adr="ADR-004",
330
+ tools=["custom security scan", "OWASP ZAP"]
331
+ ))
332
+
333
+ # FF-005: Test Coverage
334
+ registry.register(FitnessFunction(
335
+ id="FF-005",
336
+ name="Test Coverage",
337
+ category=FitnessCategory.OPERATIONAL,
338
+ description="Code coverage meets minimum threshold",
339
+ rule="Test coverage must be ≥80%",
340
+ threshold=80,
341
+ validator=validate_test_coverage,
342
+ related_nfr="NFR-T01",
343
+ tools=["Jest", "c8", "Istanbul"]
344
+ ))
345
+
346
+ # FF-006: Dependency Direction
347
+ registry.register(FitnessFunction(
348
+ id="FF-006",
349
+ name="Dependency Direction",
350
+ category=FitnessCategory.STRUCTURAL,
351
+ description="Dependencies flow inward following clean architecture",
352
+ rule="Infrastructure → Application → Domain (no reverse dependencies)",
353
+ threshold=0,
354
+ validator=validate_dependency_direction,
355
+ tools=["dependency-cruiser", "madge"]
356
+ ))
357
+
358
+ return registry
359
+
360
+
361
+ # =============================================================================
362
+ # Report Generator
363
+ # =============================================================================
364
+
365
+ def generate_fitness_report(results: List[FitnessResult]) -> str:
366
+ """Generate a markdown fitness report."""
367
+ lines = [
368
+ "# Architecture Fitness Report",
369
+ "",
370
+ f"**Generated:** {datetime.now().isoformat()}",
371
+ "",
372
+ "## Summary",
373
+ "",
374
+ "| Status | Count |",
375
+ "|--------|-------|"
376
+ ]
377
+
378
+ # Count by status
379
+ status_counts = {}
380
+ for result in results:
381
+ status = result.status.value
382
+ status_counts[status] = status_counts.get(status, 0) + 1
383
+
384
+ for status, count in status_counts.items():
385
+ emoji = {"pass": "✅", "fail": "🔴", "warn": "⚠️", "skip": "⏭️"}.get(status, "")
386
+ lines.append(f"| {emoji} {status.upper()} | {count} |")
387
+
388
+ lines.extend(["", "## Details", ""])
389
+
390
+ # Group by category
391
+ by_category = {}
392
+ for result in results:
393
+ cat = result.category.value
394
+ if cat not in by_category:
395
+ by_category[cat] = []
396
+ by_category[cat].append(result)
397
+
398
+ for category, cat_results in by_category.items():
399
+ lines.append(f"### {category.replace('_', ' ').title()}")
400
+ lines.append("")
401
+
402
+ for result in cat_results:
403
+ emoji = {"pass": "✅", "fail": "🔴", "warn": "⚠️", "skip": "⏭️"}.get(result.status.value, "")
404
+ lines.append(f"#### {emoji} {result.function_id}: {result.name}")
405
+ lines.append("")
406
+ lines.append(f"- **Status:** {result.status.value.upper()}")
407
+ lines.append(f"- **Current:** {result.current_value}")
408
+ lines.append(f"- **Threshold:** {result.threshold}")
409
+ lines.append(f"- **Message:** {result.message}")
410
+ lines.append("")
411
+
412
+ return "\n".join(lines)
413
+
414
+
415
+ # =============================================================================
416
+ # Main / Example Usage
417
+ # =============================================================================
418
+
419
+ if __name__ == "__main__":
420
+ # Create registry with standard functions
421
+ registry = create_standard_fitness_functions()
422
+
423
+ # Example context (would come from actual analysis)
424
+ context = {
425
+ "cross_context_db_access": [],
426
+ "aggregates": {
427
+ "User": 3,
428
+ "Order": 5,
429
+ "Product": 2
430
+ },
431
+ "total_endpoints": 20,
432
+ "compliant_endpoints": 19,
433
+ "non_compliant_endpoints": ["/legacy/endpoint"],
434
+ "protected_endpoints": 18,
435
+ "total_private_endpoints": 18,
436
+ "test_coverage": 85.5,
437
+ "dependency_violations": []
438
+ }
439
+
440
+ # Run all validations
441
+ results = registry.validate_all(context)
442
+
443
+ # Generate report
444
+ report = generate_fitness_report(results)
445
+ print(report)
446
+
447
+ # Also output as JSON for programmatic use
448
+ print("\n" + "=" * 60)
449
+ print("JSON Output:")
450
+ print("=" * 60)
451
+ print(json.dumps([r.to_dict() for r in results], indent=2, default=str))