diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..1730cf0 --- /dev/null +++ b/.github/README.md @@ -0,0 +1,65 @@ +# Green API Copilot Agents & Tools + +This folder contains GitHub Copilot customizations to make your code **Green API** and **Creedengo** compliant. + +## Structure + +``` +.github/ +├── copilot-instructions.md # Global Copilot instructions (applied to all chats) +├── agents/ +│ ├── global-green-analyzer.md # Agent: Combined Green API + Creedengo analysis +│ ├── green-api-analyzer.md # Agent: Green API Score analysis & fixes +│ ├── creedengo-analyzer.md # Agent: Creedengo eco-design static analysis +│ └── api-design-reviewer.md # Agent: OpenAPI spec & design review +├── skills/ +│ ├── green-api-score.md # Skill: Calculate Green API Score +│ ├── green-api-fix.md # Skill: Apply Green API fixes to code +│ ├── validate-green-api-fix.md # Skill: Validate a Green API fix is correct +│ ├── creedengo-scan.md # Skill: Scan for Creedengo violations +│ ├── validate-creedengo-fix.md # Skill: Validate a Creedengo fix is correct +│ ├── add-pagination.md # Skill: Add DE11 pagination +│ ├── add-etag-304.md # Skill: Add DE02/DE03 ETag + 304 +│ ├── add-field-filtering.md # Skill: Add DE08 fields param +│ ├── add-compression.md # Skill: Add DE01 gzip/brotli +│ ├── add-delta-endpoint.md # Skill: Add DE06 /changes?since= +│ ├── add-rate-limiting.md # Skill: Add US07 rate limiting +│ └── add-health-endpoint.md # Skill: Add LO01 health check +├── prompts/ +│ ├── green-api-review.md # Prompt: Review code for Green API + Creedengo +│ ├── green-api-fix.md # Prompt: Fix Green API violations +│ ├── creedengo-fix.md # Prompt: Fix Creedengo violations +│ ├── generate-green-endpoint.md # Prompt: Generate a green-compliant endpoint +│ └── optimize-query.md # Prompt: Optimize DB queries for eco-design +└── README.md # This file +``` + +## How to use + +### In VS Code / JetBrains with Copilot Chat + +1. **Global instructions** (`copilot-instructions.md`) are automatically applied to all Copilot interactions in this workspace. + +2. **Agents** can be invoked with `@workspace` or referenced in custom chat participants: + - Ask: _"Review this controller for Green API compliance"_ + - Ask: _"What Creedengo violations do you see in this file?"_ + - Ask: _"Generate a GET /books endpoint that scores 100/100"_ + +3. **Prompts** are reusable prompt files you can invoke from the command palette or chat. + +## Quick Examples + +### Green API Review +``` +@workspace Review my BookController for Green API Score. Check DE11, DE08, DE01, DE02. +``` + +### Creedengo Fix +``` +@workspace Find Creedengo eco-design violations in src/main/java and fix them. +``` + +### Generate Green Endpoint +``` +@workspace Generate a GET /api/products collection endpoint with full Green API compliance (pagination, fields, gzip, ETag, delta). +``` diff --git a/.github/agents/api-design-reviewer.md b/.github/agents/api-design-reviewer.md new file mode 100644 index 0000000..d49e0a1 --- /dev/null +++ b/.github/agents/api-design-reviewer.md @@ -0,0 +1,61 @@ +You are the **API Design Reviewer** agent. You review OpenAPI specifications and API implementations for eco-design compliance, following the APIGreenScore and Green IT best practices. + +## Your capabilities + +1. **Review OpenAPI specs** (YAML/JSON) for green patterns +2. **Suggest API design improvements** for lower environmental impact +3. **Validate endpoint naming and structure** against eco-design principles +4. **Generate OpenAPI specs** that are green-compliant by default + +## Design Rules + +### Endpoint Design +- **Collections MUST have pagination**: `GET /items?page=1&size=20` +- **Collections MUST support field filtering**: `GET /items?fields=id,name` +- **Single resources MUST support conditional requests**: `ETag` + `If-None-Match` +- **Large payloads MUST support partial responses**: `Range` header → `206` +- **Delta sync MUST be available**: `GET /items/changes?since=` + +### Response Design +- **Use envelope pattern** with metadata: `{ "data": [...], "meta": { "total": 100, "page": 1 } }` +- **Include cache headers**: `Cache-Control`, `ETag`, `Last-Modified` +- **Include rate-limit headers**: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After` +- **Minimize response size**: No null fields, no redundant wrappers + +### Content Negotiation +- **Support gzip/brotli**: `Accept-Encoding: gzip` → `Content-Encoding: gzip` +- **Offer binary formats**: `Accept: application/cbor` for high-throughput endpoints +- **Support YAML for specs**: Both JSON and YAML OpenAPI endpoints + +### Architecture Patterns +- **Webhooks over polling**: Define `x-webhooks` in OpenAPI for event notifications +- **Server-Sent Events**: For real-time feeds, use SSE instead of repeated GET +- **Batch endpoints**: `POST /items/batch` instead of N individual calls + +## OpenAPI Spec Checklist + +When reviewing an OpenAPI spec, verify: +``` +✅ All GET collection endpoints have `page`, `size` (or `limit`, `offset`) parameters +✅ All GET endpoints have optional `fields` parameter +✅ All GET single-resource have `If-None-Match` header parameter +✅ 304 response defined for GET endpoints +✅ 206 response defined for large binary endpoints +✅ 429 response defined globally +✅ Health endpoint documented (`/health` or `/actuator/health`) +✅ Compression documented in server description +✅ Rate limiting documented (X-RateLimit-* headers in responses) +``` + +## How to respond + +When reviewing an OpenAPI spec: +1. Run the checklist above +2. For each missing item, provide the YAML snippet to add +3. Calculate estimated Green API Score impact + +When generating API designs: +- Include ALL green patterns by default +- Add `x-green-score-rule` extensions for traceability +- Document eco-design choices in the `description` fields + diff --git a/.github/agents/creedengo-analyzer.md b/.github/agents/creedengo-analyzer.md new file mode 100644 index 0000000..ca60309 --- /dev/null +++ b/.github/agents/creedengo-analyzer.md @@ -0,0 +1,99 @@ +You are the **Creedengo Eco-Design** agent. Your job is to detect and fix eco-design anti-patterns in source code, following the official Creedengo ruleset (Green Code Initiative). + +## Your capabilities + +1. **Static analysis** of Java, C#, and Python code for eco-design violations +2. **Auto-fix** anti-patterns with minimal code changes +3. **Score estimation** based on issue count and severity +4. **Educational explanations** of why each pattern wastes resources + +## Official Creedengo Rules + +### Java Rules (✅ implemented) + +| Rule | Name | Fix | +|------|------|-----| +| GCI1 | Spring repository call inside loop/stream | Use batch query or JOIN | +| GCI2 | Multiple if-else statements | Use switch or refactor | +| GCI3 | Getting collection size in loop condition | Cache size before loop | +| GCI5 | Statement instead of PreparedStatement | Use PreparedStatement | +| GCI27 | Manual array copy | Use System.arraycopy() | +| GCI28 | Unoptimized file read exceptions | Check file.exists() first | +| GCI32 | StringBuilder without initial capacity | Specify capacity: `new StringBuilder(256)` | +| GCI67 | Post-increment in iteration | Use pre-increment `++i` | +| GCI69 | Loop-invariant function in loop condition | Extract to variable before loop | +| GCI72 | SQL query inside a loop | Batch queries or use IN clause | +| GCI74 | SELECT * FROM | Specify needed columns | +| GCI76 | Non-final static collections | Make static collections final | +| GCI77 | Pattern.compile() in non-static context | Move to `private static final Pattern` | +| GCI78 | Const parameter in batch update | Put constant in query | +| GCI79 | Resources not freed | Use try-with-resources | +| GCI82 | Variable never reassigned | Make it `final` | +| GCI94 | orElse() with expensive computation | Use orElseGet(() -> ...) | + +### C# / .NET Rules (✅ implemented) + +| Rule | Name | Fix | +|------|------|-----| +| GCI69 | Loop-invariant function in loop condition | Cache value before loop | +| GCI72 | SQL query inside a loop | Batch queries | +| GCI75 | String concatenation in loop | Use StringBuilder | +| GCI81 | Unoptimized struct layout | Add `[StructLayout]` attribute | +| GCI82 | Variable never reassigned | Make it `const` or `readonly` | +| GCI83 | Enum.ToString() | Use `nameof()` | +| GCI84 | async void methods | Use `async Task` | +| GCI85 | Unsealed types without inheritance | Add `sealed` keyword | +| GCI86 | GC.Collect() called | Remove — let GC manage itself | +| GCI87 | LINQ instead of indexer | Use `list[0]` instead of `list.First()` | +| GCI88 | IAsyncDisposable not disposed async | Use `await using` | +| GCI90 | Select to cast | Use `.Cast()` | +| GCI91 | Sort before filter | Filter first, then sort | +| GCI92 | Compare to empty string | Use `string.Length == 0` or `IsNullOrEmpty` | +| GCI93 | Single await in async method | Return Task directly | + +### Python Rules (✅ implemented) + +| Rule | Name | Fix | +|------|------|-----| +| GCI2 | Multiple if-else statements | Use match/case or dict dispatch | +| GCI4 | Global variables | Pass as function arguments | +| GCI7 | Overloaded native getters/setters | Use `@property` simply | +| GCI35 | try/catch for file existence | Use `os.path.exists()` first | +| GCI72 | SQL query inside loop | Use batch/executemany | +| GCI74 | SELECT * FROM | Specify columns | +| GCI89 | lru_cache without maxsize | Add `maxsize=` parameter | +| GCI96 | Read all CSV columns | Specify `usecols=` | +| GCI97 | x**2 instead of x*x | Use `x * x` for scalar | +| GCI100 | PyTorch inference without no_grad | Wrap in `torch.no_grad()` | +| GCI103 | .items() when only key/value needed | Use .keys() or .values() | +| GCI105 | String concatenation with += | Use `"".join()` or f-strings | +| GCI106 | math.sqrt in loop | Vectorize with numpy | +| GCI107 | Iterative matrix operations | Use numpy/pandas vectorization | +| GCI108 | list.insert(0, x) | Use `collections.deque.appendleft()` | +| GCI109 | Exceptions for control flow | Use dict.get(), getattr() with default | +| GCI110 | from module import * | Use explicit named imports | +| GCI111 | f-string in logging | Use `%s` lazy formatting | +| GCI112 | Dataclass without __slots__ | Add `slots=True` | +| GCI404 | List comprehension in iteration | Use generator expression | + +## Scoring + +- **100/100** = 0 issues +- Each issue reduces score: BLOCKER=-10, CRITICAL=-5, MAJOR=-3, MINOR=-1, INFO=-0.5 +- Grade: A (≥90), B (≥80), C (≥70), D (≥50), E (<50) + +## How to respond + +When reviewing code: +1. List violations with line numbers and official GCI rule ID +2. Show severity +3. Provide the fix inline +4. Estimate the Creedengo score + +When generating code: +- Never trigger any GCI rule +- Always close resources +- Always use efficient string handling +- Always specify SQL columns +- Prefer async I/O +- Use batch operations instead of loops with I/O diff --git a/.github/agents/global-green-analyzer.md b/.github/agents/global-green-analyzer.md new file mode 100644 index 0000000..e050c50 --- /dev/null +++ b/.github/agents/global-green-analyzer.md @@ -0,0 +1,65 @@ +You are the **Global Green Analyzer** agent. You combine the capabilities of both the **Green API Score Analyzer** and the **Creedengo Eco-Design Analyzer** into a single unified review. + +## Your role + +When asked to analyze code, you perform BOTH analyses in one pass: +1. **Green API Score** (DE01–DE11, AR01–AR05, LO01, US07, BIN01) — max 123 pts +2. **Creedengo Eco-Design** (official GCI rules) — max 100 pts + +## How to respond + +### Combined Analysis Output: + +``` +═══════════════════════════════════════ +🌿 GREEN API SCORE: XX/123 (Grade: X) +═══════════════════════════════════════ +``` + +For each Green API rule: +- ✅ DE11 Pagination (15/15) — description +- ❌ DE08 Field filtering (0/15) — missing `fields` param → [fix] + +``` +═══════════════════════════════════════ +🌱 CREEDENGO SCORE: XX/100 (Grade: X) +═══════════════════════════════════════ +``` + +For each violation found: +- 📍 [GCI##] file:line — description → fix + +### Final Summary: + +``` +═══════════════════════════════════════ +📊 COMBINED ASSESSMENT +═══════════════════════════════════════ +Green API Score: XX/123 (Grade X) +Creedengo Score: XX/100 (Grade X) +Overall Eco-Grade: X + +🏆 Top 5 Quick Wins (by impact): +1. [Rule] — fix — +N pts +2. ... +``` + +## Rules Reference + +### Green API (see @green-api-analyzer agent for full details) +DE11(15), DE08(15), DE01(15), DE02/DE03(15), DE06(10), 206(10), BIN01(10), LO01(5), US07(5), AR01(6), AR02(7), AR03(3), AR04(5), AR05(2) + +### Creedengo (see @creedengo-analyzer agent for full details) + +**Java**: GCI1, GCI2, GCI3, GCI5, GCI27, GCI28, GCI32, GCI67, GCI69, GCI72, GCI74, GCI76, GCI77, GCI78, GCI79, GCI82, GCI94 + +**C#**: GCI69, GCI72, GCI75, GCI81, GCI82, GCI83, GCI84, GCI85, GCI86, GCI87, GCI88, GCI90, GCI91, GCI92, GCI93 + +**Python**: GCI2, GCI4, GCI7, GCI35, GCI72, GCI74, GCI89, GCI96, GCI97, GCI100, GCI103, GCI105, GCI106, GCI107, GCI108, GCI109, GCI110, GCI111, GCI112, GCI404 + +## When generating code + +Apply ALL rules from both analyzers simultaneously. Mark each pattern: +- `// DE11: pagination` +- `// GCI82: final` + diff --git a/.github/agents/green-api-analyzer.md b/.github/agents/green-api-analyzer.md new file mode 100644 index 0000000..c1d193f --- /dev/null +++ b/.github/agents/green-api-analyzer.md @@ -0,0 +1,42 @@ +You are the **Green API Score Analyzer** agent. Your job is to review API code and suggest improvements to maximize the Green API Score (up to 123 points). + +## Your capabilities + +1. **Analyze endpoints** for Green API compliance (DE01–DE11, AR01–AR05, LO01, US07, BIN01) +2. **Generate code** that implements missing green patterns +3. **Review pull requests** for eco-design regressions +4. **Suggest refactorings** to improve the score + +## Rules you enforce + +### Data Efficiency (DE) — 80 pts +- **DE11 Pagination (15 pts)**: Every collection endpoint (`GET /items`) MUST accept `page`+`size` or `limit`+`offset`. Return `X-Total-Count` header. +- **DE08 Field Filtering (15 pts)**: Support `?fields=id,name,email` to reduce payload size. +- **DE01 Compression (15 pts)**: Configure gzip/brotli compression middleware. Verify `Content-Encoding` header. +- **DE02/DE03 Cache (15 pts)**: Generate ETag from response hash. Return `304 Not Modified` when `If-None-Match` matches. +- **DE06 Delta (10 pts)**: Provide `GET /resources/changes?since=2024-01-01T00:00:00Z` for incremental sync. +- **Range 206 (10 pts)**: Support `Range` header for binary/large endpoints. Return `206 Partial Content` + `Content-Range`. + +### Architecture (AR) — 23 pts +- **AR01 Event-Driven (6 pts)**: Webhooks, SSE, WebSocket, message broker instead of polling. +- **AR02 Runtime Proximity (7 pts)**: CDN, edge deployment, multi-region. +- **AR03 Single API (3 pts)**: No duplicate APIs for same business need. +- **AR04 Scalable Infra (5 pts)**: HPA, KEDA, autoscale, serverless. +- **AR05 Cloud Footprint (2 pts)**: Carbon dashboard monitoring. + +### Other — 20 pts +- **BIN01 Binary Format (10 pts)**: CBOR, Protobuf, or MessagePack endpoint. +- **LO01 Observability (5 pts)**: `/health`, `/metrics`, `/actuator/health`. +- **US07 Rate Limiting (5 pts)**: Return `429` with `Retry-After` header. + +## How to respond + +When asked to analyze an API: +1. List each rule with ✅ (pass) or ❌ (fail) +2. For each ❌, provide a code fix +3. Calculate the estimated score + +When asked to generate code: +- Include ALL green patterns by default +- Add inline comments referencing the rule ID (e.g., `// DE11: pagination`) + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..2cfca65 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,88 @@ +# Green API & Creedengo Copilot Instructions + +You are a **Green API Architecture** and **Eco-Design** assistant for backend APIs. +Your role is to help developers write API code that scores high on the **APIGreenScore** framework and passes **Creedengo** eco-design static analysis. + +## Green API Score Rules (100 pts + 23 architecture pts) + +| Rule | ID | Max | What to implement | +|------|----|-----|-------------------| +| Pagination | DE11 | 15 | All collection endpoints MUST support `page`/`size` or `limit`/`offset` query params | +| Field filtering | DE08 | 15 | Support a `fields` query param to return only requested fields | +| Compression | DE01 | 15 | Return `Content-Encoding: gzip` when `Accept-Encoding: gzip` is sent | +| Cache ETag/304 | DE02/DE03 | 15 | Return `ETag` header; respond `304 Not Modified` to `If-None-Match` | +| Delta/Changes | DE06 | 10 | Provide a `/changes?since=` endpoint for incremental sync | +| Range/Partial | 206 | 10 | Support `Range` header and return `206 Partial Content` for large payloads | +| Binary format | BIN01 | 10 | Offer at least one binary serialisation (CBOR, Protobuf, MessagePack) | +| Observability | LO01 | 5 | Expose `/health`, `/metrics`, or `/actuator/health` | +| Rate limiting | US07 | 5 | Implement rate limiting (return `429 Too Many Requests`) | +| Event-driven | AR01 | 6 | Use webhooks, SSE, WebSocket, or async messaging instead of polling | +| Runtime proximity | AR02 | 7 | Deploy close to consumers (CDN, edge, multi-region) | +| Single API | AR03 | 3 | One API per business need (no duplicate infra) | +| Scalable infra | AR04 | 5 | Auto-scaling (HPA, KEDA, serverless) | +| Cloud footprint | AR05 | 2 | Use cloud provider's carbon dashboard | + +## Creedengo Eco-Design Rules (official GCI IDs) + +### Java +- **GCI1**: Avoid calling Spring repository inside loop/stream — use batch query +- **GCI3**: Avoid getting collection size in loop condition — cache size +- **GCI5**: Use PreparedStatement instead of Statement +- **GCI69**: Avoid loop-invariant function calls in loop condition +- **GCI72**: Avoid SQL queries inside loops — batch operations +- **GCI74**: Avoid `SELECT *` — specify columns +- **GCI76**: Make static collections final +- **GCI77**: Move `Pattern.compile()` to `private static final` +- **GCI79**: Free resources with try-with-resources +- **GCI82**: Make variables that are never reassigned `final` +- **GCI94**: Use `orElseGet()` instead of `orElse()` with expensive computation + +### C# / .NET +- **GCI69**: Avoid loop-invariant function in loop condition +- **GCI72**: Avoid EF/SQL queries inside loops +- **GCI75**: Avoid string concatenation in loops — use `StringBuilder` +- **GCI81**: Specify struct layouts for memory optimization +- **GCI83**: Use `nameof()` instead of `Enum.ToString()` +- **GCI84**: Avoid `async void` — use `async Task` +- **GCI85**: Seal types that don't need inheritance +- **GCI86**: Never call `GC.Collect()` +- **GCI87**: Use collection indexer instead of LINQ `.First()/.Last()` +- **GCI88**: Dispose `IAsyncDisposable` with `await using` +- **GCI91**: Filter before sorting +- **GCI92**: Use `string.IsNullOrEmpty()` instead of comparing to `""` +- **GCI93**: Return Task directly instead of single await + +### Python +- **GCI72**: Avoid SQL queries in loops — use `executemany()` +- **GCI74**: Avoid `SELECT *` — specify columns +- **GCI89**: Set `maxsize` on `@lru_cache` +- **GCI103**: Use `.keys()`/`.values()` instead of `.items()` when appropriate +- **GCI105**: Use `"".join()` instead of `+=` for string concatenation +- **GCI109**: Avoid exceptions for control flow — use `dict.get()` +- **GCI110**: Avoid `from module import *` — use explicit imports +- **GCI111**: Use `%s` lazy formatting in logging instead of f-strings +- **GCI112**: Add `slots=True` to dataclasses +- **GCI404**: Use generator expressions instead of list comprehensions in iterations + +### General (all languages) +- Minimize HTTP calls (batch operations) +- Use async/streaming when possible +- Close resources (connections, streams) properly +- Avoid N+1 queries +- Cache expensive computations +- Use efficient data structures + +## When reviewing or generating code + +1. **Always** add pagination params to collection endpoints +2. **Always** add `fields` filtering support +3. **Always** configure gzip compression (middleware/filter) +4. **Always** add ETag generation + 304 support on GET single-resource +5. **Never** use `SELECT *` — specify columns (GCI74) +6. **Never** concatenate strings in loops (GCI75/GCI105) +7. **Never** call DB/repository in a loop (GCI1/GCI72) +8. **Prefer** async I/O over synchronous blocking +9. **Prefer** streaming large responses over buffering +10. **Always** expose health/readiness endpoints +11. **Always** free resources (GCI79 try-with-resources / GCI88 await using) +12. **Always** make non-reassigned variables final/const/readonly (GCI82) diff --git a/.github/prompts/creedengo-fix.md b/.github/prompts/creedengo-fix.md new file mode 100644 index 0000000..759063c --- /dev/null +++ b/.github/prompts/creedengo-fix.md @@ -0,0 +1,51 @@ +Scan the code for **Creedengo eco-design** violations (official GCI rules) and fix them. + +## What to look for: + +### Java +- **GCI1**: Spring repository in loop/stream → batch/JOIN +- **GCI3**: Collection.size() in loop condition → cache size +- **GCI5**: Statement vs PreparedStatement +- **GCI69**: Loop-invariant function in condition → extract +- **GCI72**: SQL query in loop → batch +- **GCI74**: SELECT * → specify columns +- **GCI77**: Pattern.compile() non-static → static final +- **GCI79**: Unclosed resources → try-with-resources +- **GCI82**: Variable never reassigned → final +- **GCI94**: orElse() with computation → orElseGet() + +### C# / .NET +- **GCI72**: EF/SQL query in loop → batch +- **GCI75**: String concat in loop → StringBuilder +- **GCI83**: Enum.ToString() → nameof() +- **GCI84**: async void → async Task +- **GCI85**: Unsealed type → sealed +- **GCI86**: GC.Collect() → remove +- **GCI87**: LINQ First()/Last() → indexer +- **GCI88**: IAsyncDisposable → await using +- **GCI91**: Sort before filter → filter first +- **GCI92**: Compare to "" → IsNullOrEmpty +- **GCI93**: Single await → return Task directly + +### Python +- **GCI72**: SQL in loop → executemany +- **GCI74**: SELECT * → columns +- **GCI89**: lru_cache without maxsize → add maxsize +- **GCI103**: .items() when only key/value → .keys()/.values() +- **GCI105**: String += in loop → join() +- **GCI109**: Exception for control flow → dict.get() +- **GCI110**: from x import * → explicit imports +- **GCI111**: f-string in logging → %s +- **GCI112**: Dataclass → add slots=True +- **GCI404**: List comprehension in for → generator + +## Output format: + +For each violation: +``` +📍 [GCI##] File:Line — Description + Before: + After: +``` + +At the end, estimate the **Creedengo Score** (100 minus 3 pts per issue). diff --git a/.github/prompts/generate-green-endpoint.md b/.github/prompts/generate-green-endpoint.md new file mode 100644 index 0000000..b17255f --- /dev/null +++ b/.github/prompts/generate-green-endpoint.md @@ -0,0 +1,52 @@ +Generate a **fully Green API compliant** REST endpoint for the given resource. + +The endpoint MUST include ALL of the following patterns: + +## Required patterns (score them inline with comments): + +1. **DE11 — Pagination**: Accept `page` and `size` query params. Return `X-Total-Count` header. +2. **DE08 — Field filtering**: Accept `fields` query param. Only serialize requested fields. +3. **DE01 — Compression**: Ensure gzip middleware is active (show config if needed). +4. **DE02/DE03 — ETag + 304**: Generate ETag from response hash. Check `If-None-Match`, return 304 if match. +5. **DE06 — Delta**: Add a `GET /resource/changes?since=` companion endpoint. +6. **206 — Range**: For list endpoints with large payloads, support `Range` header. +7. **LO01 — Health**: Ensure health endpoint exists. +8. **US07 — Rate limit**: Include rate-limit headers in response. + +## Code quality (Creedengo GCI compliant): + +### Java — avoid these violations: +- **GCI1**: No Spring repository calls inside loops/streams — batch +- **GCI72**: No SQL queries inside loops — use IN clause or batch +- **GCI74**: No `SELECT *` — specify columns +- **GCI77**: Pattern.compile must be `private static final` +- **GCI79**: Always use try-with-resources for AutoCloseable +- **GCI82**: Make variables `final` when never reassigned +- **GCI94**: Use `orElseGet()` not `orElse()` for expensive computations + +### C# — avoid these violations: +- **GCI72**: No LINQ/EF queries inside loops +- **GCI75**: No string concatenation in loops — use StringBuilder +- **GCI84**: No `async void` — use `async Task` +- **GCI85**: Seal classes that don't need inheritance +- **GCI87**: Use indexer `[0]` not `.First()` when collection supports it +- **GCI88**: Use `await using` for IAsyncDisposable +- **GCI91**: Filter before sorting +- **GCI93**: Return Task directly when only one await + +### Python — avoid these violations: +- **GCI72**: No SQL in loops — use executemany +- **GCI74**: No SELECT * — specify columns +- **GCI105**: No string += in loops — use join() +- **GCI109**: No exceptions for control flow — use dict.get() +- **GCI111**: Use %s in logging, not f-strings +- **GCI112**: Add slots=True to dataclasses +- **GCI404**: Use generator expressions in iterations, not list comprehensions + +## Output: +- Controller/handler code +- Service/repository code +- Any configuration needed (middleware, filters) +- OpenAPI spec snippet (YAML) + +Mark each green pattern with an inline comment: `// DE11: pagination` or `// GCI82: final` diff --git a/.github/prompts/green-api-fix.md b/.github/prompts/green-api-fix.md new file mode 100644 index 0000000..9965cd0 --- /dev/null +++ b/.github/prompts/green-api-fix.md @@ -0,0 +1,49 @@ +Fix the following code to achieve **maximum Green API Score**. + +Apply ALL of the following patterns. If a pattern is already present, skip it. + +## Fixes to apply: + +### DE11 — Pagination (+15 pts) +Add `page` and `size` query params to all collection GET endpoints. Return `X-Total-Count` header. + +### DE08 — Field filtering (+15 pts) +Add `fields` query param. Only serialize requested fields in response. + +### DE01 — Compression (+15 pts) +Configure gzip/brotli compression middleware. Verify response includes `Content-Encoding`. + +### DE02/DE03 — ETag + 304 (+15 pts) +Generate ETag from response content hash. Check `If-None-Match` header, return 304 if match. + +### DE06 — Delta endpoint (+10 pts) +Add `GET /resource/changes?since=` returning only modified items. + +### 206 — Range / Partial Content (+10 pts) +Support `Range` header for binary or large list endpoints. Return `206` with `Content-Range`. + +### BIN01 — Binary format (+10 pts) +Add content negotiation for `application/cbor` or `application/protobuf`. + +### LO01 — Health endpoint (+5 pts) +Expose `/health` or `/actuator/health` with status and component checks. + +### US07 — Rate limiting (+5 pts) +Add rate-limit middleware. Return `429 Too Many Requests` with `Retry-After` header. + +### AR01 — Event-driven (+6 pts) +Replace polling patterns with webhooks, SSE, or WebSocket where applicable. + +## Output: + +For each fix applied: +``` +🔧 [DE##] +N pts — description of change + File: path + Change: +``` + +Show the modified code with inline comments marking each rule: `// DE11: pagination` + +Final estimate: **Green API Score: X/123 (Grade X)** + diff --git a/.github/prompts/green-api-review.md b/.github/prompts/green-api-review.md new file mode 100644 index 0000000..c4a7995 --- /dev/null +++ b/.github/prompts/green-api-review.md @@ -0,0 +1,37 @@ +Review the following code for **Green API Score** and **Creedengo** compliance. + +## Green API Rules — check each: +1. **DE11** — Pagination: Do collection endpoints support `page`/`size`? +2. **DE08** — Field filtering: Is there a `fields` query param? +3. **DE01** — Compression: Is gzip middleware configured? +4. **DE02/DE03** — Cache: Are ETags generated? Is 304 handled? +5. **DE06** — Delta: Is there a `/changes?since=` endpoint? +6. **206** — Range: Are large payloads supporting partial content? +7. **BIN01** — Binary: Is CBOR/Protobuf available? +8. **LO01** — Observability: Is `/health` exposed? +9. **US07** — Rate limiting: Is 429 returned on abuse? +10. **AR01** — Event-driven: Are webhooks/SSE used instead of polling? + +## Creedengo Rules — check for violations: +- **GCI1/GCI72** — DB/repository calls inside loops +- **GCI74** — SELECT * without specifying columns +- **GCI77** — Pattern.compile() not static final (Java) +- **GCI75** — String concatenation in loops (C#) +- **GCI79/GCI88** — Unclosed resources +- **GCI82** — Variables that should be final/const/readonly +- **GCI84** — async void (C#) +- **GCI85** — Unsealed types (C#) +- **GCI94** — orElse() with expensive computation (Java) +- **GCI105** — String += in loops (Python) +- **GCI111** — f-string in logging (Python) + +## Output format: + +For each rule: +- ✅ if satisfied +- ❌ if violated — with file, line, and fix + +Estimate: +- **Green API Score**: X/123 (Grade A-E) +- **Creedengo Score**: X/100 (Grade A-E) +- **Top 3 quick wins** to gain the most points diff --git a/.github/prompts/optimize-query.md b/.github/prompts/optimize-query.md new file mode 100644 index 0000000..2d3e88b --- /dev/null +++ b/.github/prompts/optimize-query.md @@ -0,0 +1,32 @@ +Optimize the following database query/repository code for **eco-design** (Creedengo GCI + Green API). + +## Check for these Creedengo violations: + +1. **GCI74 — SELECT ***: Replace with explicit column list +2. **GCI1/GCI72 — N+1 / SQL in loop**: Replace loop+query with JOIN, IN clause, or batch fetch +3. **GCI95 — Unused queried columns**: Only fetch columns that are actually used downstream +4. **DE08 — Over-fetching**: Support `fields` param to only fetch needed columns +5. **DE11 — No pagination**: Add `LIMIT`/`OFFSET` or framework equivalent +6. **GCI79/GCI88 — Connection leaks**: Ensure connections are closed (try-with-resources / await using) +7. **GCI5 — Statement vs PreparedStatement** (Java): Use PreparedStatement + +## Additional optimizations: +- **Indexing**: Suggest indexes for WHERE/ORDER BY columns +- **Eager loading**: Replace `FetchType.EAGER` with `LAZY` + explicit fetch when needed (CRJVM205) +- **GCI78**: Don't set const parameters in batch update — put in query +- **GCI32**: Initialize StringBuilder with capacity + +## Output: + +For each optimization: +``` +🔧 [GCI##] Issue: + Impact: + Before: + After: +``` + +Also suggest: +- Caching strategy (Redis, in-memory, HTTP cache headers) +- Batch sizes for bulk operations +- Connection pool sizing recommendations diff --git a/.github/skills/add-compression.md b/.github/skills/add-compression.md new file mode 100644 index 0000000..04eba42 --- /dev/null +++ b/.github/skills/add-compression.md @@ -0,0 +1,26 @@ +--- +name: add-compression +description: Configure gzip/brotli compression middleware (DE01 - 15 pts) +--- +Enable response compression for the API. +## Java (Spring Boot): +Add to application.yml: +```yaml +server: + compression: + enabled: true + mime-types: application/json,application/xml,text/plain + min-response-size: 1024 +``` +## C# (.NET): +In Program.cs: +```csharp +builder.Services.AddResponseCompression(opts => { + opts.EnableForHttps = true; + opts.Providers.Add(); + opts.Providers.Add(); +}); +app.UseResponseCompression(); +``` +## Verification: +Request with `Accept-Encoding: gzip` must return `Content-Encoding: gzip`. diff --git a/.github/skills/add-delta-endpoint.md b/.github/skills/add-delta-endpoint.md new file mode 100644 index 0000000..720cb1f --- /dev/null +++ b/.github/skills/add-delta-endpoint.md @@ -0,0 +1,28 @@ +--- +name: add-delta-endpoint +description: Add a /changes?since= delta sync endpoint (DE06 - 10 pts) +--- +Add an incremental sync endpoint that returns only resources modified after a given timestamp. +## Java (Spring Boot): +```java +@GetMapping("/changes") +public ResponseEntity> getChanges( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Instant since) { + List changes = repository.findByLastModifiedAfter(since); + return ResponseEntity.ok(changes); +} +``` +## C# (.NET): +```csharp +[HttpGet("changes")] +public async Task GetChanges([FromQuery] DateTime since) +{ + var changes = await _repository.GetModifiedSinceAsync(since); + return Ok(changes); +} +``` +## Repository query: +```sql +SELECT id, name, updated_at FROM items WHERE updated_at > :since ORDER BY updated_at ASC +``` +Requires a `last_modified` / `updated_at` column with an index. diff --git a/.github/skills/add-etag-304.md b/.github/skills/add-etag-304.md new file mode 100644 index 0000000..37b3025 --- /dev/null +++ b/.github/skills/add-etag-304.md @@ -0,0 +1,51 @@ +--- +name: add-etag-304 +description: Add ETag generation and 304 Not Modified support (DE02/DE03 — 15 pts) +--- + +Add ETag + conditional GET (304) to the given single-resource endpoint. + +## Implementation: + +### Java (Spring Boot): +```java +@GetMapping("/{id}") +public ResponseEntity getById(@PathVariable Long id, + @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) { + T entity = service.findById(id); + String etag = "\"" + Integer.toHexString(entity.hashCode()) + "\""; + + if (etag.equals(ifNoneMatch)) { + return ResponseEntity.status(HttpStatus.NOT_MODIFIED).eTag(etag).build(); + } + return ResponseEntity.ok().eTag(etag).body(entity); +} +``` + +### C# (.NET): +```csharp +[HttpGet("{id}")] +public async Task GetById(int id, [FromHeader(Name = "If-None-Match")] string? ifNoneMatch) +{ + var entity = await _repository.GetByIdAsync(id); + if (entity == null) return NotFound(); + + var etag = $"\"{entity.GetHashCode():x}\""; + + if (etag == ifNoneMatch) + { + Response.Headers.ETag = etag; + return StatusCode(304); + } + + Response.Headers.ETag = etag; + return Ok(entity); +} +``` + +### Alternative — Use middleware/filter (recommended): +- Spring: `ShallowEtagHeaderFilter` +- ASP.NET: Custom middleware hashing response body + +Apply this pattern to the provided endpoint. Use `version` or `lastModified` field for ETag if available. + diff --git a/.github/skills/add-field-filtering.md b/.github/skills/add-field-filtering.md new file mode 100644 index 0000000..627d2ac --- /dev/null +++ b/.github/skills/add-field-filtering.md @@ -0,0 +1,19 @@ +--- +name: add-field-filtering +description: Add field filtering support to reduce payload size (DE08 - 15 pts) +--- +Add `fields` query parameter to the given endpoint to allow clients to request only needed fields. +## Java (Spring Boot): +Use `MappingJacksonValue` + `@JsonFilter("fieldFilter")` on DTOs with `SimpleBeanPropertyFilter.filterOutAllExcept(fields.split(","))`. +## C# (.NET): +Use reflection to project only requested properties into a Dictionary. +## OpenAPI: +```yaml +parameters: + - name: fields + in: query + description: Comma-separated list of fields to include + schema: { type: string } + example: "id,name,price" +``` +Apply this to the provided endpoint. Return only the requested fields in the response body. diff --git a/.github/skills/add-health-endpoint.md b/.github/skills/add-health-endpoint.md new file mode 100644 index 0000000..c84e92a --- /dev/null +++ b/.github/skills/add-health-endpoint.md @@ -0,0 +1,28 @@ +--- +name: add-health-endpoint +description: Add health/readiness endpoint (LO01 - 5 pts) +--- +Add observability endpoints to the API. +## Java (Spring Boot): +Add dependency `spring-boot-starter-actuator`. Expose in application.yml: +```yaml +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized +``` +## C# (.NET): +```csharp +builder.Services.AddHealthChecks() + .AddDbContextCheck(); +app.MapHealthChecks("/health"); +``` +## Expected response: +```json +{ "status": "UP", "components": { "db": { "status": "UP" } } } +``` +Endpoints: `/health`, `/health/ready`, `/health/live` diff --git a/.github/skills/add-pagination.md b/.github/skills/add-pagination.md new file mode 100644 index 0000000..d4a508c --- /dev/null +++ b/.github/skills/add-pagination.md @@ -0,0 +1,56 @@ +--- +name: add-pagination +description: Add pagination support to a collection endpoint (DE11 — 15 pts) +--- + +Add pagination to the given collection endpoint. + +## What to add: + +### Controller/Handler: +- `page` query parameter (int, default: 0 or 1) +- `size` query parameter (int, default: 20, max: 100) +- Return `X-Total-Count` response header +- Return `X-Total-Pages` response header +- Return paginated subset of data + +### Java (Spring Boot): +```java +@GetMapping +public ResponseEntity> getAll( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size) { + Page result = repository.findAll(PageRequest.of(page, size)); + return ResponseEntity.ok() + .header("X-Total-Count", String.valueOf(result.getTotalElements())) + .body(result.getContent()); +} +``` + +### C# (.NET): +```csharp +[HttpGet] +public async Task GetAll( + [FromQuery] int page = 1, + [FromQuery] int size = 20) +{ + var total = await _repository.CountAsync(); + var items = await _repository.GetPagedAsync(page, size); + Response.Headers["X-Total-Count"] = total.ToString(); + return Ok(items); +} +``` + +### OpenAPI snippet: +```yaml +parameters: + - name: page + in: query + schema: { type: integer, default: 1, minimum: 1 } + - name: size + in: query + schema: { type: integer, default: 20, minimum: 1, maximum: 100 } +``` + +Apply this pattern to the provided endpoint. + diff --git a/.github/skills/add-rate-limiting.md b/.github/skills/add-rate-limiting.md new file mode 100644 index 0000000..7c6d192 --- /dev/null +++ b/.github/skills/add-rate-limiting.md @@ -0,0 +1,29 @@ +--- +name: add-rate-limiting +description: Add rate limiting with 429 Too Many Requests (US07 - 5 pts) +--- +Add rate limiting to the API. +## Java (Spring Boot with Bucket4j): +```java +@Bean +public FilterRegistrationBean rateLimitFilter() { + // 100 requests per minute per IP +} +// Return 429 with Retry-After header +``` +## C# (.NET 7+): +```csharp +builder.Services.AddRateLimiter(opts => { + opts.AddFixedWindowLimiter("fixed", o => { + o.PermitLimit = 100; + o.Window = TimeSpan.FromMinutes(1); + o.QueueLimit = 0; + }); + opts.RejectionStatusCode = 429; +}); +app.UseRateLimiter(); +``` +## Response headers to include: +- `X-RateLimit-Limit: 100` +- `X-RateLimit-Remaining: 42` +- `Retry-After: 30` (on 429) diff --git a/.github/skills/creedengo-scan.md b/.github/skills/creedengo-scan.md new file mode 100644 index 0000000..400d199 --- /dev/null +++ b/.github/skills/creedengo-scan.md @@ -0,0 +1,69 @@ +--- +name: creedengo-scan +description: Scan code for Creedengo eco-design violations (official GCI rules) and provide fixes +--- + +Scan the provided code for official Creedengo (Green Code Initiative) eco-design anti-patterns. + +## Java detection rules: + +- **GCI1**: Spring repository call inside loop/stream → batch query or JOIN +- **GCI2**: Multiple if-else on same variable → use switch +- **GCI3**: Collection.size() in loop condition → cache before loop +- **GCI5**: Statement instead of PreparedStatement → use PreparedStatement +- **GCI27**: Manual array copy → System.arraycopy() +- **GCI32**: StringBuilder without initial capacity → specify size +- **GCI67**: Post-increment `i++` in for loop → `++i` +- **GCI69**: Loop-invariant function call in condition → extract variable +- **GCI72**: SQL query inside loop → batch queries +- **GCI74**: SELECT * FROM → specify columns +- **GCI76**: Non-final static collections → make final +- **GCI77**: Pattern.compile() in non-static context → static final +- **GCI79**: Resources not freed → try-with-resources +- **GCI82**: Variable never reassigned → make final +- **GCI94**: orElse() with computation → orElseGet() + +## C# detection rules: + +- **GCI69**: Loop-invariant call in condition → cache +- **GCI72**: SQL/EF query inside loop → batch +- **GCI75**: String concat in loop → StringBuilder +- **GCI81**: Struct without layout → [StructLayout] +- **GCI82**: Variable never reassigned → const/readonly +- **GCI83**: Enum.ToString() → nameof() +- **GCI84**: async void → async Task +- **GCI85**: Unsealed type → sealed +- **GCI86**: GC.Collect() → remove +- **GCI87**: LINQ First()/Last() → indexer +- **GCI88**: IAsyncDisposable not async disposed → await using +- **GCI91**: Sort before filter → filter first +- **GCI92**: Compare to "" → string.IsNullOrEmpty +- **GCI93**: Single await → return Task directly + +## Python detection rules: + +- **GCI2**: Multiple if-else → match/dict dispatch +- **GCI4**: Global variables → function arguments +- **GCI35**: try/catch for file check → os.path.exists() +- **GCI72**: SQL in loop → executemany +- **GCI74**: SELECT * → specify columns +- **GCI89**: lru_cache without maxsize → add maxsize +- **GCI96**: Read all CSV columns → usecols= +- **GCI103**: .items() when only key/value needed → .keys()/.values() +- **GCI105**: String += in loop → join() +- **GCI109**: Exception for control flow → dict.get() +- **GCI110**: from x import * → explicit imports +- **GCI111**: f-string in logging → %s lazy format +- **GCI112**: Dataclass without slots → slots=True +- **GCI404**: List comprehension in for → generator + +## Output format: + +``` +📍 [GCI##] file:line — description + ❌ Before: + ✅ After: +``` + +Final score: 100 − (3 pts per issue) +Grade: A (≥90) | B (≥80) | C (≥70) | D (≥50) | E (<50) diff --git a/.github/skills/green-api-fix.md b/.github/skills/green-api-fix.md new file mode 100644 index 0000000..1c6ed7b --- /dev/null +++ b/.github/skills/green-api-fix.md @@ -0,0 +1,23 @@ +--- +name: green-api-fix +description: Apply Green API Score fixes to existing code (pagination, fields, ETag, compression, etc.) +--- + +Apply Green API patterns to the provided code to maximize the score. + +## Patterns to apply (in priority order): + +1. **DE11 (15 pts)**: Add `page`/`size` params + `X-Total-Count` header to collections +2. **DE08 (15 pts)**: Add `fields` query param, filter response fields +3. **DE01 (15 pts)**: Enable gzip compression middleware +4. **DE02/DE03 (15 pts)**: Add ETag generation + 304 Not Modified +5. **DE06 (10 pts)**: Add `/changes?since=` endpoint +6. **206 (10 pts)**: Support Range header for large payloads +7. **BIN01 (10 pts)**: Add CBOR/Protobuf content negotiation +8. **LO01 (5 pts)**: Expose /health endpoint +9. **US07 (5 pts)**: Add rate limiting (429 + Retry-After) + +Skip patterns already implemented. Mark each with inline comment. + +Output the modified code and the estimated score gain. + diff --git a/.github/skills/green-api-score.md b/.github/skills/green-api-score.md new file mode 100644 index 0000000..28e8d0c --- /dev/null +++ b/.github/skills/green-api-score.md @@ -0,0 +1,31 @@ +--- +name: green-api-score +description: Analyze an API endpoint or controller and calculate its Green API Score (0-123 pts) +--- + +Analyze the provided code for Green API Score compliance. + +For each rule, determine if it passes or fails: + +| Rule | Points | Check | +|------|--------|-------| +| DE11 Pagination | /15 | Collection endpoints have `page`/`size` or `limit`/`offset` params | +| DE08 Fields | /15 | `fields` query param supported | +| DE01 Compression | /15 | Gzip middleware configured | +| DE02/DE03 Cache | /15 | ETag generated + 304 on If-None-Match | +| DE06 Delta | /10 | `/changes?since=` endpoint exists | +| 206 Range | /10 | Range header → 206 Partial Content | +| BIN01 Binary | /10 | CBOR/Protobuf/MessagePack endpoint | +| LO01 Observability | /5 | `/health` or `/actuator/health` | +| US07 Rate Limit | /5 | 429 response on abuse | +| AR01 Event-driven | /6 | Webhooks/SSE/WebSocket | +| AR02 Proximity | /7 | CDN/edge/multi-region | +| AR03 Single API | /3 | No duplicate API | +| AR04 Scalable | /5 | HPA/KEDA/autoscale | +| AR05 Carbon | /2 | Cloud carbon dashboard | + +Output: +1. Score breakdown table with ✅/❌ +2. Total score and grade (A/B/C/D/E) +3. Top 3 fixes to gain the most points + diff --git a/.github/skills/validate-creedengo-fix.md b/.github/skills/validate-creedengo-fix.md new file mode 100644 index 0000000..141cf83 --- /dev/null +++ b/.github/skills/validate-creedengo-fix.md @@ -0,0 +1,37 @@ +--- +name: validate-creedengo-fix +description: Validate that a Creedengo fix is correct and doesn't introduce new violations +--- + +Validate the provided code fix for Creedengo compliance. + +## Validation checklist: + +1. **Fix correctness**: Does the fix actually resolve the reported GCI violation? +2. **No regression**: Does the fix introduce any NEW GCI violations? +3. **Behavior preservation**: Does the fix maintain the same functional behavior? +4. **Performance**: Is the fix at least as performant (or better) than the original? +5. **Best practice**: Is the fix the idiomatic/recommended solution for this rule? + +## Specific validations per rule: + +- **GCI1/GCI72 fix** (batch queries): Verify the batch actually reduces round-trips. Check SQL is correct. +- **GCI74 fix** (columns): Verify all needed columns are listed. No missing joins. +- **GCI75/GCI105 fix** (StringBuilder/join): Verify the result is identical. +- **GCI77 fix** (static Pattern): Verify thread-safety (static final is safe). +- **GCI79/GCI88 fix** (resources): Verify ALL resources in the scope are closed. +- **GCI82 fix** (final/const): Verify the variable truly isn't reassigned anywhere. +- **GCI84 fix** (async Task): Verify callers don't rely on fire-and-forget behavior. +- **GCI85 fix** (sealed): Verify no subclass exists anywhere in the codebase. +- **GCI94 fix** (orElseGet): Verify the lambda doesn't capture mutable state incorrectly. + +## Output: + +``` +✅ VALID — Fix correctly resolves GCI## without regression +⚠️ PARTIAL — Fix resolves GCI## but introduces: [issue] +❌ INVALID — Fix does not resolve GCI## because: [reason] +``` + +If invalid, provide the correct fix. + diff --git a/.github/skills/validate-green-api-fix.md b/.github/skills/validate-green-api-fix.md new file mode 100644 index 0000000..b173211 --- /dev/null +++ b/.github/skills/validate-green-api-fix.md @@ -0,0 +1,59 @@ +--- +name: validate-green-api-fix +description: Validate that a Green API fix is correct and actually gains the expected points +--- + +Validate the provided Green API fix for correctness and score impact. + +## Validation checklist: + +1. **Rule compliance**: Does the fix actually satisfy the Green API rule requirements? +2. **Functional correctness**: Does the endpoint still behave correctly? +3. **No regression**: Does the fix break other Green API patterns already in place? +4. **Score accuracy**: Is the claimed point gain realistic? + +## Specific validations per rule: + +### DE11 — Pagination +- ✅ `page` and `size` params present with defaults +- ✅ Response is a subset (not full collection) +- ✅ `X-Total-Count` header returned +- ❌ INVALID if: params exist but full collection is always returned + +### DE08 — Field filtering +- ✅ `fields` param accepted +- ✅ Response actually omits unrequested fields +- ❌ INVALID if: param is accepted but ignored + +### DE01 — Compression +- ✅ Middleware configured +- ✅ Response includes `Content-Encoding: gzip` when `Accept-Encoding: gzip` is sent +- ❌ INVALID if: only configured but never applied (wrong order, wrong mime-types) + +### DE02/DE03 — ETag + 304 +- ✅ ETag header present on GET responses +- ✅ `If-None-Match` check implemented +- ✅ Returns 304 (not 200 with empty body) +- ❌ INVALID if: ETag is static/hardcoded or never changes + +### DE06 — Delta +- ✅ `/changes?since=` endpoint exists +- ✅ Returns only items modified after the timestamp +- ✅ `last_modified` / `updated_at` column exists and is indexed +- ❌ INVALID if: returns ALL items regardless of `since` + +### US07 — Rate limiting +- ✅ Returns 429 when limit exceeded +- ✅ `Retry-After` header present +- ❌ INVALID if: configured but never triggered (infinite limit) + +## Output: + +``` +✅ VALID — Fix correctly implements DE## (+N pts confirmed) +⚠️ PARTIAL — Fix implements DE## but: [issue] (+N pts reduced to +M) +❌ INVALID — Fix does not satisfy DE## because: [reason] (+0 pts) +``` + +If invalid, provide the correct implementation. + diff --git a/dashboard/interactive.html b/dashboard/interactive.html index d6e55eb..056b7f1 100644 --- a/dashboard/interactive.html +++ b/dashboard/interactive.html @@ -421,6 +421,26 @@

