Skip to content

Commit 72b8c62

Browse files
committed
docs(rules): add Diataxis, doc voice guidance, and Go hardening notes
- Add Diataxis framework + lightweight templates to documentation rules - Add doc voice guidance to avoid "you/your" phrasing - Add Go HTTP client hardening guidance and align Go skill + references
1 parent 56b5bf6 commit 72b8c62

6 files changed

Lines changed: 295 additions & 1 deletion

File tree

rules/210-go.mdc

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,58 @@ tlsConfig := &tls.Config{
333333
_, err := db.ExecContext(ctx, "INSERT INTO users (email) VALUES ($1)", email)
334334
```
335335

336+
### HTTP Client Hardening (MUST)
337+
338+
When calling network APIs (internal or external), apply these non-negotiable checks:
339+
340+
- **MUST** bound response body reads before `io.ReadAll` / decode.
341+
- **MUST** cap server-provided retry hints (for example `Retry-After`) to a configured maximum.
342+
- **MUST** keep "raw"/"escape hatch" API naming and docs aligned with actual behavior.
343+
- **MUST NOT** expose exported mutable global registries (maps/slices) used in policy/guardrails.
344+
- **MUST** keep test-only helpers in `*_test.go` so they do not compile into production binaries.
345+
346+
```go
347+
// GOOD: Bound network body reads
348+
const maxResponseBodyBytes = 10 << 20 // 10 MiB
349+
350+
func readBodySafe(r io.Reader) ([]byte, error) {
351+
limited := io.LimitReader(r, maxResponseBodyBytes+1)
352+
body, err := io.ReadAll(limited)
353+
if err != nil {
354+
return nil, fmt.Errorf("read response body: %w", err)
355+
}
356+
if len(body) > maxResponseBodyBytes {
357+
return nil, fmt.Errorf("response body exceeds %d bytes", maxResponseBodyBytes)
358+
}
359+
return body, nil
360+
}
361+
362+
// GOOD: Cap Retry-After-derived delays
363+
func retryDelay(attempt int, retryAfter string, maxDelay time.Duration) time.Duration {
364+
if d, ok := parseRetryAfter(retryAfter); ok {
365+
if d > maxDelay {
366+
return maxDelay
367+
}
368+
return d
369+
}
370+
return backoff(attempt, maxDelay)
371+
}
372+
373+
// BAD: Exported mutable global policy registry
374+
var KnownAccounts = map[string]Account{} // External packages can mutate this
375+
376+
// GOOD: Unexported registry + copy accessor
377+
var knownAccounts = map[string]Account{}
378+
379+
func KnownAccounts() map[string]Account {
380+
out := make(map[string]Account, len(knownAccounts))
381+
for k, v := range knownAccounts {
382+
out[k] = v
383+
}
384+
return out
385+
}
386+
```
387+
336388
---
337389

338390
## 2. Simplicity & Idiomatic Go
@@ -1354,6 +1406,90 @@ go test -race ./...
13541406
govulncheck ./...
13551407
```
13561408

1409+
### CI Gates (Hardening Extension - Recommended)
1410+
1411+
Add semantic checks for rules that standard linters miss.
1412+
1413+
```bash
1414+
# Semgrep policy checks (custom)
1415+
semgrep --config .semgrep/go-hardening.yaml ./...
1416+
```
1417+
1418+
**Sample pre-commit hook:**
1419+
1420+
```yaml
1421+
repos:
1422+
- repo: https://github.com/returntocorp/semgrep
1423+
rev: v1.129.0
1424+
hooks:
1425+
- id: semgrep
1426+
name: semgrep-go-hardening
1427+
args: ["--config", ".semgrep/go-hardening.yaml"]
1428+
files: "\\.go$"
1429+
```
1430+
1431+
**Sample `.semgrep/go-hardening.yaml`:**
1432+
1433+
```yaml
1434+
rules:
1435+
- id: go-unbounded-network-readall
1436+
message: "Bound network body reads with io.LimitReader or MaxBytesReader"
1437+
severity: ERROR
1438+
languages: [go]
1439+
patterns:
1440+
- pattern: io.ReadAll($BODY)
1441+
- pattern-not: io.ReadAll(io.LimitReader($BODY, ...))
1442+
1443+
- id: go-retry-after-without-cap
1444+
message: "Cap Retry-After derived delay to configured max delay"
1445+
severity: WARNING
1446+
languages: [go]
1447+
patterns:
1448+
- pattern: |
1449+
if $DELAY, $OK := parseRetryAfter(...); $OK {
1450+
return $DELAY
1451+
}
1452+
1453+
- id: go-exported-mutable-registry
1454+
message: "Avoid exported mutable map/slice globals for policy/guardrail registries"
1455+
severity: WARNING
1456+
languages: [go]
1457+
patterns:
1458+
- pattern-either:
1459+
- pattern: var $X = map[$K]$V{...}
1460+
- pattern: var $X = []$T{...}
1461+
- metavariable-regex:
1462+
metavariable: $X
1463+
regex: "^[A-Z].*"
1464+
1465+
- id: go-testing-helper-in-non-test-file
1466+
message: "Move test helpers/imports into *_test.go files"
1467+
severity: WARNING
1468+
languages: [go]
1469+
paths:
1470+
exclude:
1471+
- "*_test.go"
1472+
patterns:
1473+
- pattern-either:
1474+
- pattern: import "testing"
1475+
- pattern: import "net/http/httptest"
1476+
```
1477+
1478+
### How To Add New Hardening Checks (Rules + Skills)
1479+
1480+
When adding a new Go hardening guardrail, update both the rule and skill
1481+
layers so guidance and enforcement stay aligned:
1482+
1483+
1. Add the normative requirement under **HTTP Client Hardening (MUST)** in this
1484+
file (`rules/210-go.mdc`).
1485+
2. Add/extend a detection rule in the sample semgrep section (and in your repo's
1486+
actual `.semgrep/go-hardening.yaml` if present).
1487+
3. Add an anti-pattern bullet under **Anti-Patterns (Automatic Rejection)**.
1488+
4. Mirror the operator-facing summary in
1489+
`skills/go-rust-systems/SKILL.md` under **Mandatory Hardening Add-On (Go)**.
1490+
5. Update at least one concrete example in `skills/go-rust-systems/references/`
1491+
so the new requirement is demonstrated in runnable code.
1492+
13571493
---
13581494

13591495
## 6. Common Mistakes & Anti-Patterns
@@ -2985,8 +3121,11 @@ func setupLogger(level string) *slog.Logger {
29853121
- Goroutines without lifecycle management
29863122
- Swallowed errors ("Don't just check errors, handle them gracefully")
29873123
- Global mutable state
3124+
- Unbounded network body reads (`io.ReadAll` without limit on response bodies)
3125+
- Uncapped server-derived retry/sleep delays (`Retry-After` without max cap)
29883126
- Logging secrets/PII
29893127
- Zero values that require initialization to be safe ("Make the zero value useful")
3128+
- Test-only helper code in non-`*_test.go` files
29903129

29913130
### Code Smells
29923131
- Channels for simple mutual exclusion (use mutexes)

rules/810-documentation.mdc

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,93 @@ files:
1818

1919
## Documentation Strategy
2020

21+
### Diataxis Framework (Recommended)
22+
23+
Use Diataxis to keep documentation purpose-specific. Each document should have one primary mode.
24+
25+
| Mode | Primary user need | Typical question it answers |
26+
|---|---|---|
27+
| Tutorial | Learn by doing | "How do I get started?" |
28+
| How-to guide | Complete a specific task | "How do I configure X in Y?" |
29+
| Reference | Look up facts quickly | "What are all flags/options?" |
30+
| Explanation | Understand concepts and tradeoffs | "Why is this designed this way?" |
31+
32+
> [!IMPORTANT]
33+
> Do not mix documentation modes in one page unless there is a strong reason. If a page starts combining step-by-step onboarding with API option listings and architectural rationale, split it into separate pages and cross-link them.
34+
35+
### Diataxis Decision Guide
36+
37+
Before writing or editing docs, choose one mode first:
38+
39+
- If the reader is learning from scratch, write a **Tutorial**
40+
- If the reader is trying to finish a concrete task, write a **How-to guide**
41+
- If the reader needs authoritative details, write a **Reference**
42+
- If the reader needs understanding and rationale, write an **Explanation**
43+
44+
### Diataxis Templates (Lightweight)
45+
46+
#### Tutorial template
47+
48+
```markdown
49+
# <Goal-oriented tutorial title>
50+
51+
## What you will build
52+
53+
## Prerequisites
54+
55+
## Step-by-step walkthrough
56+
1. ...
57+
2. ...
58+
59+
## Verify success
60+
61+
## Next steps
62+
```
63+
64+
#### How-to guide template
65+
66+
```markdown
67+
# How to <specific task>
68+
69+
## Prerequisites
70+
71+
## Steps
72+
1. ...
73+
2. ...
74+
75+
## Validation
76+
77+
## Troubleshooting
78+
```
79+
80+
#### Reference template
81+
82+
```markdown
83+
# <Feature/API> reference
84+
85+
## Syntax / schema
86+
87+
## Parameters / fields
88+
89+
## Defaults and limits
90+
91+
## Examples
92+
```
93+
94+
#### Explanation template
95+
96+
```markdown
97+
# <Concept> explained
98+
99+
## Context
100+
101+
## Mental model
102+
103+
## Tradeoffs
104+
105+
## Related decisions and alternatives
106+
```
107+
21108
### When to Use Documentation Websites
22109

23110
**Use documentation websites** when:
@@ -147,6 +234,16 @@ docs/
147234
- Define technical terms
148235
- Use examples liberally
149236

237+
### Voice (Avoid "you/your")
238+
239+
Prefer **neutral** or **imperative** phrasing over second-person voice.
240+
241+
- **Preferred**: "Run `make test`", "Set `ENV=prod`", "The operator should rotate keys weekly"
242+
- **Avoid**: "You should run `make test`", "Make sure your environment is set to prod"
243+
244+
> [!NOTE]
245+
> This improves consistency across audiences (end-users, operators, reviewers) and makes docs read less like a conversation.
246+
150247
### Punctuation
151248

152249
**Never use em dashes (—) or en dashes (–). Use hyphens (-) instead.**

skills/documentation-standards/SKILL.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,22 @@ description: Documentation best practices including Markdown formatting, Mermaid
1212
3. **Show, Don't Just Tell**: Use examples and diagrams
1313
4. **Consistent Format**: Follow established patterns
1414

15+
## Voice
16+
17+
Prefer neutral/imperative phrasing - avoid "you/your" in professional docs.
18+
Canonical guidance: `rules/810-documentation.mdc`.
19+
20+
## Diataxis Quick Guide
21+
22+
Use one primary documentation mode per page:
23+
24+
- **Tutorial** - learning by doing
25+
- **How-to guide** - task completion
26+
- **Reference** - factual lookup
27+
- **Explanation** - concepts and rationale
28+
29+
Canonical Diataxis guidance lives in `rules/810-documentation.mdc`. Keep this skill concise and link back to the rule instead of duplicating detailed standards.
30+
1531
## README Structure
1632

1733
```markdown

skills/go-rust-systems/SKILL.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,31 @@ func TestAdd(t *testing.T) {
7878
}
7979
```
8080

81+
### Mandatory Hardening Add-On (Go)
82+
83+
For HTTP/API client code, always apply the hardening checks from
84+
`rules/210-go.mdc`:
85+
86+
- Bound response reads before `io.ReadAll`
87+
- Cap `Retry-After` and server-derived delays
88+
- Avoid exported mutable policy/guardrail registries
89+
- Keep test helpers in `*_test.go`
90+
91+
Use semgrep + pre-commit checks for these patterns because standard linting
92+
does not catch all of them reliably.
93+
94+
### How To Maintain Rule/Skill Parity
95+
96+
When you add a new Go hardening expectation:
97+
98+
1. Add the requirement to `rules/210-go.mdc` under **HTTP Client Hardening (MUST)**.
99+
2. Add detection guidance (semgrep/pre-commit) in the same rule file.
100+
3. Mirror the short operational summary in this `SKILL.md` section.
101+
4. Update at least one concrete reference example under
102+
`skills/go-rust-systems/references/`.
103+
104+
This keeps policy (rules), agent behavior (skill), and examples in sync.
105+
81106
## Rust Quick Reference
82107

83108
### Essential Commands

skills/go-rust-systems/references/go-aws-integration.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,18 @@ func NewS3Client(ctx context.Context, region string) (*s3.Client, error) {
3838

3939
```go
4040
import (
41+
"errors"
42+
"fmt"
43+
"io"
44+
"github.com/aws/aws-sdk-go-v2/aws"
4145
"github.com/aws/aws-sdk-go-v2/service/s3"
4246
"github.com/aws/aws-sdk-go-v2/service/s3/types"
4347
"github.com/aws/smithy-go"
4448
)
4549

4650
func GetObject(ctx context.Context, client *s3.Client, bucket, key string) ([]byte, error) {
51+
const maxObjectReadBytes = 10 << 20 // 10 MiB safety bound
52+
4753
result, err := client.GetObject(ctx, &s3.GetObjectInput{
4854
Bucket: aws.String(bucket),
4955
Key: aws.String(key),
@@ -63,7 +69,14 @@ func GetObject(ctx context.Context, client *s3.Client, bucket, key string) ([]by
6369
}
6470
defer result.Body.Close()
6571

66-
return io.ReadAll(result.Body)
72+
body, err := io.ReadAll(io.LimitReader(result.Body, maxObjectReadBytes+1))
73+
if err != nil {
74+
return nil, fmt.Errorf("read object body: %w", err)
75+
}
76+
if len(body) > maxObjectReadBytes {
77+
return nil, fmt.Errorf("object body exceeds %d bytes", maxObjectReadBytes)
78+
}
79+
return body, nil
6780
}
6881
```
6982

skills/go-rust-systems/references/go-testing.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ func FuzzParseURL(f *testing.F) {
4141

4242
**Test Helpers:**
4343

44+
> [!IMPORTANT]
45+
> Keep helper utilities that import `testing`/`httptest` in `*_test.go` files.
46+
> This prevents test scaffolding from being compiled into production binaries.
47+
4448
```go
4549
func setupTestDB(t *testing.T) *sql.DB {
4650
t.Helper() // Marks this as a test helper

0 commit comments

Comments
 (0)