| name | project-service-dev |
| description | Repo-local Go coding conventions for lfx-v2-project-service. Auto-attaches when editing Go files, Goa design files, Makefile, go.mod/go.sum, or anything under api/, cmd/, internal/, pkg/, or gen/. Owns logging, errors, request context, pagination, generated-code boundary, NATS/KV publishing, tests, formatting, linting, and license headers for this repo. Central architecture, V2 service classes, and cross-repo handoffs stay in /lfx-skills:lfx-platform-architecture; cross-repo ownership questions stay in /lfx-skills:lfx. |
| paths | ["**/*.go","go.mod","go.sum","Makefile","api/**","cmd/**","internal/**","pkg/**","gen/**",".claude/skills/project-service-dev/**"] |
| allowed-tools | Read, Glob, Grep, Edit, Write, Bash |
Development Conventions
Repo-local conventions for writing Go code in lfx-v2-project-service. Auto-attaches on Go and service paths and governs how every hand-written Go file in this repo is structured, named, logged, errored, tested, formatted, and linted.
What this skill owns
- Generated-code boundary (Goa).
- Logging with
log/slog and the repo's internal/infrastructure/log context helper.
- Domain errors in
internal/domain/errors.go and the HTTP mapping in cmd/project-api/service_endpoint_project.go.
- Request context propagation through
pkg/constants typed keys.
- Pagination shape for list endpoints.
- NATS subject and KV bucket usage from
pkg/constants/nats.go.
- Tests (table-driven, mocks under
internal/domain/mock.go, race-enabled).
- Formatting, linting, license header.
What this skill does NOT own
- Platform composition, V2 service classes, write/read/access-check flows, cross-service responsibilities, NATS/KV ownership, Heimdall, OpenFGA, ArgoCD: use
/lfx-skills:lfx-platform-architecture.
- "Where does X live", repo ownership routing, missing checkouts: use
/lfx-skills:lfx.
- FGA emission contract semantics: read
lfx-v2-fga-sync/docs/fga-sync-contract.md for the generic contract and docs/fga-contract.md for project-specific emissions.
- Indexer envelope semantics: read
lfx-v2-indexer-service/docs/indexer-contract.md for the generic contract and docs/indexer-contract.md for project-specific emissions.
- Service chart conventions: read
lfx-v2-helm/docs/service-chart-patterns.md; this repo's chart lives under charts/lfx-v2-project-service/.
Repository layout
api/project/v1/design/ Goa source (hand-written)
api/project/v1/gen/ Generated Goa code (never edit by hand)
cmd/project-api/ HTTP entry point, Goa endpoint adapters
internal/domain/ models, ports, errors, mock factories
internal/service/ business orchestration
internal/infrastructure/nats/ KV repositories and message publishers
internal/infrastructure/auth/ JWT auth integration
internal/infrastructure/middleware/ request_id, request_logger, authorization, body_limit
internal/infrastructure/log/ slog context helpers
pkg/constants/ NATS, HTTP, app, access-control constants
charts/lfx-v2-project-service/ Helm chart (owned by this repo)
Match existing package names and constructor styles before adding a new abstraction.
Generated code boundary
api/project/v1/gen/ is generated by make apigen. Do not edit any file under that path by hand.
- Change
api/project/v1/design/*.go first, then run make apigen (which runs goa gen github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/design -o api/project/v1).
- Generated files under
api/project/v1/gen/ are tracked in this repo. Commit generated diffs alongside the design changes that produced them.
- Implement generated interfaces in
cmd/project-api/service_endpoint_*.go. Business logic lives in internal/service/, not in the endpoint adapter.
- When a design change affects authorization, update
charts/lfx-v2-project-service/templates/ruleset.yaml in the same change.
Deeper Goa details (project-type shape, ETag/If-Match wiring, design file layout) live in references/goa-and-codegen.md.
Logging
- Use
log/slog only. Do not use fmt.Println, fmt.Printf, log.Print*, or log.Println for runtime service logging.
- Always use the
*Context variants (slog.InfoContext, slog.ErrorContext, etc.) so the context attributes appended by middleware flow through.
- Attach request-scoped fields with
log.AppendCtx(ctx, slog.String("key", value)) from internal/infrastructure/log. Middleware already appends method, path, query, host, user_agent, remote_addr, and req_header_etag when present.
- Prefer stable structured keys:
principal, project_uid, object_type, object_id, action, operation, subject, bucket, revision. Existing request-id middleware logs the key as X-REQUEST-ID via constants.RequestIDHeader.
- Never log bearer tokens, raw
Authorization headers, secrets, or full payloads that may contain PII.
- Log level is set by
LOG_LEVEL (debug, info, warn, error). Default is LevelDebug. Honor it; do not hardcode levels.
- Health-check requests (
/livez, /readyz) log at DEBUG. Everything else logs at INFO. Keep that pattern.
Errors
-
All domain errors live in internal/domain/errors.go as named sentinels (ErrProjectNotFound, ErrRevisionMismatch, ErrInvalidParentProject, etc.). Add new error returns there, do not scatter sentinels across packages.
-
Translate sentinels to HTTP at the Goa boundary in cmd/project-api/service_endpoint_project.go::handleError. New errors must be added to that switch.
-
Standard mapping for this repo:
| Error category | HTTP | Examples |
|---|
| Validation | 400 | ErrValidationFailed, ErrInvalidParentProject, ErrInvalidContentType, ErrFileTooLarge, ErrCannotDeleteNonCrowdfundingProject |
| Not found | 404 | ErrProjectNotFound, ErrDocumentNotFound, ErrLinkNotFound, ErrFolderNotFound |
| Conflict | 409 | ErrProjectSlugExists, ErrRevisionMismatch, ErrDocumentNameExists, ErrFolderNameExists, ErrFolderNotEmpty |
| Internal | 500 | ErrInternal, ErrUnmarshal |
| Unavailable | 503 | ErrServiceUnavailable |
-
Wrap upstream errors with fmt.Errorf("...: %w", err) so errors.Is and errors.Unwrap keep working.
-
Do not return raw NATS, KV, or upstream HTTP errors directly to the Goa layer; map them to a domain sentinel first.
-
Project-service currently uses named sentinels (not the shared ErrorType enum). When adding new errors, follow the sentinel pattern that the rest of the repo uses.
Request context
- Middleware and the Goa security adapter own context setup. Service-layer code must not read HTTP headers directly.
- Use the typed keys in
pkg/constants/http.go:
constants.RequestIDContextID for X-REQUEST-ID.
constants.PrincipalContextID for the Heimdall principal parsed by ProjectsAPI.JWTAuth.
constants.AuthorizationContextID for the inbound Authorization header captured by AuthorizationMiddleware.
constants.ETagContextID for response ETag values set by the custom encoder in cmd/project-api/main.go.
- Never introduce bare string context keys. Add a new typed alias to
pkg/constants/http.go if a new key is needed.
- Propagate
ctx end to end: HTTP handler to service to repository to NATS publisher. Do not drop ctx in the middle.
If-Match is decoded by generated Goa code into payload fields such as payload.IfMatch; it is not read from context.
Pagination
GetProjects is currently a temporary unpaginated local-development endpoint that lists all projects from NATS KV.
- Do not add
page_size, page_token, or next_page_token to the existing endpoint without changing the Goa design, generated code, service tests, README, and consumers together.
- For long project lists, prefer the platform read path through
lfx-v2-query-service; local list behavior should stay stable until that migration is explicitly made.
NATS and KV
- All subject strings live in
pkg/constants/nats.go. Do not hardcode subject strings at call sites; reference the constant.
- All KV bucket names live in
pkg/constants/nats.go (KVStoreNameProjects, KVStoreNameProjectSettings, etc.). Same rule.
- Use queue groups for subscriptions (
ProjectsAPIQueue) and request/reply handlers so multiple replicas share work.
- Do not write to another service's KV bucket. Use that service's request/reply subjects instead.
- Use NATS KV revisions for optimistic locking on mutable resources. GET returns the revision as
ETag; PUT and DELETE require matching If-Match. A stale revision must surface as ErrRevisionMismatch (409).
- Publish indexer and FGA messages only after the storage write succeeds, not before. See
internal/infrastructure/nats/message.go.
- Drain NATS connections during shutdown through the repo's existing graceful shutdown path in
cmd/project-api/main.go.
The full subject and bucket inventory plus per-resource publish order lives in references/nats-messaging.md.
Tests
- Co-locate
*_test.go with the file under test (e.g., project_operations_test.go next to project_operations.go).
- Depend on interfaces, not concrete types:
ProjectRepository, DocumentRepository, LinkRepository, FolderRepository, MessageBuilder, Auth. All have mocks in internal/domain/mock.go and internal/infrastructure/auth/.
- Mock framework is
github.com/stretchr/testify/mock. Use assert for assertions.
- Use table-driven tests. One test function per exported method (e.g.,
SendIndexProject corresponds to TestMessageBuilder_SendIndexProject); add cases to the existing table rather than spawning new top-level functions.
- Race detection is on by default (
TEST_FLAGS=-race in the Makefile). Tests must be race-clean.
- Run
make test before handoff. Use make test-coverage when adding new code paths.
Formatting, linting, license
-
make fmt runs go fmt ./... and then gofmt -s -w $(GO_FILES) where $(GO_FILES) excludes api/project/v1/gen/ and vendor/. It does not run goimports.
-
make lint runs golangci-lint. Use the configuration in revive.toml for revive rules; do not add inline lint disables without a comment explaining why.
-
make check verifies gofmt, then runs make lint and make license-check as a non-modifying gate. CI also runs MegaLinter via .mega-linter.yml.
-
Every new .go file must start with:
-
Every new Markdown doc in this repo starts with the HTML-comment form of the same header. SKILL.md files keep YAML front matter first, then the header comments.
-
Document exported Go symbols when golangci-lint requires it. Skip implementation comments where the code is self-explanatory.
References
Read these for depth, in priority order, when the change touches the named area.
references/go-conventions.md (38 lines): dependency-injection style, mock layout in internal/domain/mock.go, naming conventions, and a "where to add new code" table.
references/goa-and-codegen.md (42 lines): design-first workflow, project-base vs project-settings type split, ETag/If-Match wiring, and the boundary between generated and hand-written code.
references/nats-messaging.md (76 lines): subject inventory, KV bucket inventory, optimistic-locking pattern, and publish ordering after storage writes.
For repo-owned contracts, read docs/fga-contract.md and docs/indexer-contract.md before changing publishers. For chart work, read this repo's chart under charts/lfx-v2-project-service/ plus lfx-v2-helm/docs/service-chart-patterns.md. For platform shape and cross-repo handoff, use /lfx-skills:lfx-platform-architecture. For repo routing, use /lfx-skills:lfx.