Skip to content

Commit 1360120

Browse files
authored
Merge pull request #595 from NHSDigital/feature/eja-add-custom-review-instructions
Create *.instructions.md for code review
2 parents a002164 + 0c369bc commit 1360120

4 files changed

Lines changed: 160 additions & 1 deletion

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
description: "Code review instructions for the eligibility-signposting-api project"
3+
applyTo: "**"
4+
excludeAgent: ["coding-agent"]
5+
---
6+
7+
# Code Review Instructions
8+
9+
Guidelines for the eligibility-signposting-api project — a serverless AWS Lambda + Flask eligibility rules engine.
10+
11+
## Review Priorities
12+
13+
### Critical (block merge)
14+
15+
- **Security**: Exposed secrets, PII leakage (especially NHS Numbers), missing header validation
16+
- **Correctness**: Logic errors in rules engine evaluation, incorrect operator behaviour, data corruption in DynamoDB
17+
- **Breaking Changes**: API contract changes to FHIR response models or request validation
18+
19+
### Important (requires discussion)
20+
21+
- **Code Quality**: SOLID violations, excessive duplication
22+
- **Test Coverage**: Missing tests for critical paths, new rules/operators, or edge cases
23+
- **Performance**: Unnecessary DynamoDB scans, missing caching, Lambda cold start regressions
24+
- **Architecture**: Deviations from established patterns (wireup DI, chain of responsibility, operator registry)
25+
26+
### Suggestion (non-blocking)
27+
28+
- **Readability**: Naming, simplification of complex logic
29+
- **Best Practices**: Minor convention deviations
30+
- **Documentation**: Missing or incomplete docstrings
31+
32+
## Security
33+
34+
- **PII handling**: NHS Numbers must never appear in logs or error messages. `TokenError` messages must be redacted. Verify new log statements do not leak person data.
35+
- **Secrets**: No API keys, tokens, or secrets in code. Use environment variables or AWS Secrets Manager.
36+
- **NHS Number hashing**: Lookups use HMAC-SHA512 via `HashingService` with secret rotation (AWSCURRENT → AWSPREVIOUS fallback).
37+
- **Header validation**: `NHSE-Product-ID` must be present (403 if missing). `nhs-login-nhs-number` must match path parameter.
38+
- **Security headers**: Responses must include `Cache-Control: no-store, private`, `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`.
39+
40+
## Architecture
41+
42+
- **Dependency injection**: Use wireup `@service` for all services, repos, and factories. Inject via `Injected[T]`, `Inject(qualifier=...)`, or `Inject(param=...)`. Never instantiate services manually.
43+
- **Chain of responsibility**: Processing follows `CohortEligibilityHandler → BaseEligibilityHandler → FilterRuleHandler → SuppressionRuleHandler`. Extend this chain for new steps.
44+
- **Operator registry**: New operators must extend `hamcrest.BaseMatcher` and register via the decorator-based `OperatorRegistry`.
45+
- **Pydantic models**: Use `Field(alias=...)` for JSON mapping, `field_validator`/`model_validator` for validation. Response models use camelCase aliases.
46+
- **FHIR compliance**: Error responses must use `OperationOutcome` models with `application/fhir+json` content type.
47+
- **Lambda reuse**: The Flask app is cached in `CacheManager` across invocations. Changes to app initialization must not break container reuse.
48+
49+
## Performance
50+
51+
- **DynamoDB**: Use `query()` with `KeyConditionExpression`, never `scan()`. Partition key is `NHS_NUMBER`, sort key discriminator is `ATTRIBUTE_TYPE`.
52+
- **S3 configuration loading**: Campaign configs load from S3 per request. Avoid unnecessary `list_objects` or `get_object` calls.
53+
- **Caching**: Feature toggles use `TTLCache` (300s). New caching should follow the same pattern with appropriate TTLs.
54+
- **Lambda cold starts**: Avoid heavy imports at module level. Keep wireup service graph lean.
55+
56+
## Audit Trail
57+
58+
- **Completeness**: New eligibility logic must call `AuditContext.append_audit_condition()` to record evaluation details.
59+
- **Firehose delivery**: Audit events use Pydantic `AuditEvent` models sent to Kinesis Firehose. Preserve the full audit data model.
60+
61+
## Terraform
62+
63+
- **Encryption**: All AWS resources (DynamoDB, S3, Lambda, Firehose, Secrets Manager) must use KMS CMK encryption.
64+
- **Environment parity**: Verify deletion protection and PITR are enabled for production/pre-production DynamoDB tables.
65+
- **Safety**: Terraform changes must not destroy or replace stateful resources (DynamoDB tables, S3 buckets) unintentionally.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
description: "Python coding standards for the eligibility-signposting-api project"
3+
applyTo: "**/*.py"
4+
---
5+
6+
# Python Coding Standards
7+
8+
## Naming Conventions
9+
10+
- `snake_case` for functions, variables, and module names.
11+
- `PascalCase` for class names.
12+
- `UPPER_SNAKE_CASE` for constants.
13+
- Prefix private methods and attributes with `_`.
14+
15+
## Code Style
16+
17+
- Line length limit: 120 characters (enforced by ruff).
18+
- Use type hints for all function signatures and return types.
19+
- Prefer `dataclass` for simple domain objects, Pydantic `BaseModel` for validated/serialized models.
20+
- Use `StrEnum` for string enumerations.
21+
- Avoid bare `except:` — catch specific exceptions.
22+
23+
## Error Handling
24+
25+
```python
26+
# Bad: silent failure
27+
try:
28+
person = repo.get(nhs_number)
29+
except Exception:
30+
pass
31+
32+
# Good: specific exceptions with context
33+
try:
34+
person = repo.get(nhs_number)
35+
except PersonNotFoundError:
36+
raise
37+
except ClientError as e:
38+
raise RepositoryError(f"Failed to query person table: {e}") from e
39+
```
40+
41+
## Dependency Injection (wireup)
42+
43+
- Decorate services with `@service`. Do not instantiate services manually.
44+
- Use `Inject(qualifier=...)` for AWS client disambiguation.
45+
- Use `Inject(param=...)` for configuration values.
46+
- Register factory functions with `@service` for boto3 clients.
47+
48+
```python
49+
# Good
50+
@service
51+
class MyService:
52+
def __init__(self, repo: Injected[MyRepo]) -> None:
53+
self._repo = repo
54+
55+
# Bad: manual instantiation
56+
class MyService:
57+
def __init__(self) -> None:
58+
self._repo = MyRepo()
59+
```
60+
61+
## Pydantic Models
62+
63+
- Use `Field(alias=...)` for JSON key mapping.
64+
- Use `field_validator` / `model_validator` for custom validation.
65+
- Response models must use camelCase aliases (`alias_generator=to_camel` or explicit aliases).
66+
- Use `model_dump(by_alias=True)` when serializing for API responses.
67+
68+
## Testing
69+
70+
- Use `pytest` with pyHamcrest assertions (`assert_that`, `is_`, `has_entries`, `contains_exactly`, etc.).
71+
- Use `brunns-matchers` for Werkzeug response assertions.
72+
- Use project auto-matchers (`BaseAutoMatcher`) for dataclass/Pydantic model assertions.
73+
- Use `polyfactory` (`DataclassFactory` / `ModelFactory`) for test data builders.
74+
- Mock AWS services with `moto`, not manual stubs.
75+
- Use `@pytest.mark.parametrize` for rule/operator test cases.
76+
77+
```python
78+
# Good: pyHamcrest with specific matchers
79+
def test_eligible_person_returns_eligible_status():
80+
result = evaluate(person, campaign)
81+
assert_that(result, is_(has_property("status", equal_to(Status.ELIGIBLE))))
82+
83+
# Bad: generic assert
84+
def test_eligible():
85+
result = evaluate(person, campaign)
86+
assert result is not None
87+
```
88+
89+
## Logging
90+
91+
- Use structured JSON logging via `python-json-logger`.
92+
- Never log NHS Numbers or other PII.
93+
- Include `request_id` via the `ContextVar` pattern for request tracing.

scripts/config/gitleaks.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ regexes = [
1616
]
1717

1818
[allowlist]
19-
paths = ['''.terraform.lock.hcl''', '''poetry.lock''', '''yarn.lock''']
19+
paths = ['''.terraform.lock.hcl''', '''poetry.lock''', '''yarn.lock''', '''.github/instructions/\*.instructions.md''']
2020
stopwords = ['''dummy_key''', '''dummy_secret''', '''192.0.0.1''', '''prance = "^25.4.8.0"''', '''25.4.8.0''']

scripts/config/vale/styles/config/vocabularies/words/accept.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Terraform
2727
toolchain
2828
Trufflehog
2929
Uncomment
30+
Werkzeug
3031
Syncytial
3132
pyenv
3233
colima

0 commit comments

Comments
 (0)