@3lineas/grooveskills 1.5.0 → 1.7.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/README.md +8 -0
- package/dist/{chunk-PLMYUKGW.js → chunk-E63GTCOE.js} +475 -146
- package/dist/chunk-E63GTCOE.js.map +1 -0
- package/dist/cli.js +23 -3
- package/dist/cli.js.map +1 -1
- package/dist/main.js +1 -1
- package/package.json +3 -2
- package/skills/auth-rbac-policies/SKILL.md +100 -0
- package/skills/auth-rbac-policies/references/auth-adapters-examples.md +99 -0
- package/skills/caching-rate-limiting/SKILL.md +109 -0
- package/skills/caching-rate-limiting/references/cache-rate-limit-examples.md +129 -0
- package/skills/clean-architecture-ddd/SKILL.md +1 -0
- package/skills/controller-orchestration-queues/SKILL.md +109 -0
- package/skills/controller-orchestration-queues/references/controller-queue-examples.md +115 -0
- package/skills/docs-standards/SKILL.md +113 -0
- package/skills/docs-standards/references/adr-template-and-lifecycle.md +111 -0
- package/skills/docs-standards/references/code-comments-and-docstrings.md +147 -0
- package/skills/docs-standards/references/relative-paths-and-diagrams.md +123 -0
- package/skills/env-feature-flags/SKILL.md +94 -0
- package/skills/env-feature-flags/references/env-feature-flags-examples.md +93 -0
- package/skills/external-service-adapters/SKILL.md +148 -0
- package/skills/external-service-adapters/references/email-and-payment-examples.md +198 -0
- package/skills/storage-file-adapters/SKILL.md +125 -0
- package/skills/storage-file-adapters/references/storage-adapters-examples.md +164 -0
- package/skills/webhook-idempotency/SKILL.md +104 -0
- package/skills/webhook-idempotency/references/webhook-idempotency-examples.md +112 -0
- package/dist/chunk-PLMYUKGW.js.map +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Architecture Decision Records (ADR) Lifecycle and Template
|
|
2
|
+
|
|
3
|
+
Authoritative specification for creating, managing, and deprecating Architecture Decision Records.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. What is an ADR?
|
|
8
|
+
|
|
9
|
+
An Architecture Decision Record (ADR) captures an important architectural or technical choice made on a project along with its context and consequences.
|
|
10
|
+
|
|
11
|
+
Key attributes of effective ADRs:
|
|
12
|
+
- **Immutable historical record**: Once accepted, an ADR is not revised to say something different. If the decision changes, a new ADR supersedes it.
|
|
13
|
+
- **Context-focused**: Explains *why* options were selected and what trade-offs were accepted.
|
|
14
|
+
- **English-only**: Standardized in English across all engineering teams.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 2. ADR Lifecycle States
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
flowchart LR
|
|
22
|
+
Proposed["1. Proposed"] --> Accepted["2. Accepted"]
|
|
23
|
+
Proposed --> Rejected["Rejected"]
|
|
24
|
+
Accepted --> Superseded["3. Superseded"]
|
|
25
|
+
Accepted --> Deprecated["Deprecated"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
1. **Proposed**: Under RFC or active peer discussion. Implementation has not started.
|
|
29
|
+
2. **Accepted**: Approved by technical leads/team. Implementation adheres to this decision.
|
|
30
|
+
3. **Rejected**: Decision evaluated and abandoned. Preserved to prevent re-hashing settled debates.
|
|
31
|
+
4. **Superseded**: Replaced by a newer ADR (e.g., "Superseded by ADR-0012").
|
|
32
|
+
5. **Deprecated**: The feature or architectural pattern has been retired completely.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 3. Storage Formats
|
|
37
|
+
|
|
38
|
+
Projects may choose either a **Unified Log** (ideal for smaller or harness-managed repos) or **Modular Files** (ideal for enterprise systems).
|
|
39
|
+
|
|
40
|
+
### Option A: Unified Log (`decisions.md`)
|
|
41
|
+
Maintained under `.grooveskills/memory/decisions.md` or `docs/decisions.md`:
|
|
42
|
+
|
|
43
|
+
```markdown
|
|
44
|
+
# Architecture Decision Records (ADR)
|
|
45
|
+
|
|
46
|
+
| Date | Title | Decision | Reason & Consequences |
|
|
47
|
+
| :--- | :--- | :--- | :--- |
|
|
48
|
+
| 2026-11-01 | Adopt Harness | Initialized `.grooveskills/` | Standardized agent memory and 3-lane execution |
|
|
49
|
+
| 2026-11-03 | Fail-Fast Env Validation | Adopted Valibot at boot time | Catches missing env vars before startup; slight schema build overhead |
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Option B: Modular Numbered Records (`docs/architecture/adr/XXXX-title.md`)
|
|
53
|
+
Numbered sequentially with zero-padded prefixes:
|
|
54
|
+
- `0001-use-hono-over-express.md`
|
|
55
|
+
- `0002-cache-aside-with-redis.md`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 4. Canonical ADR Template (Markdown)
|
|
60
|
+
|
|
61
|
+
Use the following template when authoring dedicated ADR files:
|
|
62
|
+
|
|
63
|
+
```markdown
|
|
64
|
+
# ADR-0004: Decouple External Providers via Pure Domain Ports
|
|
65
|
+
|
|
66
|
+
- **Status**: Accepted
|
|
67
|
+
- **Date**: 2026-11-03
|
|
68
|
+
- **Deciders**: Engineering Team
|
|
69
|
+
- **Consulted**: Tech Leads, Security Team
|
|
70
|
+
- **Supersedes**: ADR-0001 (Direct Vendor SDK Calls)
|
|
71
|
+
|
|
72
|
+
## Context and Problem Statement
|
|
73
|
+
Our application previously instantiated third-party SDKs (Stripe, Resend) directly inside HTTP controllers and actions. This created tight coupling, prevented automated unit testing with in-memory fakes, and made switching cloud providers painful.
|
|
74
|
+
|
|
75
|
+
## Decision Drivers
|
|
76
|
+
- Need for deterministic unit testing without network mocks or live API keys.
|
|
77
|
+
- Need to isolate credential management into dedicated infrastructure wrappers.
|
|
78
|
+
- Need to support swapping email providers (Resend -> AWS SES) with zero changes to business logic.
|
|
79
|
+
|
|
80
|
+
## Considered Options
|
|
81
|
+
1. **Direct SDK usage** (Status quo) - Minimal initial boilerplate, but fragile and untestable.
|
|
82
|
+
2. **Repository pattern for all APIs** - Misleading abstraction for non-database operations.
|
|
83
|
+
3. **Domain Ports & Swappable Adapters** (Chosen) - Actions depend on pure interfaces.
|
|
84
|
+
|
|
85
|
+
## Decision Outcome
|
|
86
|
+
Chosen Option: **Domain Ports & Swappable Adapters**. We define pure domain interfaces (e.g. `PaymentGatewayPort`) under `src/domains/<domain>/` and concrete implementations under `src/infrastructure/adapters/`.
|
|
87
|
+
|
|
88
|
+
### Positive Consequences
|
|
89
|
+
- Business actions remain 100% pure and independent of vendor libraries.
|
|
90
|
+
- Unit tests use fast `MemoryPaymentGatewayAdapter` executing in milliseconds.
|
|
91
|
+
- Provider changes require writing only one new adapter file.
|
|
92
|
+
|
|
93
|
+
### Negative Consequences / Trade-offs
|
|
94
|
+
- Requires writing an explicit port interface and DTO mapping per external provider.
|
|
95
|
+
- Increases the number of files by one port and one wrapper per integration.
|
|
96
|
+
|
|
97
|
+
## Links & References
|
|
98
|
+
- Relative spec link: [Architecture Overview](../system-design.md)
|
|
99
|
+
- Domain Ports Guide: [External Service Adapters Guide](../../guides/adapters.md)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 5. Review & Authoring Checklist
|
|
105
|
+
|
|
106
|
+
Before submitting an ADR:
|
|
107
|
+
- [ ] Is it written in clear, concise English?
|
|
108
|
+
- [ ] Does it reference relevant documents using relative POSIX paths?
|
|
109
|
+
- [ ] Are trade-offs (negative consequences) explicitly stated, not hidden?
|
|
110
|
+
- [ ] Is the status clearly marked?
|
|
111
|
+
- [ ] Does the file stay well under the 200-line limit?
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# In-Code Comments and Docstrings Standard
|
|
2
|
+
|
|
3
|
+
Guidelines for writing intent-focused, maintainable code comments and docstrings in TypeScript, Python, and Rust.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Golden Philosophy: Document the "Why", Not the "What"
|
|
8
|
+
|
|
9
|
+
Comments must justify non-obvious engineering decisions, document invariants, and highlight failure modes. They must never repeat what the code already says.
|
|
10
|
+
|
|
11
|
+
### Anti-Patterns to Avoid
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
// ❌ WRONG: Useless repetition of obvious code
|
|
15
|
+
// Increment counter by one
|
|
16
|
+
counter += 1;
|
|
17
|
+
|
|
18
|
+
// ❌ WRONG: Commented-out dead code (delete it; git keeps history)
|
|
19
|
+
// const oldRate = fetchV1Rate(currency);
|
|
20
|
+
// return oldRate * 1.05;
|
|
21
|
+
|
|
22
|
+
// ❌ WRONG: Speculative "maybe one day" comments
|
|
23
|
+
// TODO: Maybe we can convert this to a microservice in 2028 if needed
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Clean Patterns to Adopt
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// ✅ CORRECT: Explains the underlying constraint / rationale
|
|
30
|
+
// Stripe requires idempotency keys to expire within 24 hours. We pin the key
|
|
31
|
+
// to the daily order UUID to avoid duplicate authorization calls on network retry.
|
|
32
|
+
const idempotencyKey = `ord_${order.id}_${txDate}`;
|
|
33
|
+
|
|
34
|
+
// ✅ CORRECT: Documents intentional performance trade-off
|
|
35
|
+
// Using a bitmask instead of Set<string> here because this validation executes
|
|
36
|
+
// inside the hot per-packet loop processing ~100k records/second.
|
|
37
|
+
const permissionsMask = roleFlags & REQUIRED_FLAGS;
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 2. TypeScript / JavaScript (TSDoc & JSDoc)
|
|
43
|
+
|
|
44
|
+
Use standard TSDoc for public module APIs, interfaces, and actions. Keep types inside TypeScript; do not duplicate type annotations inside docstrings.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
export interface CacheRememberOptions {
|
|
48
|
+
ttlSeconds: number;
|
|
49
|
+
tags?: string[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Executes the fallback fetcher and caches the result when a cache miss occurs.
|
|
54
|
+
*
|
|
55
|
+
* Employs a single-flight mutex per key to prevent thundering-herd cache stampedes
|
|
56
|
+
* when multiple concurrent requests query an expired key simultaneously.
|
|
57
|
+
*
|
|
58
|
+
* @param key - Unique cache partition key.
|
|
59
|
+
* @param ttlSeconds - Cache TTL in seconds.
|
|
60
|
+
* @param fetcher - Asynchronous producer function executed on miss.
|
|
61
|
+
* @returns The cached or freshly computed value.
|
|
62
|
+
* @throws {AppError} If fetcher throws or cache connection drops.
|
|
63
|
+
*/
|
|
64
|
+
export async function remember<T>(
|
|
65
|
+
key: string,
|
|
66
|
+
ttlSeconds: number,
|
|
67
|
+
fetcher: () => Promise<T>
|
|
68
|
+
): Promise<T> {
|
|
69
|
+
// Implementation...
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 3. Python (PEP 257 & Google Style)
|
|
76
|
+
|
|
77
|
+
Use triple-quoted strings for modules, classes, and functions. Document exceptions raised and edge-case behavior.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
"""Rate limiting domain service with sliding-window support."""
|
|
81
|
+
|
|
82
|
+
from typing import Protocol
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class RateLimiterPort(Protocol):
|
|
86
|
+
"""Domain contract for rate limiting adapters."""
|
|
87
|
+
|
|
88
|
+
def is_allowed(self, client_id: str, limit: int, window_sec: int) -> bool:
|
|
89
|
+
"""Evaluate if the client request stays within the sliding window limit.
|
|
90
|
+
|
|
91
|
+
Uses a Redis Sorted Set under the hood to ensure atomic score increments.
|
|
92
|
+
Fails open (returns True) if the Redis node is unreachable to prevent
|
|
93
|
+
taking down downstream checkout APIs during cache outages.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
client_id: Unique IP, API token, or user account identifier.
|
|
97
|
+
limit: Maximum requests allowed in the sliding window.
|
|
98
|
+
window_sec: Window duration in seconds.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if the call is allowed, False if limit is exceeded.
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 4. Rust (Rustdoc: `///` and `//!`)
|
|
109
|
+
|
|
110
|
+
Use `//!` for module-level documentation and `///` for structs, enums, and functions. Include doc tests or failure conditions.
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
//! Cryptographic HMAC webhook signature verifier.
|
|
114
|
+
//!
|
|
115
|
+
//! Provides zero-allocation constant-time verification for incoming
|
|
116
|
+
//! payment webhooks.
|
|
117
|
+
|
|
118
|
+
use subtle::ConstantTimeEq;
|
|
119
|
+
|
|
120
|
+
/// Verifies whether an incoming payload matches the expected HMAC-SHA256 signature.
|
|
121
|
+
///
|
|
122
|
+
/// Compares signatures using constant-time comparison to protect against
|
|
123
|
+
/// timing attacks on the cryptographic hash digest.
|
|
124
|
+
///
|
|
125
|
+
/// # Errors
|
|
126
|
+
///
|
|
127
|
+
/// Returns `Err(WebhookError::InvalidSignature)` if the digest lengths
|
|
128
|
+
/// mismatch or if the signature does not authenticate the payload.
|
|
129
|
+
pub fn verify_signature(
|
|
130
|
+
secret: &[u8],
|
|
131
|
+
payload: &[u8],
|
|
132
|
+
expected_hex_sig: &str,
|
|
133
|
+
) -> Result<(), WebhookError> {
|
|
134
|
+
// Implementation...
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## 5. Commenting Checklist
|
|
141
|
+
|
|
142
|
+
Before committing code:
|
|
143
|
+
- [ ] Are all comments and docstrings written in English?
|
|
144
|
+
- [ ] Are all commented-out dead code blocks deleted?
|
|
145
|
+
- [ ] Do comments answer "Why was it built this way?" rather than "What does this line do?"?
|
|
146
|
+
- [ ] For public functions: are edge cases, thrown exceptions, or fallbacks documented?
|
|
147
|
+
- [ ] Does the file stay under 200 lines?
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Relative Paths and Mermaid Diagrams Standard
|
|
2
|
+
|
|
3
|
+
Reference guide for constructing portable markdown links and bulletproof Mermaid diagrams.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Relative POSIX Links
|
|
8
|
+
|
|
9
|
+
Documentation must remain navigable across GitHub/GitLab web view, local IDEs, CLI tools, and static site generators without relying on machine-specific absolute paths.
|
|
10
|
+
|
|
11
|
+
### 1.1 Strict Rules
|
|
12
|
+
|
|
13
|
+
1. **Always Relative**:
|
|
14
|
+
- ✅ Correct: `[Architecture Overview](../architecture/overview.md)`
|
|
15
|
+
- ❌ Wrong: `[Architecture Overview](/Users/alice/projects/app/docs/architecture/overview.md)`
|
|
16
|
+
- ❌ Wrong: `[Architecture Overview](file:///C:/projects/app/docs/architecture/overview.md)`
|
|
17
|
+
2. **POSIX Forward Slashes Only**:
|
|
18
|
+
- Always use `/` even in Windows environments (`./guides/setup.md`, never `.\guides\setup.md`).
|
|
19
|
+
3. **Explicit Dot Notation**:
|
|
20
|
+
- Siblings or subfolders: prefix with `./` (e.g., `./references/diagrams.md`).
|
|
21
|
+
- Parents: prefix with `../` (e.g., `../../README.md`).
|
|
22
|
+
4. **Anchor Links**:
|
|
23
|
+
- Match GitHub header slug generation: lowercase, hyphen-separated, strip special symbols.
|
|
24
|
+
- Example: `[Jump to Section](#3-mermaid-syntax-safety)`.
|
|
25
|
+
|
|
26
|
+
### 1.2 Path Resolution Reference
|
|
27
|
+
|
|
28
|
+
Given this file layout:
|
|
29
|
+
```text
|
|
30
|
+
repo/
|
|
31
|
+
├── README.md
|
|
32
|
+
├── docs/
|
|
33
|
+
│ ├── overview.md
|
|
34
|
+
│ └── architecture/
|
|
35
|
+
│ ├── system-design.md
|
|
36
|
+
│ └── adr/
|
|
37
|
+
│ └── 0001-queue.md
|
|
38
|
+
└── src/
|
|
39
|
+
└── index.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Source File | Destination File | Correct Markdown Link |
|
|
43
|
+
| :--- | :--- | :--- |
|
|
44
|
+
| `README.md` | `docs/overview.md` | `[Overview](./docs/overview.md)` |
|
|
45
|
+
| `docs/overview.md` | `docs/architecture/system-design.md` | `[System Design](./architecture/system-design.md)` |
|
|
46
|
+
| `docs/architecture/adr/0001-queue.md` | `docs/architecture/system-design.md` | `[System Design](../system-design.md)` |
|
|
47
|
+
| `docs/architecture/adr/0001-queue.md` | `README.md` | `[Root README](../../../README.md)` |
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Mermaid Diagram Construction
|
|
52
|
+
|
|
53
|
+
Mermaid diagrams render directly in GitHub and modern editors. Invalid syntax breaks the entire document view. Follow these guidelines to ensure resilient diagrams.
|
|
54
|
+
|
|
55
|
+
### 2.1 Direction and Layout
|
|
56
|
+
- Prefer `flowchart TD` (Top-to-Bottom) for hierarchical workflows or pipelines.
|
|
57
|
+
- Prefer `flowchart LR` (Left-to-Right) for message busses, data processing streams, or step sequences.
|
|
58
|
+
|
|
59
|
+
### 2.2 Node Names and Safe Quoting
|
|
60
|
+
Mermaid syntax uses special characters like `(`, `[`, `{`, `<`, `>`, `"`, and `:` for shape definitions. Placing these inside unquoted labels triggers parsing failures.
|
|
61
|
+
|
|
62
|
+
**The Golden Rule**: Always wrap the label string in double quotes `["..."]`.
|
|
63
|
+
|
|
64
|
+
```mermaid
|
|
65
|
+
flowchart TD
|
|
66
|
+
%% ✅ CORRECT: All node texts and edge labels safely quoted
|
|
67
|
+
Client["Client Application (Web / Mobile)"]
|
|
68
|
+
Gateway["API Gateway: Port 8080"]
|
|
69
|
+
Queue[("Cloudflare Queue: events-stream")]
|
|
70
|
+
Worker["Queue Worker: SyncProcessor"]
|
|
71
|
+
|
|
72
|
+
Client -->|"POST /v1/events"| Gateway
|
|
73
|
+
Gateway -->|"Publish message"| Queue
|
|
74
|
+
Queue -->|"Batch consume"| Worker
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2.3 Subgraphs with Safe Identifiers
|
|
78
|
+
Always give subgraphs explicit alphanumeric identifiers before their quoted titles:
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart LR
|
|
82
|
+
subgraph IngestionBoundary["Ingestion Domain"]
|
|
83
|
+
IngestController["IngestController"]
|
|
84
|
+
VerifyHmac["VerifyHmacAction"]
|
|
85
|
+
IngestController --> VerifyHmac
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
subgraph ProcessingBoundary["Async Processing Domain"]
|
|
89
|
+
WorkerJob["ProcessJobWorker"]
|
|
90
|
+
DbStore[("PostgreSQL Store")]
|
|
91
|
+
WorkerJob -->|"Write audit"| DbStore
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
VerifyHmac -->|"Enqueue task"| WorkerJob
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2.4 Sequence Diagrams
|
|
98
|
+
For sequence flows, quote participants with descriptive names:
|
|
99
|
+
|
|
100
|
+
```mermaid
|
|
101
|
+
sequenceDiagram
|
|
102
|
+
autonumber
|
|
103
|
+
actor User as "Client App"
|
|
104
|
+
participant API as "API Controller"
|
|
105
|
+
participant Policy as "RBAC Policy Engine"
|
|
106
|
+
participant DB as "Database Cluster"
|
|
107
|
+
|
|
108
|
+
User->>+API: POST /organizations/42/members
|
|
109
|
+
API->>+Policy: canManageOrg(actor, orgId)
|
|
110
|
+
Policy-->>-API: Allowed (true)
|
|
111
|
+
API->>+DB: INSERT INTO org_members ...
|
|
112
|
+
DB-->>-API: Result(201 Created)
|
|
113
|
+
API-->>-User: 201 Created (MemberDTO)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2.5 Common Pitfalls & Anti-Patterns
|
|
117
|
+
1. **Unquoted brackets inside shapes**:
|
|
118
|
+
- ❌ Broken: `A[User [Admin]]`
|
|
119
|
+
- ✅ Correct: `A["User [Admin]"]`
|
|
120
|
+
2. **Missing line breaks**:
|
|
121
|
+
- Keep node declarations on separate lines for readability.
|
|
122
|
+
3. **Overly dense diagrams**:
|
|
123
|
+
- If a diagram exceeds 20 nodes, break it down into modular sub-flowcharts across topic guides.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: env-feature-flags
|
|
3
|
+
description: Architectural standards for fail-fast environment validation with Valibot and decoupled Feature Flags wrappers with swappable adapters (PostHog, LaunchDarkly, Unleash, In-Memory). Guarantees that applications crash at boot time on invalid config and allows toggling features without coupling code to vendor SDKs. TRIGGER when reading environment variables, structuring process.env, configuring feature flags, or running A/B experiments.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fail-Fast Environment Validation & Feature Flags Adapters
|
|
7
|
+
|
|
8
|
+
Opinionated architectural standard combining **Fail-Fast Environment Schema Validation** (using Valibot) and **Feature Flags Adapters** (PostHog, LaunchDarkly, In-Memory).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Architectural Invariants
|
|
13
|
+
|
|
14
|
+
1. **Fail-Fast at Startup**:
|
|
15
|
+
- Environment variables must be validated on application boot. If a required secret or URL is missing or malformed, the process exits immediately with a clean diagnostic message.
|
|
16
|
+
- Raw `process.env` is NEVER accessed directly inside business actions or controllers. All code imports the strongly typed `env` singleton.
|
|
17
|
+
2. **Feature Flags as a Domain Port**:
|
|
18
|
+
- Business actions evaluate feature availability through `FeatureFlagPort`.
|
|
19
|
+
- Vendor SDKs (PostHog, LaunchDarkly, Unleash) remain isolated inside infrastructure adapters.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Directory Layout
|
|
24
|
+
|
|
25
|
+
```text
|
|
26
|
+
src/
|
|
27
|
+
├── config/
|
|
28
|
+
│ └── env.ts # Fail-fast Valibot environment parser (<70 lines)
|
|
29
|
+
├── domains/
|
|
30
|
+
│ └── feature-flags/
|
|
31
|
+
│ ├── feature-flag.port.ts # Domain interface for flags (<40 lines)
|
|
32
|
+
│ └── flag-keys.ts # Strongly typed flag registry (<30 lines)
|
|
33
|
+
└── infrastructure/
|
|
34
|
+
└── adapters/
|
|
35
|
+
└── feature-flags/
|
|
36
|
+
├── posthog-flags.adapter.ts # PostHog implementation (<70 lines)
|
|
37
|
+
├── launchdarkly-flags.adapter.ts # LaunchDarkly implementation (<70 lines)
|
|
38
|
+
└── memory-flags.adapter.ts # In-memory testing flags fake (<50 lines)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 3. Fail-Fast Environment Config (`src/config/env.ts`)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import * as v from "valibot";
|
|
47
|
+
|
|
48
|
+
const EnvSchema = v.object({
|
|
49
|
+
NODE_ENV: v.fallback(v.picklist(["development", "production", "test"]), "development"),
|
|
50
|
+
PORT: v.fallback(v.pipe(v.string(), v.transform(Number)), 3000),
|
|
51
|
+
DATABASE_URL: v.pipe(v.string(), v.url("DATABASE_URL debe ser una URL válida.")),
|
|
52
|
+
API_SECRET_KEY: v.pipe(v.string(), v.minLength(32, "API_SECRET_KEY requiere al menos 32 caracteres.")),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function loadEnv() {
|
|
56
|
+
const result = v.safeParse(EnvSchema, process.env);
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
console.error("❌ Error fatal en variables de entorno:");
|
|
59
|
+
for (const issue of result.issues) {
|
|
60
|
+
console.error(` - ${issue.path?.[0]?.key}: ${issue.message}`);
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
return result.output;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const env = loadEnv();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. Feature Flag Port (`src/domains/feature-flags/feature-flag.port.ts`)
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
export type FeatureFlagKey = "new-checkout-flow" | "ai-copilot" | "export-pdf";
|
|
76
|
+
|
|
77
|
+
export interface FeatureFlagEvaluationContext {
|
|
78
|
+
distinctId: string;
|
|
79
|
+
organizationId?: string;
|
|
80
|
+
properties?: Record<string, unknown>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface FeatureFlagPort {
|
|
84
|
+
isEnabled(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<boolean>;
|
|
85
|
+
getVariant<T = string>(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<T | null>;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 5. Detailed Adapters Reference
|
|
92
|
+
|
|
93
|
+
For real implementations of PostHog, LaunchDarkly, and In-Memory Feature Flag adapters, see:
|
|
94
|
+
- `references/env-feature-flags-examples.md`
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Implementation Reference: Env & Feature Flag Adapters
|
|
2
|
+
|
|
3
|
+
Production implementations of PostHog, LaunchDarkly, and In-Memory testing feature flag adapters.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. PostHog Feature Flags Adapter (`posthog-node`)
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// src/infrastructure/adapters/feature-flags/posthog-flags.adapter.ts
|
|
11
|
+
import { PostHog } from "posthog-node";
|
|
12
|
+
import type { FeatureFlagPort, FeatureFlagKey, FeatureFlagEvaluationContext } from "@/domains/feature-flags/feature-flag.port";
|
|
13
|
+
|
|
14
|
+
export class PostHogFeatureFlagAdapter implements FeatureFlagPort {
|
|
15
|
+
constructor(private client: PostHog) {}
|
|
16
|
+
|
|
17
|
+
async isEnabled(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<boolean> {
|
|
18
|
+
const distinctId = context?.distinctId ?? "anonymous";
|
|
19
|
+
const res = await this.client.isFeatureEnabled(key, distinctId, {
|
|
20
|
+
personProperties: context?.properties,
|
|
21
|
+
});
|
|
22
|
+
return Boolean(res);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getVariant<T = string>(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<T | null> {
|
|
26
|
+
const distinctId = context?.distinctId ?? "anonymous";
|
|
27
|
+
const variant = await this.client.getFeatureFlag(key, distinctId, {
|
|
28
|
+
personProperties: context?.properties,
|
|
29
|
+
});
|
|
30
|
+
return (variant as T) ?? null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. LaunchDarkly Feature Flags Adapter (`@launchdarkly/node-server-sdk`)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/infrastructure/adapters/feature-flags/launchdarkly-flags.adapter.ts
|
|
41
|
+
import type { LDClient } from "@launchdarkly/node-server-sdk";
|
|
42
|
+
import type { FeatureFlagPort, FeatureFlagKey, FeatureFlagEvaluationContext } from "@/domains/feature-flags/feature-flag.port";
|
|
43
|
+
|
|
44
|
+
export class LaunchDarklyFeatureFlagAdapter implements FeatureFlagPort {
|
|
45
|
+
constructor(private client: LDClient) {}
|
|
46
|
+
|
|
47
|
+
async isEnabled(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<boolean> {
|
|
48
|
+
const user = {
|
|
49
|
+
key: context?.distinctId ?? "anonymous",
|
|
50
|
+
custom: { organizationId: context?.organizationId, ...context?.properties },
|
|
51
|
+
};
|
|
52
|
+
return this.client.variation(key, user, false);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getVariant<T = string>(key: FeatureFlagKey, context?: FeatureFlagEvaluationContext): Promise<T | null> {
|
|
56
|
+
const user = {
|
|
57
|
+
key: context?.distinctId ?? "anonymous",
|
|
58
|
+
custom: { organizationId: context?.organizationId, ...context?.properties },
|
|
59
|
+
};
|
|
60
|
+
return this.client.variation(key, user, null);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 3. In-Memory Testing Flags Adapter
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// src/infrastructure/adapters/feature-flags/memory-flags.adapter.ts
|
|
71
|
+
import type { FeatureFlagPort, FeatureFlagKey } from "@/domains/feature-flags/feature-flag.port";
|
|
72
|
+
|
|
73
|
+
export class MemoryFeatureFlagAdapter implements FeatureFlagPort {
|
|
74
|
+
private flags = new Map<string, boolean>();
|
|
75
|
+
private variants = new Map<string, unknown>();
|
|
76
|
+
|
|
77
|
+
setFlag(key: FeatureFlagKey, enabled: boolean): void {
|
|
78
|
+
this.flags.set(key, enabled);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setVariant(key: FeatureFlagKey, variant: unknown): void {
|
|
82
|
+
this.variants.set(key, variant);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async isEnabled(key: FeatureFlagKey): Promise<boolean> {
|
|
86
|
+
return this.flags.get(key) ?? false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async getVariant<T = string>(key: FeatureFlagKey): Promise<T | null> {
|
|
90
|
+
return (this.variants.get(key) as T) ?? null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|