@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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/bin/cli.js +5 -0
- package/package.json +49 -0
- package/sources.json +25 -0
- package/src/cli.js +108 -0
- package/src/commands/doctor.js +340 -0
- package/src/commands/init.js +168 -0
- package/src/commands/list.js +146 -0
- package/src/commands/remove.js +182 -0
- package/src/commands/update.js +170 -0
- package/src/utils.js +154 -0
- package/templates/.claude/commands/idea2prd-manual.md +35 -0
- package/templates/.claude/skills/explore/SKILL.md +218 -0
- package/templates/.claude/skills/explore/references/questioning-techniques.md +151 -0
- package/templates/.claude/skills/explore/references/task-brief-templates.md +355 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +418 -0
- package/templates/.claude/skills/goap-research-ed25519/references/ed25519-verification.md +658 -0
- package/templates/.claude/skills/goap-research-ed25519/references/research-actions.md +544 -0
- package/templates/.claude/skills/goap-research-ed25519/references/source-evaluation.md +560 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +662 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/goap_planner.py +720 -0
- package/templates/.claude/skills/idea2prd-manual/SKILL.md +695 -0
- package/templates/.claude/skills/idea2prd-manual/references/adr-catalog.md +288 -0
- package/templates/.claude/skills/idea2prd-manual/references/c4-model.md +277 -0
- package/templates/.claude/skills/idea2prd-manual/references/completion-checklist-template.md +446 -0
- package/templates/.claude/skills/idea2prd-manual/references/ddd-patterns.md +261 -0
- package/templates/.claude/skills/idea2prd-manual/references/fitness-functions-catalog.md +414 -0
- package/templates/.claude/skills/idea2prd-manual/references/pseudocode-style.md +404 -0
- package/templates/.claude/skills/idea2prd-manual/scripts/ai_context_builder.py +491 -0
- package/templates/.claude/skills/idea2prd-manual/scripts/c4_generator.py +311 -0
- package/templates/.claude/skills/idea2prd-manual/scripts/fitness_validator.py +451 -0
- package/templates/.claude/skills/idea2prd-manual/scripts/pseudocode_generator.py +430 -0
- 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))
|