Poultry egg-farm management system. Backend: .NET 10 (C#), layered DDD. Frontend: React 19 + Vite SPA in web/. Postgres via EF Core.
This file is the shared brief for any coding agent (Claude Code, Codex, etc.) and
the canonical rule set for the repo. Humans usually want the short path first:
CONTRIBUTING.md (develop, test, commit),
docs/ (runbooks, decision records, releasing),
SECURITY.md.
Every rule here is one paragraph. A rule that carries a → link was earned
by a defect that shipped, and the narrative — what broke, which review round
found it, what the wrong fix was — is behind that link in
docs/decisions/: follow it before changing the rule. A
rule with no link is a plain convention that has not yet cost anything; it needs
consistency, not archaeology.
- Communicating · Layout · Build / test / run
- Conventions — the rules that break things when ignored
- Secrets · Host-agnostic repo · One serving instance
- Writing a guard · Pre-commit hook · CI security gates
- Releases · Git / PR workflow · Phase context · graphify
Keep explanations clear and human-sounding. Provide concise, focused responses. Skip preambles and recaps — lead with the action or answer.
src/
Cluckwork.Domain aggregates, value objects, domain events (no deps)
Cluckwork.Application feature handlers, repository interfaces, validators
Cluckwork.Infrastructure EF Core, Identity/JWT, repositories, seeding, jobs
Cluckwork.Api minimal-API endpoints, middleware, Program.cs
web/ React/Vite SPA (see web/README.md)
deploy/ docker-compose (.yml prod, .dev.yml dev DB), .env.example
specs/ product + technical specs, wireframes
tests/ Domain.Tests, Application.Tests, Api.IntegrationTests
Dependencies point inward: Api → Application/Infrastructure → Domain. Domain depends on nothing.
The request pipeline order and the egg-loop state machine are drawn in
docs/architecture.md — read it before moving middleware
or adding an aggregate state.
dotnet build Cluckwork.sln # warnings are errors — keep it clean
dotnet test Cluckwork.sln # 688 tests as of 2026-07; integration needs Docker- Integration tests spin up a real Postgres via Testcontainers (
dockerrequired). No SQLite — EF SQL semantics differ. - Run full stack (prod-like):
docker compose -f deploy/docker-compose.yml up --build→ SPA + API on http://localhost:8080 (single container; API serves the built SPA fromwwwroot). - Run frontend dev:
cd web && npm run dev→ http://localhost:5173 (proxies/api→ :8080). - Debug API (no docker stack):
docker compose -f deploy/docker-compose.dev.yml up -d(Postgres on :5432), then run/debugCluckwork.Api(Development env). Dev secrets live in user-secrets, not files.
- Result pattern: domain/handlers return
Result/Result<T>(seeDomain/Common). Don't throw for expected failures; throw only for invariant violations (e.g.Flock.Createguards). - Handler per feature, invoked directly from endpoints — no MediatR. Register handlers/validators/repos in
Program.cs. - Validation: FluentValidation validators (
*Validator), one per command; endpoints callValidateAsyncand returnValidationProblem. - Endpoints: minimal APIs grouped under
/api/v1/...viaMap<Feature>Endpoints; writes require auth + anIdempotency-Key(middleware). - Nullable enabled, no unused usings — both are build-breaking.
- Every aggregate mutation must bump
Version.Versionis an EF concurrency token (IsConcurrencyToken): EF puts the original value in the UPDATE'sWHEREbut never auto-increments it, so a mutation withoutVersion++silently loses concurrent races — both writers matchWHERE Version = N— instead of 409ing. This shipped three times; each fix carries a parallel-race integration test, and so must any new mutation. - Multi-tenancy: every tenant-owned entity has
AccountId, enforced by an EF global query filter plus aTenantStampInterceptor(stamps on insert).TenantContextresolves per-request from the JWTaccount_idclaim; at startup it is unresolved, so seeders useIgnoreQueryFilters(). - Transient-DB retry stops at unreplayable work (#269).
EnableRetryOnFailurecovers self-contained EF units only; an automatic replay above a stateful detector (a counter, a CAS stamp, a single-use claim) cannot tell "this request racing itself" from the signal the detector exists to catch. Two cures, and picking wrong ships the bug:SingleAttemptExecutionwhen the replay is itself observable, a durability probe on a self-minted token when the replay writes nothing. →269-transient-db-retry-boundary.md AuditEventsis not time-partitioned, on purpose (#505). The dominant read filters onAccountId+EntityType+EntityIdwith no date predicate, so monthly partitions would turn one index lookup into one per partition for no pruning benefit. If it is ever needed, partition byAccountId. →505-audit-events-no-time-partition.md
InitialCreateis frozen; one migration per change (#407). EF never re-runs an applied migration, so a column hand-folded intoInitialCreatesilently does not exist on any booted database — it surfaces as broken behaviour, not as a migration error.InitialCreatealso carries un-regenerable expression indexes and the base-reference SQL; regenerating desynchronises__EFMigrationsHistoryeverywhere. Pre-#407 dev databases cannot migrate forward — drop and recreate. →407-migration-freeze.md- Base reference data ships as guarded raw-SQL migrations (#283). The default account, four assignable roles, default egg grades and packed-unit conversions are
migrationBuilder.SqlwithWHERE NOT EXISTSguards — neverHasData/InsertData, which key on the PK and emitUpdateData/DeleteDatathat silently reverts the farm's own edits. Grades guard whole-set (they are user-renamable); roles, conversions and account guard per-key. →283-migrations-base-provisioning.md - Schema docs are generated, committed, and regenerated with every migration (#417).
docs/schema/comes fromtools/schema-docs/generate.sh; CI'sbuild-and-testrunsgenerate.sh --checkand fails a stale PR. Never hand-edit them, and resolve a post-rebase conflict there by regenerating. →417-schema-docs.md - The design-time connection is fail-closed (#318).
AppDbContextDesignTimeFactoryhas no default: an unsetCLUCKWORK_MIGRATIONS_CONNECTIONthrows, and every target meets the same TLS floor as a Production boot, except an explicitly acknowledged loopback viaCLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK=true. →318-design-time-migration-connection.md
- Auth: asymmetric JWT + rotating refresh tokens. PEM keys come from config with escaped
\n; normalize viaPemKey.NormalizebeforeImportFromPem. Integration tests generate an ephemeral RSA pair at test-process startup (TestJwtKeysinCluckworkWebApplicationFactory.cs) — no real key material is ever committed. - Both JWT keys are checked at boot, and the check is serving-only (#510/#347).
AddCluckworkIdentityrequires both keys non-blank and importable; useIsNullOrWhiteSpace, never??(the shippedappsettings.jsoncarries"", which??does not catch), and import at boot rather than inside theAddJwtBearerdelegate (which makes a corrupt key a per-request 500 behind a green health check). →510-jwt-key-boot-check.md - Credential epoch revocation (#364). Every access/refresh token is bound to the user's
CredentialEpochand every password-reset path bumps it. Epoch0is permanently retired, a missing or malformed claim is always a mismatch, andCredentialEpochMiddlewaredoes a fresh DB read per authenticated request — the round trip is the fail-closed guarantee, so do not cache it. Break it and a revoked credential keeps working. →364-credential-epoch-revocation.md - First-run admin:
bootstrap-admin(#283). Creates an Owner with a generated password (stdout only, never the logger/OTLP) andMustChangePassword=true, only if the default account has no Owner; a re-run is a silent no-op. While the flag is set,MustChangePasswordMiddleware403s everything exceptauth/change-passwordandauth/logout. →283-first-run-admin-provisioning.md - Break-glass:
recover-admin(#265). Same run-then-exit shape asseed, but deliberately not environment-gated — it must work against a real Production database. One transaction: freshly generated temp password (never one passed on the command line), rotated security stamp, every refresh token revoked, and aUser.BreakGlassResetaudit row carrying--reason. →265-break-glass-recovery.md· runbook - Nothing writes an audit event without an actor (#500).
AuditWriterthrows on an unresolvedICurrentUser; system callers declare aSystemActorsidentity and both seeders require an Owner.ICurrentUseris an authorization input, not a label —FlockScopeGuardreads its roles, so an actor built from a literal rather thanUserManager.GetRolesAsynccan fail an entire seed. →500-audit-actor.md
- Process role, not statement order (#347). A boot guard's scope is declared, never positional:
ProcessRoles.From(args)computesServing | OneShotonce, before the host is built, and every role-scoped guard takes it. Gating on where a statement sits is what #331 was — a validation running at service registration killedrecover-adminwith SIGABRT 134. Scope the whole subsystem, not the one setting that bit you, and give every serving-only guard a row inProcessRoleGuardTests.ServingOnlyGuards— one per violation, not per subsystem. →347-process-role.md - Proxy-trust boot guard (#260). HSTS (#144) and the per-IP login limiter (#143) only work if the app trusts the proxy's
X-Forwarded-*, which it does only for networks inRateLimiting:TrustedProxies— so an empty list in Production fails the boot rather than running with inert HSTS and a one-bucket limiter. Opt out withRateLimiting:AllowNoTrustedProxies=trueonly for a direct-TLS deploy with no fronting proxy. →260-proxy-trust.md - Production Postgres TLS floor (#261/#262). In Production the effective
sslmodeis a fail-closed allow-list:VerifyCA/VerifyFullsilent,Requirewarns, everything else — including unset — fails the boot.Database:AllowInsecureConnection=trueis the explicit opt-out for the co-located plaintext compose stack. When mapping a libpq param, a missing keyword usually means it is spelled differently in Npgsql (keepalives→Tcp Keepalive), not that it is unmappable. →261-postgres-tls-floor.md - GSS/Kerberos negotiation off by default (#332).
PostgresConnectionStringappendsGSS Encryption Mode=Disableunless the operator set it (detected by presence, not value), textually, after the TLS floor runs — a round-trip through the Npgsql builder would reorder the operator's string and throw on any keyword this version does not know. →332-gss-kerberos.md - Farm timezone + tzdata/ICU (#264). A farm sets its IANA zone in Settings after first login, not at provisioning time. The clock resolves zones via
TimeZoneInfo.FindSystemTimeZoneByIdand fails closed, so the runtime image must carry tzdata + ICU — never an Alpine/chiseled base, neverInvariantGlobalization=true.TimeZoneAvailability.EnsureResolvableasserts a canary at boot for both process roles. →264-farm-timezone.md - A new Production boot guard must be taught to the sim harness (#370). Every guard that fails the boot on missing config, and every config-key add/rename/retire, updates
tools/simulation/bootstrap.sh,docker-compose.sim.ymlandverify-harness.shin the same PR — that harness runs Production config on purpose, is deliberately not in CI, and nothing tells you when you break it. Satisfy guards properly, never by disabling one. →370-sim-harness-boot-guards.md
- Migrate command + prod migration split (#263).
dotnet Cluckwork.Api.dll migrateapplies migrations then exits — the pre-deploy-job entrypoint. Production setsDatabase:MigrateOnStartup=false, so the serving process never runs DDL; the guarantee is ordering, andDatabaseReadyHealthCheckis the backstop (/health/ready503s while any migration is pending). →263-migrate-command.md - Container health probe: the
healthcheckverb (#266). Dispatched before host build (no DI, DB or config), it GETs/health/readyover loopback and exits0only on a 2xx — the hardened image ships no curl/wget, and the probe must never report a false green. →266-container-health-probe.md - Container image hardening (#267). Runtime stage runs non-root (
USER $APP_UID), all three base images are digest-pinned, and Trivy fails the build on a fixable HIGH/CRITICAL. Keep the full glibc base per #264. →267-container-hardening.md - Seed / simulation data is never boot-seeded (#280, #284, #279). Run it explicitly against an already-base-seeded, non-Production database:
ASPNETCORE_ENVIRONMENT=Development dotnet Cluckwork.Api.dll seed --profile demo|simulation(unset env → Production → blocked). The verb migrates, seeds, exits — authoritative and fail-loud. Both profiles require an Owner (#500). →280-seed-and-simulation.md - A write-contract change must update its non-CI callers (#394). Coverage is not uniform: the seeders are covered only to the handler layer, so a validator-only tightening is invisible to every seeder test, and
tools/simulation/k6/plus the Playwright specs are uncovered while a green baseline hides it. Verify the request/status contract by reading the callers, not by running. →394-write-contract-callers.md - SPA E2E lives in
tools/simulation/ui/(#277/#385). Playwright drives the real built SPA over the sameseed --profile simulationfixture k6 uses;web/stays Vitest. Three enforced rules, each of which has caught something: never hardcode a credential, never hardcode English, respect the farm clock. Anything reasoning aboutinertor the accessibility tree must go through CDP (src/ax.ts) — Playwright's own APIs do not modelinert(#501). →277-spa-e2e.md - Production logs: compact JSON on stdout; base layer argument-free (#404). The base
appsettings.jsonConsole entry carriesNameonly — a template left beside the formatter is silently bound instead of it. Trace fields differ by format (@tr/@spin compact JSON,{TraceId}/{SpanId}in the Dev template), and compact JSON serializes every property, so anything pushed viaBeginScope/LogContextreaches the collector. →404-production-logs.md
deploy/.envis gitignored (real values).deploy/.env.exampleholds placeholders only.- Local API debug config uses
dotnet user-secrets(keyed byUserSecretsIdinCluckwork.Api.csproj). - No hardcoded passwords/keys in source — GitGuardian scans PRs. Generate test credentials at runtime.
This repo is host-portable — it must build and run against any host without carrying provider-specific config. The app reads every environment specific from config/env and never names or branches on a hosting provider.
- Stays here (portable operational contract): the Dockerfile and its
HEALTHCHECK,deploy/compose as a local/reference stack, the health probes (/health/live,/health/ready), themigrate/seed/recover-admin/healthcheckverbs,.env.example, and docs that state requirements ("needs tzdata + ICU", "needs a trusted-proxy list", "needs TLS to Postgres"). - Does NOT belong here (goes to a separate deployment/ops repo): provider deploy manifests (
railway.json,fly.toml), IaC, CDN/DNS/edge config, secret-store wiring, provider-named runbooks, and the concrete environment values (proxy CIDRs, CA bundles, connection URLs).
Reviewers: treat a hardcoded provider name in code, config, or a committed doc like a missing test — flag it. Naming a provider as a passing example in prose is tolerable only when no portable phrasing works; prefer the neutral term.
Run one serving instance. Every instance runs DurableJobWorker, but its poll and the three recurring sweeps now run only under a single-leader gate — a session-scoped Postgres advisory lock (pg_try_advisory_lock) on a dedicated, non-pooled connection (#271, closed): at most one instance leads, crash recovery is automatic (a dead leader's session releases the lock), and the contract is at-most-one-leader with at-least-once, idempotent handlers — never "exactly once". That guarantee holds on a session-pinned Postgres endpoint (a direct connection or a session-pooled proxy); under a transaction-pooling proxy (e.g. PgBouncer in transaction mode) the lock can migrate across backends and single-leader is not guaranteed — a backend-PID affinity check narrows but does not close the window, so that topology relies on the at-least-once + idempotent contract until a dedicated session-pinned lease endpoint (#556) lands. The per-account report concurrency cap (#311) was the last independent in-process blocker, and #545 closed it: it now runs on the shared renewable lease (#543), keyed per AccountId, each permit pinned to the backend that granted it — so N replicas enforce one combined per-account permit count, degrading to a bounded per-instance ceiling (never fail-open) under a store outage. The IP-keyed auth limiters (#143) were another — and #544 closed it: they now run on the shared IFixedWindowCounter (#543, Redis-backed with an in-process fallback + alarm), so N replicas enforce one combined per-IP budget instead of N. The step-up grant registry was another — the blocker with teeth — and #338 closed it: replay now lives in the shared IClaimOnceStore (#543) and logout revocation in the durable per-user ApplicationUser.StepUpLogoutEpoch, an integer compared for equality (never a timestamp), so both survive across replicas without a shared clock. #307 is closed and does not license scaling.
Do not extend that list from memory — it was twice derived wrongly, both times a process-local limiter. Re-derive it by walking every AddSingleton/AddHostedService under src/ plus every in-memory state primitive, then excluding deliberately. The run-then-exit verbs are unaffected: they never start hosted services. The #543 shared-state registrations (IConnectionMultiplexer + the IClaimOnceStore/IFixedWindowCounter ports) are not blockers — they are the shared store: #338 wired the IClaimOnceStore grant-replay caller and #544 wired the IFixedWindowCounter auth limiters (both closed above). #545 wired the report cap onto the lease backends directly: ReportConcurrencyCapRegistration constructs RedisLease/InProcessLease itself and pins each permit to its granting backend, rather than resolving a shared ILease port from DI. Redis-backed they are multi-replica-safe, their in-process fallbacks a deliberate alarmed degradation. → 271-single-serving-instance.md
A guard is a test whose job is to fail when someone later does the wrong thing — the migration freeze, the body-reading endpoint check, the simulation manifest's exact counts. A wrong guard is worse than no guard, because it reads as safety. #407 spent five review rounds on one. → 407-writing-a-guard.md; in brief:
- Run a local adversarial pass before the first push — mutation checks, or a second agent handed the diff and told to refute it.
- Mutation first, claim second — never write "this catches X" before running the mutation that makes the guard go red.
- Two misses of the same shape mean the METHOD is wrong — prefer "walk everything, exclude deliberately" over "list what I thought of".
- For a pinned/golden value, prove portability — repetition on one machine cannot detect environment leakage.
- Prefer the boring guard — complexity costs double when the complicated thing is the thing you are trusting.
git config core.hooksPath .githooks enables a ~2s pre-commit hook: unit tests (domain + application) when .cs/.csproj/.sln files are staged, npm run typecheck when web/ files are staged. Integration tests are deliberately excluded (Docker, slow) — CI is the authority. Skip once with --no-verify.
CI fails a PR when a production dependency carries a known high+ advisory — NuGet (dotnet list package --vulnerable) and npm prod deps (npm audit --omit=dev; dev-only advisories are logged, not blocking). Plus dependency-review, CodeQL (advisory), and a weekly scheduled audit. Both audit gates run through .github/scripts/vuln-gate.mjs and fail closed; the only mute is a dated .github/security-exceptions.json entry (exact GHSA id, required expires). → 146-ci-security-gates.md
- NuGet lock files. Every project has a committed
packages.lock.jsonand CI restores--locked-mode, so a package add or bump commits the regenerated lock files in the same commit or CI fails withNU1004. Dependabot NuGet PRs are auto-healed by.github/workflows/dependabot-lockfix.yml. - Pin third-party Actions to a full commit SHA with a trailing
# vX.Y.Zcomment — never a mutable tag (the 2026-03aquasecurity/trivy-actionand 2025-03tj-actions/changed-filescompromises both retargeted tags).actions/*andgithub/*may keep major-version tags.
Two stages, deliberately separate: CI publishes an image per merge; the release PR turns one into a version. docs/releasing.md is the how-to; this section is the invariants; the full mechanism is in 351-releases.md.
-
Every merge to
mainpublishesghcr.io/<owner>/<repo>:sha-<commit>from thepublishjob. Merging the "Release vX.Y.Z" PR drafts the release, promotes that commit's image to:vX.Y.Z, then publishes. -
Promotion is a server-side retag of the existing digest (
--prefer-index=falseis load-bearing; the default wraps a new top-level digest), never a rebuild — a rebuild yields different bytes no scan ever examined. -
Promotion reads the digest from CI's own run artifact, never by resolving
:sha-<commit>, which is mutable in the public window between merge and CI's push. Adding a CI job that should gate a release? Add it topublish.needs— that list is exactly what the digest artifact proves. -
The release stays a draft until its image is promoted; GitHub withholds the git tag for a draft, so a failed promotion leaves no version pointing at nothing.
-
The version comes from conventional commits, damped below 1.0.0. A breaking commit bumps the minor, and breaking means either form — a
!after the type (feat!:,fix!:) or aBREAKING CHANGEfooter. Everything else,feat:included, is a patch. The damping isbump-minor-pre-major+bump-patch-for-minor-pre-majorinrelease-please-config.json, so the mapping flips silently at 1.0.0 — reach it deliberately with aRelease-As:footer. -
A commit-body parse error drops the whole commit (no changelog entry, no bump, green run): never start a line with
word(that has another(inside it..githooks/commit-msgcatches the body, but no local hook sees a PR title — and on a multi-commit PR the title is the release note. -
The release PR is opened with a GitHub App token, not
GITHUB_TOKEN. Every App consumer must keeppermission-*downscoping — omitting it mints the union of every grant the App holds, silently. -
Never hand-edit
.release-please-manifest.jsonorversion.txt— release-please owns them. -
Deploy by digest, never by tag. Obtaining the digest and verifying its origin are two separate problems: get it from the release's
image.jsonasset, verify withgh attestation verify(all three of--bundle-from-oci,--signer-workflow,--source-refare load-bearing and none is the default), then confirm the tag still resolves to the digest you verified — comparing againstreference, never the asset's separatedigestfield. Full commands:docs/releasing.md.Net, stated at exactly the strength the argument supports: the internal gate fails closed for a leaked registry credential. The external gate also stops a branch push substituting its own bytes. Neither stops a branch writer swapping in other attested bytes — the tag/digest comparison above raises the cost, but that actor holds registry write too, so nothing in this repo closes it; branch/dispatch permissions and immutable tags do. And neither survives a merge to
main. Once a backdooredci.ymlis the definition onmain, its attestation is genuinely valid — right signer workflow, right source ref — because--source-refrecords which ref built this, not whether that ref's content is trustworthy. This repo allows a self-merge (mainrequires a PR but zero approving reviews), so that path is open today and no flag on the verify command closes it; review of changes tomainis the only control that does.The paragraph directly above is the canonical statement of the boundary.
docs/releasing.mdand theci.ymlcomment carry a summary and point here rather than restating it, because successive corrections to this claim repeatedly updated one copy and left the others contradicting it. If you correct it, correct it in all three and check they agree. -
Package visibility and the host's pull credential are deploy-side concerns (cluckwork-deploy#6), not this repo's.
origin= GitHub (github.com/mforce/cluckwork);gitea= backup mirror. Useghfor PRs.mainis protected — branch, push, open a PR; don't commit tomain. Branch names:feat/…,chore/…,docs/…,spec/…. PRs squash-merge.- The PR title is the release note. It becomes the squashed commit subject, which release-please parses for both the changelog and the version bump — so a typo'd or non-conventional prefix silently costs a bump.
- Only commit/push when the human asks.
- Keep phase epics in sync: when filing a slice issue, add it to the phase epic's checklist (epic #14 = Phase 1.1, #15 = Phase 1.5); when its PR merges, check it off. Milestone assignment alone is not enough — the epics are how work is navigated.
- Keep documentation in sync (owner directive, 2026-07-17): every PR that adds or changes user-visible behavior updates, in the same PR, (1)
specs/product/GLOSSARY.mdwhen a concept appears or changes meaning, and (2) the SPA Help page + in-app glossary. Treat a missing doc update like a missing test.
Phase 1.0 (MVP) is shipped — epic #13 closed. The egg loop runs end-to-end from the SPA: daily entry (by grade) → submit → egg lots → stock → customer → sales order → FIFO allocation → stock decremented.
Phase 1.1 (Operational fill) is shipped — epic #14 closed 2026-08-11: RBAC UI, product catalog / egg-grade management, inventory movement ledger, feed/water/mortality, expenses, payments, dashboard, reports, audit UI, exports, i18n infrastructure. Follow-on work discovered while shipping it moved to epic #15.
Current phase: 1.5 (epic #15, specs/product/specs.md §6) — egg product hardening: legacy import, inventory reconciliation, alert center, packaging inventory, additives/supplements, vaccination records, native-speaker es/tl review, deployment readiness, and the Phase 1.1 carryover items on the epic.
Domain terms (flock lifecycle, daily entry states, egg lots, grades, culls, FIFO allocation) are defined in specs/product/GLOSSARY.md — read it before renaming or modeling anything, and docs/architecture.md for how those states actually connect.
This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships. When the user types /graphify, use the installed graphify skill or instructions before doing anything else.
- For codebase questions, run
graphify query "<question>"first whengraphify-out/graph.jsonexists. Usegraphify path "<A>" "<B>"for relationships andgraphify explain "<concept>"for focused concepts — these return a scoped subgraph, usually much smaller thanGRAPH_REPORT.mdor raw grep output. - Dirty
graphify-out/files are expected after hooks or incremental updates and are not a reason to skip graphify. Only skip it if the task is about stale graph output, or the user says not to. - If
graphify-out/wiki/index.mdexists, use it for broad navigation instead of raw source browsing. ReadGRAPH_REPORT.mdonly for broad architecture review. - Run
graphify update .periodically (AST-only, no API cost) — but not as part of every code change: bundling it into each commit inflates PRs with unrelated changed lines.