① Cibles à analyser & authentification

+
+ 📤 Envoi vers SobriIT (optionnel) + +
Si activé, le Green Score sera publié sur la plateforme SobriIT après l'analyse.
+ +
+
@@ -610,6 +630,26 @@

① Analyse locale (Green Score + Creedengo source)

+
+ 📤 Envoi vers SobriIT (optionnel) + +
Si activé, Green Score + Creedengo seront publiés sur SobriIT après l'analyse.
+ +
+
@@ -923,6 +963,28 @@

initBridge(); +// ── SobriIT toggle (show/hide URL+key fields) + localStorage persistence ── +function setupSobriitToggle(checkboxId, fieldsId, urlId, keyId, lsPrefix) { + const cb = $(checkboxId), fields = $(fieldsId), urlEl = $(urlId), keyEl = $(keyId); + if (!cb || !fields) return; + // Restore from localStorage + try { + if (localStorage.getItem(lsPrefix + ".enabled") === "true") cb.checked = true; + if (urlEl) urlEl.value = localStorage.getItem(lsPrefix + ".url") || ""; + if (keyEl) keyEl.value = localStorage.getItem(lsPrefix + ".key") || ""; + } catch {} + const sync = () => { + fields.style.display = cb.checked ? "" : "none"; + try { localStorage.setItem(lsPrefix + ".enabled", cb.checked); } catch {} + }; + cb.addEventListener("change", sync); + sync(); + if (urlEl) urlEl.addEventListener("input", () => { try { localStorage.setItem(lsPrefix + ".url", urlEl.value); } catch {} }); + if (keyEl) keyEl.addEventListener("input", () => { try { localStorage.setItem(lsPrefix + ".key", keyEl.value); } catch {} }); +} +setupSobriitToggle("send-to-sobriit", "sobriit-fields", "sobriit-base-url", "sobriit-api-key", "greenapi.sobriit.remote"); +setupSobriitToggle("loc-send-to-sobriit", "loc-sobriit-fields", "loc-sobriit-base-url", "loc-sobriit-api-key", "greenapi.sobriit.local"); + // ── Step 2 rendering ──────────────────────────────────────────── function renderTargetsSummary(targets){ $("targets-summary").innerHTML = targets.map(t => { @@ -1029,6 +1091,9 @@

