Repository navigation
docs: split the README into audience-scoped docs and adopt repo-template scaffolding #548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # Editor defaults, honoured natively by most editors and IDEs with no toolchain, | ||
| # no dependency and no CI job. Its job is to stop whitespace-only diffs from | ||
| # burying real changes in review. | ||
| # | ||
| # It is a convenience, NOT a gate — nothing enforces it. Do not cite it as the | ||
| # reason a formatter is unnecessary. | ||
| root = true | ||
|
|
||
| [*] | ||
| charset = utf-8 | ||
| end_of_line = lf | ||
| insert_final_newline = true | ||
| trim_trailing_whitespace = true | ||
| indent_style = space | ||
| indent_size = 2 | ||
|
|
||
| # Markdown: two trailing spaces are a hard line break, so trimming them changes | ||
| # the rendered output. | ||
| [*.md] | ||
| trim_trailing_whitespace = false | ||
|
|
||
| # .NET | ||
| [*.{cs,csx}] | ||
| indent_size = 4 | ||
|
|
||
| [*.{csproj,props,targets,sln}] | ||
| indent_size = 2 | ||
|
|
||
| # Generated — do not reformat. Schema docs are byte-compared by CI (#417) and a | ||
| # lock file is written by the tool that owns it. | ||
| [docs/schema/**] | ||
| insert_final_newline = unset | ||
| trim_trailing_whitespace = unset | ||
|
|
||
| [**/packages.lock.json] | ||
| insert_final_newline = unset | ||
|
|
||
| [**/package-lock.json] | ||
| insert_final_newline = unset | ||
|
|
||
| [Makefile] | ||
| indent_style = tab |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # Line endings are normalised to LF in the repo and checked out as LF | ||
| # everywhere, Windows included. | ||
| # | ||
| # Why this file exists at all: `.githooks/*` are POSIX `sh` scripts that git | ||
| # executes via their shebang. A CRLF checkout appends `\r` to the shebang line, | ||
| # and the kernel then looks for an interpreter literally named `/bin/sh\r` — | ||
| # which fails with a message that names neither the hook nor the cause. The same | ||
| # `\r` silently ends up inside every string a workflow's `run:` block compares, | ||
| # and inside every value `tools/simulation/*.sh` reads back out of an env file. | ||
| # | ||
| # `core.autocrlf` is a per-machine setting, so it cannot be relied on. This file | ||
| # ships with the repo and applies to every clone. | ||
| * text=auto eol=lf | ||
|
|
||
| # Executable text files where a stray CR is fatal rather than cosmetic. Listed | ||
| # explicitly so the intent survives someone relaxing the line above. | ||
| *.sh text eol=lf | ||
| .githooks/* text eol=lf | ||
|
|
||
| # Generated. `linguist-generated` keeps them out of language stats and collapses | ||
| # them in pull-request diffs; they are still tracked, still reviewed by | ||
| # regenerating, never by hand-editing (#417 for the schema docs). | ||
| docs/schema/** linguist-generated=true | ||
| **/packages.lock.json linguist-generated=true | ||
| **/package-lock.json linguist-generated=true | ||
|
|
||
| # Binary — never diffed, never line-ending-normalised. | ||
| *.png binary | ||
| *.jpg binary | ||
| *.ico binary | ||
| *.woff binary | ||
| *.woff2 binary | ||
| *.dump binary |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| # Required reviewers per path. Gitignore syntax; the LAST matching rule wins, so | ||
| # put the narrowest rules last. | ||
| # | ||
| # Covers the one gap no gate in this repo can: a merge to `main` that rewrites | ||
| # the workflows themselves. A backdoored `ci.yml` on `main` produces a build | ||
| # attestation that is genuinely valid — right signer workflow, right source ref | ||
| # — because `--source-ref` records which ref built the image, not whether that | ||
| # ref's content is trustworthy. Review of changes to `main` is the only control | ||
| # that closes it. → the "Deploy by digest" bullet in AGENTS.md. | ||
| # | ||
| # SHIPPED INERT, deliberately. Today `main` requires a pull request with | ||
| # `required_approving_review_count: 0` and a single owner with write access, so | ||
| # "Require review from Code Owners" would be either a deadlock (nobody else can | ||
| # approve) or theatre (the owner approves their own PR). An entry naming an | ||
| # unresolvable user is silently ignored by GitHub, so a plausible-looking broken | ||
| # file is worse than an empty one. | ||
| # | ||
| # To activate, all three are required: | ||
| # 1. Uncomment and replace @OWNER with a user or team that has write access — | ||
| # someone other than the person who normally opens these PRs. | ||
| # 2. Branch protection → tick "Require review from Code Owners", and raise | ||
| # "Required approving reviews" above 0. | ||
| # 3. Open a PR touching .github/ and confirm the owner is auto-requested. | ||
|
|
||
| # * @OWNER | ||
|
|
||
| # Paths where a self-merge rewrites what every other check asserts. | ||
| # /.github/ @OWNER | ||
| # /.githooks/ @OWNER | ||
| # /.github/CODEOWNERS @OWNER | ||
| # /docs/decisions/ @OWNER | ||
| # /AGENTS.md @OWNER | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| <!-- | ||
| The PR TITLE is the release note. Squash-merge takes the commit subject from it, | ||
| and release-please parses that for the changelog and the bump — so a | ||
| non-conventional prefix silently costs the bump, with a green run. No local hook | ||
| sees the PR title. | ||
|
|
||
| feat(scope): … fix(scope): … docs|chore|test|ci|build|style(scope): … | ||
| feat!: … or a BREAKING CHANGE: footer | ||
| --> | ||
|
|
||
| ## What and why | ||
|
|
||
| <!-- What changed and the problem it solves, in a few lines. Link the issue. | ||
| This body becomes the squashed commit in main — keep it short, and mind the | ||
| `word((` parser trap: see CONTRIBUTING.md#commit-messages. --> | ||
|
|
||
| ## How it was verified | ||
|
|
||
| <!-- | ||
| Commands run and what they printed — not "tests pass". For a guard, name the | ||
| mutation you ran and which test went red. → docs/decisions/407-writing-a-guard.md | ||
| --> | ||
|
|
||
| ## Checklist | ||
|
|
||
| Delete any line that does not apply — an inapplicable ticked box is noise. | ||
|
|
||
| - [ ] The **PR title** is a conventional commit, and is the release note I want. | ||
| - [ ] Tests cover the change, and I watched the new ones fail first. | ||
| - [ ] Any aggregate mutation bumps `Version` and has a parallel-race test. | ||
| - [ ] Any new guard was **mutation-checked**: green baseline, mutant red on that | ||
| guard's own named assertion, baseline green after restoring. | ||
| - [ ] A new migration ships with regenerated `docs/schema/` in this PR (#417). | ||
| - [ ] A new Production boot guard or config key also updates the sim harness (#370). | ||
| - [ ] Docs updated in this PR — `specs/product/GLOSSARY.md` and the SPA Help page | ||
| for anything user-visible. | ||
| - [ ] The slice is on its phase epic's checklist (#14 Phase 1.1, #15 Phase 1.5). | ||
| - [ ] A non-obvious decision is recorded in `docs/decisions/`, or there is none. | ||
| - [ ] No hardcoded credential, in application **or** test code. | ||
| - [ ] No hardcoded hosting-provider name in code, config, or a committed doc. | ||
| - [ ] Any new third-party Action is pinned to a **full commit SHA** with a | ||
| trailing `# vX.Y.Z` comment. | ||
| - [ ] A package add or bump commits the regenerated lock file in the same commit. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| # Contributing | ||
|
|
||
| Humans start here. Coding agents start at [`AGENTS.md`](AGENTS.md), which is the | ||
| canonical rule set — this file is the short human-facing path through it and | ||
| links there rather than restating rationale. | ||
|
|
||
| ## Once per clone | ||
|
|
||
| ```bash | ||
| git config core.hooksPath .githooks | ||
| ``` | ||
|
|
||
| Enables two fast hooks: **pre-commit** (unit tests for staged .NET changes, | ||
| `npm run typecheck` for staged `web/` changes) and **commit-msg** (rejects a | ||
| message release-please would silently drop from the changelog). Integration tests | ||
| are deliberately excluded — Docker, slow; CI is the authority. Skip once with | ||
| `--no-verify`. | ||
|
|
||
| ## Local development | ||
|
|
||
| Prerequisites: **.NET 10 SDK**, **Docker**, **Node 26+**. | ||
|
|
||
| **Frontend:** | ||
|
|
||
| ```bash | ||
| cd web | ||
| npm install | ||
| npm run dev # http://localhost:5173, proxies /api to the API | ||
| ``` | ||
|
|
||
| **API from the IDE / CLI** — start just the database, then run the API (config | ||
| comes from user-secrets, never files): | ||
|
|
||
| ```bash | ||
| docker compose -f deploy/docker-compose.dev.yml up -d # Postgres on :5432 | ||
| dotnet run --project src/Cluckwork.Api | ||
| ``` | ||
|
|
||
| `ASPNETCORE_ENVIRONMENT` matters: unset means Production, which fails the boot | ||
| against a plaintext local Postgres (the #261/#262 TLS floor). | ||
|
|
||
| A fresh database has no admin user — base data is migration-baked, credentials | ||
| never are. Provision one: [first admin provisioning](docs/runbooks/first-admin-provisioning.md). | ||
|
|
||
| ### Resetting the dev database | ||
|
|
||
| Always wipe through Compose: | ||
|
|
||
| ```bash | ||
| docker compose -f deploy/docker-compose.dev.yml down -v | ||
| docker compose -f deploy/docker-compose.dev.yml up -d | ||
| ``` | ||
|
|
||
| Do **not** `docker volume rm` a volume name from memory. The live volume is | ||
| `cluckwork-dev_cluckwork-dev-pg18`, and an older `…-pg` volume may still be lying | ||
| around from before the Postgres 18 bump — removing that one succeeds, prints the | ||
| name back, and leaves the real database untouched, so a wipe can look like it | ||
| worked when nothing happened. | ||
|
|
||
| You need a wipe (not a migration) if boot fails with | ||
| `42P07: relation "Accounts" already exists`. That database still carries the | ||
| pre-squash migration history: the 34 migrations were replaced by a single | ||
| `InitialCreate`, which EF then sees as pending and tries to apply over tables | ||
| that already exist. Such a database cannot migrate forward — recreate it. | ||
|
|
||
| ## Tests | ||
|
|
||
| ```bash | ||
| dotnet test Cluckwork.sln # integration tests spin up Postgres via Docker | ||
| cd web && npm test # Vitest + Testing Library | ||
| ``` | ||
|
|
||
| Expectations, enforced at review like a missing feature: | ||
|
|
||
| - every change under `src/` ships with tests in the same PR; | ||
| - every change under `web/` ships with Vitest tests in the same PR; | ||
| - **every aggregate mutation bumps `Version`** and gets a parallel-race | ||
| integration test — see the `Version` bullet in [`AGENTS.md`](AGENTS.md#conventions-follow-these); | ||
| - a new **guard** (a test whose job is to fail when someone later does the wrong | ||
| thing) is mutation-checked before you claim it catches anything — | ||
| [`docs/decisions/407-writing-a-guard.md`](docs/decisions/407-writing-a-guard.md). | ||
|
|
||
| ## Changing the database schema | ||
|
|
||
| **Add a migration. Never edit `InitialCreate`.** | ||
|
|
||
| ```bash | ||
| # Against the local dev Postgres. The connection is fail-closed (#318): there is | ||
| # no default, and every target is held to the same TLS floor as a Production | ||
| # boot — the loopback opt-out below is what permits plaintext, and only for | ||
| # localhost/127.0.0.1/::1. | ||
| CLUCKWORK_MIGRATIONS_CONNECTION='Host=localhost;Port=5432;Database=cluckwork;Username=…;Password=…' \ | ||
| CLUCKWORK_MIGRATIONS_ALLOW_INSECURE_LOOPBACK=true \ | ||
| dotnet ef migrations add <Name> \ | ||
| -p src/Cluckwork.Infrastructure -s src/Cluckwork.Api | ||
| ``` | ||
|
|
||
| EF never re-runs an applied migration, so a column hand-folded into | ||
| `InitialCreate` **silently does not exist** on any booted database. It surfaces as | ||
| broken behaviour — a failing login, a missing field — not as a migration error, | ||
| which is what makes it worth a rule. `InitialCreate` is also not regenerable: it | ||
| carries four `lower("Name")` expression indexes EF cannot model plus the guarded | ||
| reference-data SQL, and re-adding it mints a new timestamp that desynchronises | ||
| `__EFMigrationsHistory` everywhere. `MigrationSecurityReviewTests` fails if it | ||
| stops being the first migration or loses its recorded id. | ||
|
|
||
| **Same PR:** regenerate the schema docs (`tools/schema-docs/generate.sh`) and | ||
| commit them — CI runs `generate.sh --check` and fails a stale PR (#417). | ||
|
|
||
| Full reasoning: [`docs/decisions/407-migration-freeze.md`](docs/decisions/407-migration-freeze.md). | ||
|
|
||
| ## Branches and PRs | ||
|
|
||
| - **`main` is protected.** Branch, push, open a PR — never commit to `main`. | ||
| - Branches: `feat/…`, `fix/…`, `chore/…`, `docs/…`, `spec/…`. PRs squash-merge. | ||
| - The PR body is prefilled from | ||
| [`pull_request_template.md`](.github/pull_request_template.md). Delete lines | ||
| that do not apply rather than ticking them. | ||
| - **Keep the phase epic in sync**: a new slice issue goes on the epic's checklist | ||
| (epic #14 = Phase 1.1, #15 = Phase 1.5); tick it when the PR merges. Milestone | ||
| assignment alone is not enough — the epics are how work is navigated. | ||
|
|
||
| ## Commit messages | ||
|
|
||
| [Conventional Commits](https://www.conventionalcommits.org/). **Subject:** | ||
| `type(scope): summary`, lowercase type, no space before the colon. The scope is | ||
| free-form — the area touched. | ||
|
|
||
| | type | changelog section | example | | ||
| |---|---|---| | ||
| | `feat` | Features | `feat(eggs): make cracked and dirty eggs sellable stock via condition grades` | | ||
| | `fix` | Bug fixes | `fix(sales): reject fractional order-line quantities` | | ||
| | `perf` | Performance | `perf(reports): stream the CSV export instead of buffering it` | | ||
| | `refactor` | Refactoring | `refactor(api): extract service registration from Program` | | ||
| | `docs` | Documentation | `docs(agents): record the guard-writing rules #407 paid five rounds for` | | ||
| | `ci` | *hidden* | `ci(e2e): workflow_dispatch job for the Playwright smoke suite` | | ||
| | `test` | *hidden* | `test(e2e): Playwright smoke suite for the SPA over the #243 sim fixture` | | ||
| | `build` | *hidden* | `build(deps): bump Npgsql to 10.0.2` | | ||
| | `chore` | *hidden* | `chore(web): drop the unused date-fns dependency` | | ||
| | `style` | *hidden* | `style(web): apply Prettier to the untouched settings screens` | | ||
|
|
||
| Add `!` for a breaking change — `feat(api)!: drop the v0 endpoints` — or a | ||
| `BREAKING CHANGE:` footer. *Hidden* types stay out of the changelog text but | ||
| **still bump the patch digit**; they cost a number, not a deploy. What each type | ||
| does to the version number is in [`docs/releasing.md`](docs/releasing.md#what-decides-the-version). | ||
|
|
||
| Two traps, both producing a **green run with no changelog entry and no bump**: | ||
|
|
||
| **1. The PR title is the release note.** On a multi-commit PR the squashed subject | ||
| comes from it, and no local hook can see it. A non-conventional title silently | ||
| costs the bump. | ||
|
|
||
| **2. A body line starting with `word(` that has another `(` inside it** breaks the | ||
| parser — and an unparseable commit is dropped *entirely*, not just that line. Two | ||
| commits have already been lost this way. Backticks do not protect it: | ||
|
|
||
| ```text | ||
| Assert.Single(AllMigrations()) ← breaks the parser | ||
| Assert.Single(AllMigrations()) ← fine (indented) | ||
| - Assert.Single(AllMigrations()) ← fine (list item) | ||
| see Assert.Single(AllMigrations()) ← fine (word in front) | ||
| ``` | ||
|
|
||
| Only line *starts* matter, so `see foo(x) and bar(y())` mid-sentence was never a | ||
| problem. `.githooks/commit-msg` catches this in your own message and prints the | ||
| rewrite; it cannot see a PR title. | ||
|
|
||
| ## Reviewing | ||
|
|
||
| Block on these like a missing test: | ||
|
|
||
| - a hardcoded credential, in application **or** test code (GitGuardian scans PRs); | ||
| - a hardcoded hosting-provider name in code, config, or a committed doc — see | ||
| **Host-agnostic repo** in [`AGENTS.md`](AGENTS.md#host-agnostic-repo-deployment-boundary); | ||
| - a third-party Action pinned to a tag rather than a full commit SHA; | ||
| - an aggregate mutation with no `Version++`; | ||
| - a new Production boot guard that does not also update the sim harness (#370); | ||
| - a user-visible change with no matching doc update — `specs/product/GLOSSARY.md` | ||
| when a concept appears or changes meaning, plus the SPA Help page and in-app | ||
| glossary. | ||
|
|
||
| ## Dependencies | ||
|
|
||
| - A package add or bump commits the regenerated `packages.lock.json` **in the same | ||
| commit** — CI restores `--locked-mode` and otherwise fails with `NU1004`. | ||
| - A known-vulnerable production dependency fails CI. The only mute is a dated | ||
| entry in `.github/security-exceptions.json` — see [`SECURITY.md`](SECURITY.md). |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the documented activation steps are followed,
/.github/,/.githooks/, and/docs/decisions/do not recursively own files under those directories in CODEOWNERS syntax; directory contents require patterns such as/.github/**. Consequently workflow changes such as.github/workflows/ci.ymlwould remain ownerless even though this file says activation closes the self-merge control gap.AGENTS.md reference: AGENTS.md:L236-L242
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rejected, with evidence — a trailing-slash CODEOWNERS pattern is recursive.
GitHub's own CODEOWNERS syntax examples state it twice:
So
/.github/would own.github/workflows/ci.ymlonce activated;/.github/**is not required. Leaving the patterns as they are.(Restating for the record: the file ships inert — every pattern is commented out — because
maincurrently hasrequired_approving_review_count: 0and one owner with write access, so activating it would be a deadlock or self-approval. The claim in the header is about what activation would close, not about anything this PR turns on.)