Development Conventions (lfx-v2-committee-service)
Repo-owned development conventions for this committee service. Central architecture lives elsewhere (see "Central skills" below); this skill owns how Go code, repo-owned contracts, and the service-local chart are maintained in this repo.
Central skills (do not duplicate)
lfx-skills:lfxowns cross-repo topology, ownership routing, repo discovery, glossary, and missing-checkout handling.lfx-skills:lfx-platform-architectureowns V2 platform composition, service classes (native, wrapper, proxy, platform), write/read/access-check/index flows, NATS and KV ownership, and handoff points across Self Serve, Goa services, OpenFGA, fga-sync, indexer-service, query-service, access-check, Heimdall, Helm, and ArgoCD. It also owns the V2 service taxonomy.
Use them through /lfx-skills:lfx and /lfx-skills:lfx-platform-architecture
when the question is about cross-repo or platform composition. This skill
governs only repo-local implementation, contracts, and chart wiring.
Repo layout (what this skill governs)
cmd/committee-api/is the Goa service entry point: design files, presentation-layer service implementation,main.go,http.go.cmd/committee-cli/is the operational CLI (one binary,command+subcommandshape, seecmd/committee-cli/README.md).gen/is Goa-generated code. Do not edit.internal/domain/holdsmodel/(entities) andport/(interfaces).internal/service/holds use-case orchestration (*Writer,*Reader,MessageHandler).internal/infrastructure/holds NATS storage (nats/), auth (auth/), AI adapters for the weekly brief (ai/), M2M source clients for meetings / mailing lists / votes (m2m/), and mocks (mock/).internal/middleware/holds HTTP middleware (request ID, authorization).pkg/holds reusable utilities:constants,errors,log,redaction,fields,env,concurrent,utils.docs/holds committee-owned contracts for emitted indexer/FGA messages and invite/application flow behavior. Keep these synced with code changes.charts/lfx-v2-committee-service/holds this service's Helm chart: Deployment env vars, Gateway HTTPRoute, Heimdall RuleSet, NATS KV buckets, Object Store, and JetStream stream resources.
Match the existing package boundary before adding a new one. Use-case logic
goes in internal/service/; storage adapters in internal/infrastructure/nats/;
interfaces in internal/domain/port/.
Generated code boundary
- Never hand-edit files under
gen/(Goa output). - Change Goa design files under
cmd/committee-api/design/first, then runmake apigen. Commit generated output with the design change. cmd/committee-api/service/*(presentation layer) implements the Goa service interface and adapts to domain types; keep business logic ininternal/service/, not in this layer.- See
references/goa-patterns.mdfor committee-service Goa specifics (base/settings split, per-sub-resource methods, ETag/If-Match handling).
Logging
Use Go's log/slog together with this repo's pkg/log helpers. Do not use
fmt.Println, fmt.Printf, or log.Print* for runtime logging.
- Prefer the
*Contextvariants (slog.DebugContext,slog.InfoContext,slog.WarnContext,slog.ErrorContext) so thepkg/logcontext handler can inject ambient attributes (request_id, etc.). - Add ambient request fields via
log.AppendCtx(ctx, slog.String(...))in middleware. Service code reads them implicitly through the context handler. - Stable structured fields when available:
request_id,principal,object_type,object_id,operation,committee_uid,member_uid,project_uid. - Honor
LOG_LEVELandLOG_ADD_SOURCE(seecmd/committee-api/README.md). - Never log raw JWTs, bearer headers, secrets, or full payloads that may
contain PII. Use
redaction.Redactfrompkg/redactionfor partial identifiers (emails, usernames) when they must appear in logs.
Errors
This repo has a typed domain-error family in pkg/errors. Use it. Do not
introduce a parallel sentinel-error family.
- Constructors:
errors.NewValidation,errors.NewNotFound,errors.NewConflict,errors.NewForbidden,errors.NewTooManyRequests,errors.NewEditedBriefExists,errors.NewServiceUnavailable,errors.NewUnexpected(and matching types:Validation,NotFound,Conflict,Forbidden,TooManyRequests,EditedBriefExists,ServiceUnavailable). - Use
errors.Join/ wrap upstream errors soerrors.Isanderrors.Asstill work. The base type already joins viaerrors.Join(err...). - Translate domain errors at the Goa transport boundary in
cmd/committee-api/service/error.go(thewrapErrorswitch). Do not return raw upstream HTTP errors or NATS errors to clients. - HTTP status mapping enforced by
wrapError:errors.Validation-> 400BadRequestErrorerrors.NotFound-> 404NotFoundErrorerrors.EditedBriefExists-> 409GroupWeeklyBriefEditedExistsErrorerrors.Conflict-> 409ConflictErrorerrors.Forbidden-> 403ForbiddenErrorerrors.TooManyRequests-> 429GroupWeeklyBriefThrottleExceededErrorerrors.ServiceUnavailable-> 503ServiceUnavailableError- default -> 500
InternalServerError
- When you add a new domain error case, extend
wrapErrorin the same change.
Request context
- HTTP middleware owns request-context setup. Service-layer code must not read HTTP headers directly.
- Use the typed context keys from
pkg/constants:PrincipalContextID,EmailContextID,AuthorizationContextID,OnBehalfContextID,RequestIDHeader. Do not introduce bare string context keys. internal/middleware/request_id.gocallslog.AppendCtx(ctx, slog.String(string(constants.RequestIDHeader), requestID)); follow the same pattern when adding a new ambient field.- Propagate
context.Contextdown through all use-case and storage calls. Passctxfirst in any new function that does I/O, logging, or cancellation.
NATS, subjects, KV, and Object Store
- All NATS subject strings and KV bucket names live in
pkg/constants/(subjects.go,storage.go). Never hardcode a subject or bucket string at a call site. - The committee service also owns the
committee-documentsNATS Object Store for uploaded file bytes. Metadata stays in thecommittee-documents-metadataKV bucket. - Use queue groups for shared subscriptions. This service uses
lfx.committee-api.queuefor request/reply and fire-and-forget event handlers, plus durable JetStream consumers: one for total-member recounts (committee-member-eventsstream) and one for async weekly-brief generation (weekly-brief-eventsstream). - Never write directly to another service's KV bucket. Cross-service writes go through that service's NATS RPC or its message contracts.
- Drain the NATS connection through the existing graceful-shutdown path in
internal/infrastructure/nats/. Do not add a parallel shutdown. - When subjects, queue groups, payloads, KV buckets, Object Stores, or streams
change, update
references/nats-messaging.mdin the same change. - Local subject and bucket inventory: see
references/nats-messaging.md.
Contracts and chart wiring
docs/indexer-contract.mdmust match theIndexingConfig, tags, and data structs emitted by the writer/orchestrator code. Do not copy generic indexer examples forward without checking this repo'sinternal/service/*writer.goandinternal/domain/model/*.docs/fga-contract.mdmust match access messages built byinternal/service/committee_writer.goandinternal/service/committee_member_writer.go.docs/invite-application-flows.mdmust match the status transitions incmd/committee-api/service/committee_service.go.cmd/committee-api/README.mdshould reflect the actual Goa endpoints incmd/committee-api/design/committee.go.- Chart changes stay under
charts/lfx-v2-committee-service/. Route shared chart conventions tolfx-v2-helm/docs/service-chart-patterns.md; do not duplicate shared Helm policy here.
Tests
Depend on interfaces from
internal/domain/port/for repositories, message publishers, FGA clients, NATS clients, and upstream proxies.Place mocks in
internal/infrastructure/mock/. Reuse the existing fakes before adding new ones.Table-driven tests for branching behavior. One
Test<Type>_<Method>per exported method; add cases to its table rather than fanning out new functions.Co-locate
*_test.gowith the code under test. Usepackage <pkg>_testfor black-box tests (seeinternal/domain/model/committee_link_test.go); use same-package tests when exercising unexported behavior.For typed-error assertions use the typed-error pattern in
internal/service/committee_writer_test.go:var conflictErr errs.Conflict if !errors.As(err, &conflictErr) { t.Fatalf(...) }Run
make testbefore handing off. It already runsgo testwith-race; use targeted package tests only while iterating.
Formatting, linting, headers
Run
make fmt(wrapsgo fmt ./...andgofmt -s -won tracked Go files).Run
make lint(golangci-lint pinned in the Makefile). Fix lint warnings; do not blanket-silence them.License header on every new
.gofile (Goa-generated files are excluded by the Makefile'slicense-check):// Copyright The Linux Foundation and each contributor to LFX. // SPDX-License-Identifier: MITMarkdown files in
.claude/skills/.../references/use the HTML-comment form (<!-- Copyright ... -->). Match the existing files in the directory.Document exported Go symbols when revive/golangci-lint requires it (see
revive.toml). Add implementation comments only where the code is not self-explanatory.
Companion files
docs/indexer-contract.md,docs/fga-contract.md, anddocs/invite-application-flows.mdare the authoritative committee-domain contracts. Update them in the same PR as any behavior change.- For generic FGA semantics, read
lfx-v2-fga-sync/docs/fga-sync-contract.md. For generic indexer semantics, readlfx-v2-indexer-service/docs/indexer-contract.md. For shared chart conventions, readlfx-v2-helm/docs/service-chart-patterns.md.
References
references/goa-patterns.md: committee-service Goa design layout, base / settings split, ETag handling, per-sub-resource method structure.references/nats-messaging.md: subjects, queue groups, KV buckets, Object Store, and streams owned or consumed by this service.