swaggers: state.swaggers, bearer: state.bearer, appname: state.appname || undefined, + sendToSobriit: $("send-to-sobriit")?.checked || false, + sobriitBaseUrl: $("sobriit-base-url")?.value?.trim() || undefined, + sobriitApiKey: $("sobriit-api-key")?.value?.trim() || undefined, endpoints: selected.map(r => ({ target: r.target, method: r.method, path: r.path, calls: r.calls || 3, @@ -1042,7 +1107,7 @@

} state.report = data.report; clearStatus("status-2"); - renderResults(data.report, data.config, data.log_tail); + renderResults(data.report, data.config, data.log_tail, data.sobriit); } catch (err) { setStatus("status-2","error","❌ " + (err.message || err)); $("results-body").innerHTML = `
💥

Échec de l'analyse — voir le message ci-dessus.

`; @@ -1343,7 +1408,7 @@

if (arrow) arrow.textContent = open ? "▲" : "▼"; } -function renderResults(envelope, cfg, log){ +function renderResults(envelope, cfg, log, sobriit){ const report = (envelope && envelope.report) ? envelope.report : envelope; const gs = report.green_score || {}; const total = gs.total ?? "?"; @@ -1397,6 +1462,16 @@

$("results-meta").textContent = `📅 ${ts} · ${disc.endpoints_measured||0}/${disc.endpoints_discovered||0} endpoints · repeat=${cfg?.repeat ?? "?"}`; + // SobriIT status banner + let sobriitHtml = ""; + if (sobriit) { + if (sobriit.ok) { + sobriitHtml = `
📤 SobriIT : résultats envoyés avec succès (app: ${escHtml(sobriit.applicationId||"—")}, build: ${escHtml(sobriit.buildId||"—")})
`; + } else { + sobriitHtml = `
📤 SobriIT : échec de l'envoi — ${escHtml(sobriit.error||"erreur inconnue")}
`; + } + } + $("results-body").innerHTML = `
@@ -1445,6 +1520,7 @@

${log ? `
📜 Logs analyzer
${escHtml(log)}
` : ""}

+ ${sobriitHtml} `; } @@ -1665,6 +1741,9 @@

gitRepo, gitBranch, gitSubdir, stack, sourceDir, buildAndRun, consumerRegion, enableGeoip, cloudFootprintConfirmed, + sendToSobriit: $("loc-send-to-sobriit")?.checked || false, + sobriitBaseUrl: $("loc-sobriit-base-url")?.value?.trim() || undefined, + sobriitApiKey: $("loc-sobriit-api-key")?.value?.trim() || undefined, }, { timeoutMs: 10 * 60 * 1000 }); // 10 min — match server timeout if (!ok || !data.ok) { const detail = data.log ? `
📜 Logs start.sh
${escHtml(data.log)}
` : ""; @@ -1685,11 +1764,10 @@

state.localReport = data.report || null; renderLocalResults(data.report, data.creedengo, data.config, data.log_tail, { appname, - // True when the report (if requested) is fresh; null when not requested - // → suppresses the warning entirely on the front side. freshGS: !data.report ? false : data.report_fresh !== false, freshCD: !cdRequested ? null : (!data.creedengo ? false : data.creedengo_fresh !== false), cdRequested, + sobriit: data.sobriit, }); setLocalStep(2); } catch (err) { @@ -1891,6 +1969,17 @@


Vérifiez les logs start.sh ci-dessous pour comprendre pourquoi.

` : ""; + // SobriIT status banner (local tab) + let sobriitHtml = ""; + if (opts?.sobriit) { + const s = opts.sobriit; + if (s.ok) { + sobriitHtml = `
📤 SobriIT : résultats envoyés avec succès (app: ${escHtml(s.applicationId||"—")}, build: ${escHtml(s.buildId||"—")})
`; + } else { + sobriitHtml = `
📤 SobriIT : échec de l'envoi — ${escHtml(s.error||"erreur inconnue")}
`; + } + } + $("loc-results-body").innerHTML = warnHtml + `
${gsCardHtml} @@ -1935,6 +2024,7 @@

` : ""} ${log ? `
📜 Logs start.sh
${escHtml(log)}
` : ""} + ${sobriitHtml} `; } diff --git a/scripts/greenapianalyzer-server.py b/scripts/greenapianalyzer-server.py index 7a6200f..a506a48 100755 --- a/scripts/greenapianalyzer-server.py +++ b/scripts/greenapianalyzer-server.py @@ -921,6 +921,20 @@ def _handle_local_analyze(self, payload): "log": log[-12000:], }) + # ── SobriIT integration (conditional) ── + sobriit_result = None + if payload.get("sendToSobriit"): + try: + sobriit_result = _sobriit_send( + appname=appname, + green_report=report, + creedengo_report=creedengo_report, + base_url=payload.get("sobriitBaseUrl"), + api_key=payload.get("sobriitApiKey"), + ) + except Exception as e: + sobriit_result = {"ok": False, "error": str(e)} + return self._send_json(200, { "ok": True, "report": report, @@ -930,6 +944,7 @@ def _handle_local_analyze(self, payload): "creedengo_requested": creedengo_requested, "exit_code": proc.returncode, "log_tail": log[-8000:], + "sobriit": sobriit_result, "config": { "targets": targets, "appname": appname, @@ -979,3 +994,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/scripts/greenapianalyzer.sh b/scripts/greenapianalyzer.sh index b04a8e5..0dcb96a 100755 --- a/scripts/greenapianalyzer.sh +++ b/scripts/greenapianalyzer.sh @@ -47,6 +47,7 @@ OUTPUT_DIR="$ROOT/reports" DEBUG="" SKIP_WAIT=false SKIP_DASHBOARD=false +SEND_TO_SOBRIIT=false while [ $# -gt 0 ]; do case "$1" in @@ -59,6 +60,7 @@ while [ $# -gt 0 ]; do --output-dir) OUTPUT_DIR="${2:-}"; shift 2 ;; --skip-wait) SKIP_WAIT=true; shift ;; --skip-dashboard) SKIP_DASHBOARD=true; shift ;; + --send-to-sobriit) SEND_TO_SOBRIIT=true; shift ;; --debug) DEBUG="--debug"; shift ;; -h|--help) sed -n '1,40p' "$0"; exit 0 ;; *) echo "Unknown option: $1" >&2; exit 2 ;; @@ -224,3 +226,21 @@ fi echo "" echo "✅ Report ready: $LATEST" +############################################################################### +# 4) SobriIT integration — send results if --send-to-sobriit is set +############################################################################### +if [ "$SEND_TO_SOBRIIT" = true ]; then + echo "" + echo "━━━ 📤 Sending results to SobriIT ━━━" + SOBRIIT_CMD=(python3 "$ROOT/scripts/sobriit_sender.py" + --appname "$APPNAME") + [ -f "$LATEST" ] && SOBRIIT_CMD+=(--green-report "$LATEST") + CREEDENGO_REPORT="$ROOT/reports/creedengo-report.json" + [ -f "$CREEDENGO_REPORT" ] && SOBRIIT_CMD+=(--creedengo-report "$CREEDENGO_REPORT") + if "${SOBRIIT_CMD[@]}"; then + echo "✅ Results sent to SobriIT" + else + echo "⚠️ Failed to send results to SobriIT (non-blocking)" + fi +fi + diff --git a/scripts/sobriit_sender.py b/scripts/sobriit_sender.py new file mode 100644 index 0000000..73acbea --- /dev/null +++ b/scripts/sobriit_sender.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +""" +sobriit_sender.py +───────────────── +Standalone module that sends Green API Score and Creedengo analysis results +to the SobriIT platform via its REST API. + +Configuration (environment variables): + SOBRIIT_BASE_URL Base URL of the SobriIT API (e.g. https://sobriit.example.com) + SOBRIIT_API_KEY API key for authentication (sent as X-API-Key header) + + Application metadata (optional, used when creating a new application): + SOBRIIT_APP_CODE Application code (defaults to appname) + SOBRIIT_APP_DSI DSI name + SOBRIIT_APP_TRIBE Tribe name + SOBRIIT_APP_TRIBE_ID Tribe UUID + +These can also be passed as function parameters (override env vars). + +Usage from Python: + from sobriit_sender import send_to_sobriit + result = send_to_sobriit(appname, green_report_dict, creedengo_report_dict) + +Usage from CLI (for shell script integration): + python3 scripts/sobriit_sender.py \\ + --appname myapp \\ + --green-report reports/latest-report.json \\ + --creedengo-report reports/creedengo-report.json \\ + [--base-url https://sobriit.example.com] \\ + [--api-key xxx] \\ + [--app-code CODE] \\ + [--app-dsi DSI] \\ + [--app-tribe TRIBE] \\ + [--app-tribe-id UUID] + Exit code 0 = success, 1 = failure (non-blocking by design). +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import urllib.error +import urllib.request +from pathlib import Path + + +# ─── HTTP helpers (stdlib only) ────────────────────────────────────────────── + +def _api_call(method: str, url: str, api_key: str, + body: dict | None = None, timeout: int = 15) -> tuple[int, dict | str]: + """Perform an HTTP request to SobriIT. Returns (status_code, parsed_json_or_text).""" + headers = { + "X-API-Key": api_key, + "Accept": "application/json", + } + data = None + if body is not None: + headers["Content-Type"] = "application/json; charset=utf-8" + data = json.dumps(body, ensure_ascii=False).encode("utf-8") + + req = urllib.request.Request(url, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=timeout) as resp: + raw = resp.read() + try: + return resp.status, json.loads(raw) + except Exception: + return resp.status, raw.decode("utf-8", errors="replace") + except urllib.error.HTTPError as e: + raw = e.read() if e.fp else b"" + try: + return e.code, json.loads(raw) + except Exception: + return e.code, raw.decode("utf-8", errors="replace") + except Exception as exc: + return 0, str(exc) + + +def _json_str(obj) -> str | None: + """Serialize obj to a JSON string, or None if obj is None/empty.""" + if obj is None: + return None + if isinstance(obj, str): + return obj if obj.strip() else None + try: + return json.dumps(obj, ensure_ascii=False) + except Exception: + return str(obj) + + +# ─── Main send function ───────────────────────────────────────────────────── + +def send_to_sobriit( + appname: str, + green_report: dict | None = None, + creedengo_report: dict | None = None, + base_url: str | None = None, + api_key: str | None = None, + app_code: str | None = None, + app_dsi: str | None = None, + app_tribe: str | None = None, + app_tribe_id: str | None = None, +) -> dict: + """ + Send analysis results to SobriIT. + + Flow: + 1. GET /api/v1/applications/by-name/{appname} → lookup existing app + If 404 → POST /api/v1/applications to create it + 2. POST /api/v1/builds → create a build linked to the application + 3. POST /api/v1/reports/greenapibackend → create the detailed report + + Returns a status dict: + {"ok": True/False, "applicationId": ..., "buildId": ..., "reportId": ..., "error": ...} + """ + base_url = (base_url or os.environ.get("SOBRIIT_BASE_URL", "")).rstrip("/") + api_key = api_key or os.environ.get("SOBRIIT_API_KEY", "") + app_code = app_code or os.environ.get("SOBRIIT_APP_CODE", "") or appname + app_dsi = app_dsi or os.environ.get("SOBRIIT_APP_DSI", "") or None + app_tribe = app_tribe or os.environ.get("SOBRIIT_APP_TRIBE", "") or None + app_tribe_id = app_tribe_id or os.environ.get("SOBRIIT_APP_TRIBE_ID", "") or None + + if not base_url: + return {"ok": False, "error": "SOBRIIT_BASE_URL not configured"} + if not api_key: + return {"ok": False, "error": "SOBRIIT_API_KEY not configured"} + if not appname: + return {"ok": False, "error": "appname is required"} + if not green_report and not creedengo_report: + return {"ok": False, "error": "no report data to send"} + + # ── Extract green score data ── + gs = {} + report_section = {} + if green_report: + report_section = green_report.get("report") or {} + gs = report_section.get("green_score") or {} + + green_total = gs.get("total") + green_max = gs.get("max") + green_grade = gs.get("grade") + score_normalised = round(green_total / green_max * 100, 2) if green_total and green_max else None + report_timestamp = report_section.get("timestamp") + + # ── Extract creedengo data ── + cs = {} + if creedengo_report: + cs = creedengo_report.get("creedengo_score") or {} + + # ── Step 1: Lookup or create application ── + app_id = None + quoted = urllib.request.quote(appname, safe='') + + # Try lookup by name first, then by code via the search endpoint + for search_field in ("name", "code"): + status, resp = _api_call( + "GET", + f"{base_url}/api/v1/applications/search?{search_field}={quoted}", + api_key, + ) + if status == 200 and isinstance(resp, dict): + items = resp.get("data") or [] + # Exact match (search may return partial matches) + for item in items: + if isinstance(item, dict) and item.get("id"): + val = (item.get(search_field) or "").strip() + if val.lower() == appname.lower(): + app_id = item["id"] + break + if app_id: + break + + # Also try the direct by-name endpoint as fallback + if not app_id: + status, resp = _api_call( + "GET", + f"{base_url}/api/v1/applications/by-name/{quoted}", + api_key, + ) + if status == 200 and isinstance(resp, dict) and resp.get("id"): + app_id = resp["id"] + + # If still not found, create the application with all available fields + if not app_id: + app_payload = { + "name": appname, + "code": app_code, + "dsi": app_dsi, + "tribe": app_tribe, + "tribeId": app_tribe_id, + "globalScore": score_normalised or 0.0, + "performance": 0.0, + "accessibility": 0.0, + "bestPractices": 0.0, + "ecoindex": 0.0, + "bestPracticesCnumr": round(cs.get("total", 0) / cs.get("max", 100) * 100, 2) if cs.get("max") else 0.0, + "accessibilityAxeCore": 0.0, + "greenApiScore": score_normalised or 0.0, + "greenBackendScore": 0.0, + } + # Remove None values + app_payload = {k: v for k, v in app_payload.items() if v is not None} + + status, resp = _api_call("POST", f"{base_url}/api/v1/applications", api_key, body=app_payload) + if status in (200, 201) and isinstance(resp, dict) and resp.get("id"): + app_id = resp["id"] + else: + return {"ok": False, "error": f"failed to create application (HTTP {status}): {resp}"} + + # ── Step 2: Create build ── + build_payload = { + "applicationId": app_id, + "tag": report_timestamp or "", + "globalScore": score_normalised or 0.0, + "performance": 0.0, + "accessibility": 0.0, + "bestPractices": 0.0, + "ecoindex": 0.0, + "bestPracticesCnumr": round(cs.get("total", 0) / cs.get("max", 100) * 100, 2) if cs.get("max") else 0.0, + "accessibilityAxeCore": 0.0, + "greenApiScore": score_normalised or 0.0, + "greenBackendScore": 0.0, + } + status, resp = _api_call("POST", f"{base_url}/api/v1/builds", api_key, body=build_payload) + if status not in (200, 201) or not isinstance(resp, dict) or not resp.get("id"): + return { + "ok": False, + "applicationId": app_id, + "error": f"failed to create build (HTTP {status}): {resp}", + } + build_id = resp["id"] + + # ── Step 3: Create GreenApiBackendReport ── + report_payload = { + "buildId": build_id, + "appName": appname, + "reportTimestamp": report_timestamp, + # Green Score fields + "greenApiScoreTotal": green_total, + "greenApiScoreMax": green_max, + "greenApiGrade": green_grade, + "scoreNormalised100": score_normalised, + "greenScoreBreakdownJson": _json_str(gs.get("breakdown")), + "ruleResourceMappingJson": _json_str(gs.get("rule_resource_mapping")), + "greenScoreDetailsJson": _json_str(gs.get("details")), + "endpointRulesJson": _json_str(report_section.get("endpoint_rules")), + "totalsJson": _json_str(report_section.get("totals")), + "endpointsJson": _json_str(report_section.get("endpoints")), + "measurementsJson": _json_str(report_section.get("measurements")), + "autoDiscoveryJson": _json_str(report_section.get("auto_discovery")), + # Creedengo fields + "creedengoScoreTotal": cs.get("total"), + "creedengoScoreMax": cs.get("max"), + "creedengoGrade": cs.get("grade"), + "creedengoIssuesCount": cs.get("issues_count"), + "creedengoTotalEffortMinutes": cs.get("total_effort_minutes"), + "creedengoLanguage": creedengo_report.get("language") if creedengo_report else None, + "creedengoProject": creedengo_report.get("project") if creedengo_report else None, + "creedengoSeverityJson": _json_str(cs.get("severity_breakdown")), + "creedengoMeasuresJson": _json_str(creedengo_report.get("measures")) if creedengo_report else None, + "creedengoRulesSummaryJson": _json_str(creedengo_report.get("rules_summary")) if creedengo_report else None, + "creedengoCategoriesJson": _json_str(creedengo_report.get("categories")) if creedengo_report else None, + "creedengoTopFilesJson": _json_str(creedengo_report.get("top_files")) if creedengo_report else None, + "creedengoIssuesJson": _json_str(creedengo_report.get("issues")) if creedengo_report else None, + "sonarIssuesJson": _json_str(creedengo_report.get("sonar_issues")) if creedengo_report else None, + "creedengoDetectionJson": _json_str(creedengo_report.get("detection")) if creedengo_report else None, + } + + # Remove None values (SobriIT accepts nullable but cleaner without) + report_payload = {k: v for k, v in report_payload.items() if v is not None} + + status, resp = _api_call( + "POST", f"{base_url}/api/v1/reports/greenapibackend", api_key, body=report_payload + ) + if status not in (200, 201): + return { + "ok": False, + "applicationId": app_id, + "buildId": build_id, + "error": f"failed to create report (HTTP {status}): {resp}", + } + + report_id = resp.get("id") if isinstance(resp, dict) else None + return { + "ok": True, + "applicationId": app_id, + "buildId": build_id, + "reportId": report_id, + } + + +# ─── CLI entry point (for shell script integration) ───────────────────────── + +def main(): + p = argparse.ArgumentParser(description="Send analysis results to SobriIT") + p.add_argument("--appname", required=True, help="Application name") + p.add_argument("--green-report", help="Path to latest-report.json") + p.add_argument("--creedengo-report", help="Path to creedengo-report.json") + p.add_argument("--base-url", default=None, help="SobriIT base URL (or SOBRIIT_BASE_URL env)") + p.add_argument("--api-key", default=None, help="SobriIT API key (or SOBRIIT_API_KEY env)") + p.add_argument("--app-code", default=None, help="Application code (or SOBRIIT_APP_CODE env, defaults to appname)") + p.add_argument("--app-dsi", default=None, help="DSI name (or SOBRIIT_APP_DSI env)") + p.add_argument("--app-tribe", default=None, help="Tribe name (or SOBRIIT_APP_TRIBE env)") + p.add_argument("--app-tribe-id", default=None, help="Tribe UUID (or SOBRIIT_APP_TRIBE_ID env)") + args = p.parse_args() + + green_report = None + if args.green_report: + fp = Path(args.green_report) + if fp.is_file(): + with fp.open("r", encoding="utf-8") as f: + green_report = json.load(f) + else: + print(f"⚠ Green report not found: {args.green_report}", file=sys.stderr) + + creedengo_report = None + if args.creedengo_report: + fp = Path(args.creedengo_report) + if fp.is_file(): + with fp.open("r", encoding="utf-8") as f: + creedengo_report = json.load(f) + else: + print(f"⚠ Creedengo report not found: {args.creedengo_report}", file=sys.stderr) + + if not green_report and not creedengo_report: + print("❌ No report files found — nothing to send.", file=sys.stderr) + sys.exit(1) + + result = send_to_sobriit( + appname=args.appname, + green_report=green_report, + creedengo_report=creedengo_report, + base_url=args.base_url, + api_key=args.api_key, + app_code=args.app_code, + app_dsi=args.app_dsi, + app_tribe=args.app_tribe, + app_tribe_id=args.app_tribe_id, + ) + + print(json.dumps(result, indent=2, ensure_ascii=False)) + sys.exit(0 if result.get("ok") else 1) + + +if __name__ == "__main__": + main() + diff --git a/scripts/start.sh b/scripts/start.sh index 4bec60c..3d6dfc3 100644 --- a/scripts/start.sh +++ b/scripts/start.sh @@ -1195,13 +1195,34 @@ else echo "💡 Tip: run with --creedengo to also run Creedengo eco-design code analysis" fi +############################################################################### +# Report paths (used by SobriIT + Dashboard) +############################################################################### +LATEST_REPORT="$ROOT/reports/latest-report.json" +CREEDENGO_REPORT="$ROOT/reports/creedengo-report.json" + +############################################################################### +# SobriIT integration — send results if --send-to-sobriit is set +############################################################################### +if [ "$SEND_TO_SOBRIIT" = true ]; then + echo "" + echo "━━━ 📤 Sending results to SobriIT ━━━" + SOBRIIT_CMD=(python3 "$ROOT/scripts/sobriit_sender.py" + --appname "$APPNAME") + [ -f "$LATEST_REPORT" ] && SOBRIIT_CMD+=(--green-report "$LATEST_REPORT") + [ -f "$CREEDENGO_REPORT" ] && SOBRIIT_CMD+=(--creedengo-report "$CREEDENGO_REPORT") + if "${SOBRIIT_CMD[@]}"; then + echo "✅ Results sent to SobriIT" + else + echo "⚠️ Failed to send results to SobriIT (non-blocking)" + fi +fi + ############################################################################### # Dashboard generation — AFTER all analyses (green-score + creedengo) ############################################################################### echo "" echo "━━━ 📊 Generating final Dashboard ━━━" -LATEST_REPORT="$ROOT/reports/latest-report.json" -CREEDENGO_REPORT="$ROOT/reports/creedengo-report.json" if [ -f "$ROOT/scripts/generate-dashboard.sh" ] && [ -f "$LATEST_REPORT" ]; then DASHBOARD_ARGS=("$LATEST_REPORT" "$ROOT/dashboard/index.save.html" "$ROOT/dashboard/index.html")