This guide provides essential information for Claude instances working with the LFX V2 Project Service codebase. It includes build commands, architecture patterns, and key technical decisions.
Central LFX skills:
/lfx-skills:lfx: cross-repo routing, "where does X live" questions, owner/peer repos, missing checkouts./lfx-skills:lfx-platform-architecture: platform composition, V2 service classes (native, wrapper, proxy, platform), write/read/access-check flows, cross-service responsibilities, NATS/KV ownership, handoff points across Self Serve, FGA, indexer, query, Heimdall, OpenFGA, Helm, ArgoCD.Repo-local project-service skills and docs:
/project-service-devat.claude/skills/project-service-dev/auto-attaches on Go and service paths and owns logging, errors, request context, pagination, generated-code boundary, NATS/KV publishing, tests, formatting, linting, and license headers for this repo./project-service-pr-readinesschecks pre-PR shape only: branch/JIRA/conventional commits/rebase/DCO+GPG/diff size/protected files./project-service-preflightruns the mechanical Go pre-PR pipeline after readiness: working tree, license, formatting, lint, build, tests, protected files, commit verification, generated-code freshness, and change summary.- Repo-local docs under
docs/own concrete subjects, payloads, emitted contracts, and domain behavior; this repo's chart owns project-service Helm values and templates.- If the central plugin is missing, install with
/plugin marketplace add linuxfoundation/lfx-skillsthen/plugin install lfx-skills@lfx-skills.
These rules apply to all contributors and AI agents working in this repo. Read this section first — it governs commit signing, message format, PR shape, and data hygiene across all work.
Every commit must carry both a GPG signature and a DCO sign-off:
git commit -s -S
# -s adds: Signed-off-by: Your Name <your@email.com>
# -S attaches a GPG signatureOne-time git config setup:
# Tell git which key to use (replace with your key ID from the command below)
gpg --list-secret-keys --keyid-format LONG
git config --global user.signingkey <YOUR_KEY_ID>
git config --global commit.gpgsign trueFor GPG key generation and uploading your public key to GitHub, see the GitHub GPG documentation.
If you forget the sign-off on the last commit, fix it with:
git commit --amend -sFollow Angular conventional commits. Jira tickets are optional — include one when the work has a known ticket, anywhere in the commit message (subject or body). The preferred placement is at the end of the subject line.
type(scope): summary [LFXV2-NNNN]
| Part | Rule |
|---|---|
type |
Required: feat | fix | docs | test | refactor | chore | build | ci | perf | style | revert |
(scope) |
Optional but recommended; lowercase, e.g. (project), (nats), (service) |
! |
Optional breaking-change marker, placed after scope: feat(api)!: |
summary |
Lowercase first letter, imperative mood, no trailing period; max 72 chars total on first line |
[LFXV2-NNNN] |
Optional; include when a Jira ticket exists — omit entirely if there is none |
Examples:
feat(project): add slug validation on create [LFXV2-1234]
fix(nats): handle stale KV entry on concurrent update [LFXV2-5678]
refactor(service): extract email renderer into dedicated package
docs: update NATS subject table in README
chore: bump golangci-lint to v1.62
feat(api)!: remove deprecated slug endpoint [LFXV2-9999]
The commit-msg hook enforces type format, lowercase summary, no trailing period, 72-char limit, and DCO sign-off (see Pre-commit Hooks below). Ticket inclusion and placement are conventions, not mechanically enforced.
Title — same pattern as the commit message:
type(scope): summary [LFXV2-NNNN]
Required description sections:
## Summary
What changed and why (2–4 bullet points).
## Ticket
[LFXV2-NNNN](https://linuxfoundation.atlassian.net/browse/LFXV2-NNNN)
*(Omit this section entirely if there is no associated Jira ticket.)*
## Changes
- Bullet describing each meaningful change made in this PR.
## API Changes
*(Required when the PR touches `api/`, `cmd/project-api/service_endpoint_*.go`,
or Goa design files. Omit this section entirely otherwise.)*
| Endpoint | Method | Change Type | Before | After | Breaking? |
|---|---|---|---|---|---|
| `/projects/:id` | PUT | New field | — | `display_name` | No |No production data may appear anywhere in committed files — code, tests, comments, or documentation. This covers real names, email addresses, organization names, user IDs, and domain names from production or staging environments.
Approved fake-data conventions for tests and mocks:
| Data type | Approved pattern |
|---|---|
| Names | Test User, Alice Example, Bob Fixture |
| Emails | *@example.com — e.g. alice@example.com |
| UUIDs | Sequential: 00000000-0000-0000-0000-000000000001 |
| Orgs | Test Org, Example Foundation |
| Domains | example.com, test.invalid |
Real-looking names, corporate domains, or UUIDs that appear to come from production data must not appear even if slightly modified or "anonymized." When in doubt, make the data unmistakably fictional.
Hooks live in .githooks/ and are installed by make deps (also available standalone as make hooks). Two hooks run on every commit:
pre-commit (runs before the message is written):
- Auto-formats staged
.gofiles withgofmtand re-stages them - Checks license headers across all tracked source files
commit-msg (runs after the message is written):
- Validates Angular conventional commit format
- Rejects placeholder Jira tickets (
[LFXV2-0000]) - Verifies the
Signed-off-by:DCO trailer is present
If a hook blocks your commit, fix the issue and re-run git commit. To amend a missing sign-off: git commit --amend -s.
The LFX V2 Project Service is a RESTful API service that manages projects within the Linux Foundation's LFX platform. It provides CRUD operations for projects with built-in authorization and audit capabilities.
- Language: Go 1.25+
- API Framework: Goa v3 (code generation framework)
- Messaging: NATS with JetStream for event-driven architecture
- Storage: NATS Key-Value stores (no traditional database)
- Authentication: JWT with Heimdall middleware
- Authorization: OpenFGA for fine-grained access control
- Container: Chainguard distroless images
- Orchestration: Kubernetes with Helm charts
The service follows Clean Architecture principles with clear separation of concerns:
.github/ # CI/CD workflow files for Github Actions
api/ # API contracts
└── project/
└── v1/
├── design/ # Goa API design specifications
└── gen/ # Generated code (gitignored)
charts/ # Helm charts containing kubernetes template files for deployments
cmd/project-api/ # Presentation Layer (HTTP entry point, Goa endpoint adapters)
├── service_endpoint_*.go # Goa endpoint adapters (project, link, folder, document)
├── http.go # HTTP server wiring
└── main.go # Application entry point, NATS subscription wiring
internal/ # Core business logic
├── domain/ # Domain layer (interfaces, models, errors, mocks)
│ └── models/ # Domain entities (project, link, folder, document)
├── service/ # Service layer (business logic, NATS RPC handlers, event subscriber)
│ ├── *_operations.go # Per-resource business orchestration
│ ├── project_handlers.go # Inbound NATS request/reply RPC handlers
│ ├── project_subscriber.go # Inbound NATS event subscribers (settings updates, invite acceptance)
│ ├── document_subscriber.go # Inbound NATS event subscribers (document/link created notifications)
│ ├── converters.go # Domain ↔ Goa ↔ pkg/events wire-type converters; ProjectProjection for multi-output fan-out
│ ├── user_resolver.go # UserResolver: centralised user identity lookup (JWT, auth service, fallback)
│ ├── notification_dispatcher.go # NotificationDispatcher: role-change email/invite orchestration
│ └── email/ # Email template rendering (one file per email type)
└── infrastructure/ # Infrastructure layer
├── auth/ # JWT authentication
├── log/ # Structured logging helpers (AppendCtx, InitStructureLogConfig)
├── middleware/ # HTTP middleware (auth, request ID, body limit, logger)
└── nats/ # NATS repository, object store, message builder, user reader
pkg/ # Shared packages across services
├── constants/ # Shared constants (NATS subjects, KV buckets, HTTP, access control)
└── events/ # NATS event wire types consumed by other services
scripts/ # Scripts for services and miscellaneous tasks
- Database Independence: Repository interfaces allow switching storage backends
- Testability: Each layer can be tested in isolation using mocks
- Event-Driven: All data changes trigger NATS messages for downstream services
- Separation of Concerns: Clear boundaries between layers
# Install Go 1.25+
# Install Goa framework
go install goa.design/goa/v3/cmd/goa@v3.22.6
# Install linting tools
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latestmake apigen
# or directly: goa gen github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/design -o api/project/v1make buildmake test # Run unit tests
make test-verbose # Verbose output
make test-coverage # Generate coverage report# Basic run
make run
# With debug logging
make debug
# With custom flags (direct go run)
go run ./cmd/project-api -d -p 8080make fmt # Format code
make lint # Run golangci-lint
make check # Check format and lint without modifyingCRITICAL — while the branch is pre-PR, post-commit review is mandatory. After every commit on the local branch, launch all three reviewer subagents via the Agent tool in parallel:
lfx-skills:lfx-general-code-reviewer,lfx-skills:lfx-project-service-code-reviewer, ANDlfx-skills:lfx-project-service-learnings-reviewer(each withrun_in_background: true) — then keep working while they run. If Claude displays plugin agents without thelfx-skills:namespace, use the equivalent displayed general, project-service, and learnings reviewer names. Before opening a PR, every running review must return clean (or remaining findings explicitly documented as trade-offs), the full-branch sweep must run clean if the branch has more than one commit (brancharg), AND/project-service-pr-readinessmust clear every Critical finding before/project-service-preflightruns.Once the PR is open, do NOT invoke these reviewers on iteration commits. CodeRabbit + Copilot auto-trigger on every push and own the audit surface from that point. The general, project-service, and learnings reviewers are pre-PR insurance only.
- Commit your work.
git commit -s -S. Do not wait for any prior review to finish. - Immediately launch all three reviewer subagents in parallel. Use
subagent_type: lfx-skills:lfx-general-code-reviewer,subagent_type: lfx-skills:lfx-project-service-code-reviewer, andsubagent_type: lfx-skills:lfx-project-service-learnings-reviewer, each withrun_in_background: true. - Post-commit mode prompt for all three reviewers (exact):
target repo: lfx-v2-project-service\n\nReview the latest commit.Appendextra: <focus>on a new line only when there is a priority hint to add. Do NOT passbranchhere. If this work cycle is launched from the LFX workspace parent, thetarget repo:line is required so all three reviewers operate in this repo. - Keep working. Start the next commit while the reviewers run. Do not block on them.
- When reviews return: roll every Critical finding and every reasonable Important finding into the next commit.
When the work is done and no more code commits are planned:
- Wait for every running review to complete.
- If any returned review flags Critical or reasonable Important: add a fix commit, launch all three reviewers again on the new state, wait, and loop until clean or explicitly documented as a trade-off.
- Full-branch sweep — only if the branch has more than one commit. Launch
lfx-skills:lfx-general-code-reviewer,lfx-skills:lfx-project-service-code-reviewer, andlfx-skills:lfx-project-service-learnings-revieweragain with prompttarget repo: lfx-v2-project-service\nbranch\n\nReview the branch's diff against origin/main.. Address any new findings, then re-run the sweep until clean. - Run
/project-service-pr-readinessfor branch and commit shape only. - Run
/project-service-preflightfor mechanical Go validation and PR summary. - Only then push and open the PR.
- Wait for CodeRabbit + Copilot to comment after each push.
- Triage every Critical and reasonable Important finding against current code.
- Roll fixes into a
fix(review): ...commit. - Push. Repeat until clean.
The service uses Goa v3 for API code generation. This is critical to understand:
- Design First: API is defined in
api/project/v1/design/files - Generated Code: Running
make apigengenerates toapi/project/v1/gen/:- HTTP server/client code
- Service interfaces
- OpenAPI specifications
- Type definitions
- Implementation: You implement the generated interfaces in
cmd/project-api/service*.gofiles
- Update
api/project/v1/design/project.gowith new method - Run
make apigen(from repository root) to regenerate code - Implement the Goa endpoint adapter in
cmd/project-api/service_endpoint_*.go(translation only); put business logic ininternal/service/*_operations.go - Add tests alongside the implementation (
internal/service/*_operations_test.goand the adapter test) - Update Heimdall ruleset in
charts/*/templates/ruleset.yaml
The service uses NATS for:
- Storage: Key-Value stores for project data
- Events: Publishing events on data changes
- RPC: Handling requests from other services
projects: Base project informationproject-settings: Project settings (separated for access control)project-links: Project link recordsproject-folders: Project folder recordsproject-documents-metadata: Project document metadataproject-documents: Project document binaries (NATS object store)
All bucket names live as constants in pkg/constants/nats.go.
Complete API endpoint documentation and NATS message handlers are now documented in README.md.
There are two distinct NATS patterns in this service — both use QueueSubscribe but for different purposes:
Request/reply RPC (internal/service/project_handlers.go): another service sends a request and blocks waiting for a response. The handler calls msg.Respond(data) to return data to the caller.
Event subscriptions (internal/service/project_subscriber.go and internal/service/document_subscriber.go): the service reacts to events that were already published (including by itself). No caller is waiting — the handler is fire-and-forget and never calls msg.Respond.
// Inbound RPC — request/reply, caller blocks waiting for response
"lfx.projects-api.get_name" // Get project name by UID
"lfx.projects-api.get_slug" // Get project slug by UID
"lfx.projects-api.get_logo" // Get project logo URL by UID
"lfx.projects-api.get_writers" // Get project writers by UID
"lfx.projects-api.slug_to_uid" // Convert slug to UID
"lfx.projects-api.get_parent_uid" // Get parent project UID
// Inbound events — fire-and-forget, no reply expected
"lfx.projects-api.project_settings.updated" // Self-published; sends role notification emails / invites on member changes
"lfx.invite-service.invite_accepted" // From invite-service (enriched event); promotes matching email-only users to LFID across all projects
"lfx.projects-api.project_document.created" // Self-published; emails project writers/auditors about the new document
"lfx.projects-api.project_link.created" // Self-published; emails project writers/auditors about the new link
// Outbound events (published by this service)
"lfx.index.project" // Project created/updated/deleted for indexing
"lfx.index.project_settings" // Settings created/updated/deleted for indexing
"lfx.index.project_link" // Link created/deleted for indexing
"lfx.index.project_folder" // Folder created/deleted for indexing
"lfx.index.project_document" // Document created/deleted for indexing
"lfx.projects-api.project_settings.updated" // Settings changed (before/after snapshot)
"lfx.projects-api.project_document.created" // File document uploaded (events.ProjectDocumentCreatedMessage)
"lfx.projects-api.project_link.created" // Link added (events.ProjectLinkCreatedMessage)
"lfx.fga-sync.update_access" // Generic FGA access control updates
"lfx.fga-sync.delete_access" // Generic FGA access control deletion
// Outbound request/reply (published by this service, awaits a response)
"lfx.email-service.send_email" // Request to email service for role notifications
"lfx.invite-service.send_invite" // Request to invite service for non-LFID usersThe service uses the generic FGA sync handlers for access control. All messages use the GenericFGAMessage envelope:
// Update access control (full sync) — fgatypes.GenericAccessData
GenericFGAMessage{
ObjectType: "project",
Operation: "update_access",
Data: GenericAccessData{
UID: "project-uid",
Public: true,
Relations: map[string][]string{
"writer": []string{"username1", "username2"},
"auditor": []string{"username3"},
"meeting_coordinator": []string{"username4"},
"executive_director": []string{"username5"},
},
References: map[string][]string{
"parent": []string{"project:parent-uid"},
},
},
}
// Delete all access control — fgatypes.GenericDeleteData
GenericFGAMessage{
ObjectType: "project",
Operation: "delete_access",
Data: GenericDeleteData{
UID: "project-uid",
},
}Key Points:
- Relations map user roles to usernames (e.g.,
"writer": ["user1", "user2"]) - References map object relationships with formatted UIDs (e.g.,
"parent": ["project:parent-uid"]) - Update operations are full sync - any relations not included will be removed
- Delete operations remove all access control tuples for the resource
- Mock all external dependencies (repository, message builder)
- Test each layer in isolation
- Use table-driven tests for comprehensive coverage
- Write one function tests containing multiple test cases that focus on a single function
- Focus on testing exported functions of packages
- Unit tests should be alongside the implementation code with the same file name with a suffix of
*_test.go - IMPORTANT: Each function should have exactly ONE corresponding test function (e.g.,
SendIndexProject→TestMessageBuilder_SendIndexProject) which can have multiple tests cases within it. - Add test cases within existing test functions if the function you are trying to test already has one rather than creating new test functions
func TestEndpoint(t *testing.T) {
tests := []struct {
name string
payload *projsvc.Payload
setupMocks func(*domain.MockRepo, *domain.MockMsg)
wantErr bool
}{
// Test cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
api, mockRepo, mockMsg := setupAPI()
tt.setupMocks(mockRepo, mockMsg)
// Test logic
})
}
}| Variable | Description | Default | Required |
|---|---|---|---|
PORT |
HTTP listen port | 8080 | No |
NATS_URL |
NATS server URL | nats://localhost:4222 | No |
LOG_LEVEL |
Log level | info | No |
JWKS_URL |
JWT verification endpoint | - | No |
AUDIENCE |
JWT audience | lfx-v2-project-service | No |
JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL |
Mock auth for local dev | - | No |
SKIP_ETAG_VALIDATION |
Skip If-Match/ETag revision enforcement on writes (true to skip; local dev only) |
false | No |
LFX_ENVIRONMENT |
Deployment environment (prod/production, staging/stg/stage, dev/development); drives the default self-serve base URL when LFX_SELF_SERVE_BASE_URL is empty; defaults to prod when unset |
- | No |
LFX_SELF_SERVE_BASE_URL |
Base URL for project links in notification emails; takes precedence over LFX_ENVIRONMENT |
derived from LFX_ENVIRONMENT (prod when unset) |
No |
EMAILS_ENABLED |
Gate for outbound role-notification emails to LFID users (true to enable) |
false | No |
INVITES_ENABLED |
Gate for outbound invite requests to non-LFID users (true to enable) |
false | No |
When deployed, the service uses OpenFGA for authorization:
- GET /projects - Denied in deployed environments (local development only)
- POST /projects - Requires
writeron parent (if specified) - GET /projects/:id - Requires
vieweron project - GET /projects/:id/settings - Requires
auditoron project - PUT /projects/:id - Requires
writeron project - PUT /projects/:id/settings - Requires
writeron project - DELETE /projects/:id - Requires
owneron project
There are two main development setup options documented in DEVELOPMENT.md:
For integration testing with complete LFX stack:
- Install lfx-platform Helm chart (includes NATS, Heimdall, OpenFGA, Authelia, Traefik)
- Use
make helm-install-localwith values.local.yaml - Full authentication and authorization enabled
For rapid development:
# Just run NATS locally
docker run -d -p 4222:4222 nats:latest -js
# Create KV stores
nats kv add projects --history=20 --storage=file
nats kv add project-settings --history=20 --storage=file
# Run service with mock auth
export NATS_URL=nats://localhost:4222
export JWT_AUTH_DISABLED_MOCK_LOCAL_PRINCIPAL=test-user
make runSecurity Note: Option B bypasses all authentication/authorization - only for local development.
make helm-install-local: Install with local valuesmake helm-restart: Restart deployment podmake docker-build: Build Docker image
# Build from repository root
docker build -t lfx-v2-project-service:latest .
# The Dockerfile uses:
# - Chainguard Go image for building
# - Chainguard static image for runtime (distroless)
# - Multi-stage build for minimal image size# Install Helm chart
helm install lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx
# Update deployment
helm upgrade lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx
# View generated manifests
helm template lfx-v2-project-service ./charts/lfx-v2-project-service/ -n lfx- OpenFGA can be disabled for local development
- NATS KV buckets are created automatically
- Heimdall middleware handles JWT validation
- Traefik IngressRoute for HTTP routing
GitHub Actions workflows:
- mega-linter.yml: Comprehensive linting (Go, YAML, Docker, etc.)
- project-api-build.yml: Build and test on PRs
- license-header-check.yml: Ensure proper licensing
- Generate API code
- Build binary
- Run unit tests
- Lint with MegaLinter
Problem: Changes to design files not reflected in implementation
Solution: Always run make apigen after modifying design files
Problem: Concurrent updates without proper ETag validation Solution: Always include If-Match header in PUT/DELETE requests (server responds with ETag header on GET request)
Problem: Service fails to start due to NATS connection Solution: Ensure NATS is running and NATS_URL is correct
Problem: Invalid slug format causes API errors
Solution: Slugs must match ^[a-z][a-z0-9_\-]*[a-z0-9]$
Problem: Creating projects with invalid parent_uid Solution: parent_uid must be empty string or valid UUID
Use the provided script to load test data:
cd scripts/load_mock_data
go run main.go -bearer-token "your-token" -num-projects 10Projects are split into two parts for access control:
- Base: Core project info (stored in
projectsKV) - Settings: Sensitive settings (stored in
project-settingsKV)
Every data modification publishes NATS messages:
- Index messages for search service
- Access control updates for authorization service
NATS message payload types that other services consume belong in pkg/events/, not internal/. This lets downstream services (e.g., lfx-v2-invite-service) import the canonical struct definitions directly.
- Domain types in
internal/domain/models/may differ from wire types and can evolve independently. - Explicit converter functions in
internal/service/converters.gomap from domain → event type before publishing. - Example:
DomainSettingsToEvent(*models.ProjectSettings) events.ProjectSettings - When a single
(base, settings)domain pair needs to produce multiple output shapes, construct aProjectProjectionviaNewProjectProjection(base, settings)and call the appropriate methods (ToFull,ToFGAMessage,ToEventSettings, etc.) rather than forwarding the pair to each standalone converter independently.
Rule: if a struct appears in a NATS message payload, it belongs in pkg/events/, not internal/.
Important context values:
request-id: Unique request identifierauthorization: JWT token from headeretag: ETag value for optimistic concurrency (sent as If-Match header in requests)
Domain errors are named sentinels in internal/domain/errors.go, mapped to HTTP status codes by handleError in cmd/project-api/service_endpoint_project.go:
ErrProjectNotFound/ErrDocumentNotFound/ErrLinkNotFound/ErrFolderNotFound→ 404ErrProjectSlugExists/ErrRevisionMismatch/ErrDocumentNameExists/ErrFolderNameExists/ErrFolderNotEmpty→ 409ErrValidationFailed/ErrInvalidParentProject/ErrInvalidContentType/ErrFileTooLarge/ErrCannotDeleteNonCrowdfundingProject→ 400ErrInternal/ErrUnmarshal→ 500ErrServiceUnavailable→ 503
The internal/service/ layer contains three focused modules extracted to avoid duplication. Always reach for these before adding new logic inline.
| Module | File | Purpose | How to use |
|---|---|---|---|
UserResolver |
user_resolver.go |
Centralised user identity lookup: resolves display names from JWT, auth service, or falls back gracefully | Call s.Resolver.ResolveDisplayName(ctx, events.Actor{Username: username}) or s.Resolver.ResolveRequestingUser(ctx) — never inline the auth-service lookup |
NotificationDispatcher |
notification_dispatcher.go |
Orchestrates role-change emails and invite requests for LFID and non-LFID users, respects EmailsEnabled/InvitesEnabled feature flags |
Call s.Dispatcher.Dispatch(ctx, projectUID, name, url, actor, changes) from HandleProjectSettingsUpdated — never add notification logic to subscribers or operations directly |
ProjectProjection |
converters.go |
Bundles a (base, settings) domain pair and exposes typed output methods for each target type universe |
Call NewProjectProjection(base, settings) once then .ToFull(), .ToFGAMessage(), .ToEventSettings(), etc. — never forward the same pair to multiple standalone converters |
resolveRevision pattern (project_operations.go): write operations (update, delete) must resolve the resource revision either from an If-Match header or by fetching it from the repository. Use the s.resolveRevision(ctx, ifMatch, fetchFn) helper — do not duplicate the SkipEtagValidation branching inline.
- Enable Debug Logging: Run with
-dflag or setLOG_LEVEL=debug - Check NATS Messages: Use
nats sub "lfx.>"to monitor all messages - Verify KV Data: Use
nats kv get projects <uid>to check stored data - HTTP Traces: Middleware logs all requests with timing
- Generated Code: Check
api/project/v1/gen/directory for Goa-generated interfaces
The project has a clear documentation hierarchy:
- README.md: Project overview, quick start, API endpoints, deployment setup
- DEVELOPMENT.md: Comprehensive developer guide with build/test/deploy workflows
- CLAUDE.md: AI assistant instructions and technical details (this file)
Key documentation patterns:
- README focuses on getting the service running quickly
- DEVELOPMENT.md covers the full development workflow
- Avoid duplicating content between files - use cross-references instead
- Design First: Update Goa design files before implementation
- Test Coverage: Write comprehensive unit tests
- Mock External Deps: Use mocks for repository and message builder
- Follow Clean Architecture: Respect layer boundaries
- Update Docs: Keep documentation current and avoid duplication
- Lint Clean: Ensure
make checkpasses