cleanup phase 7

This commit is contained in:
hamid
2026-08-02 18:58:46 +03:30
parent 51e86a1e5f
commit 72ab290da1
15 changed files with 329 additions and 189 deletions
+167
View File
@@ -0,0 +1,167 @@
---
name: backend-feature
description: >-
Add a feature to the Balinyaar .NET server — a command, a query, or both — end to end: the Application
slice, the controller, EF configuration/migration if it touches a table, tests, and the doc updates it
triggers. Use when implementing a new endpoint or extending an existing one anywhere under server/.
---
# Balinyaar Backend Feature
The sequence for shipping one CQRS slice, from the Application layer to a green gate.
**Precedence.** This skill is the **procedure** — what order to do things in. The **rules** within each
step live in `docs/rules/server/` and this skill defers to them; it doesn't restate them.
| For | Read |
|-----|------|
| The dispatcher, folder shape, `OperationResult`, the controller skeleton, authorization | [docs/rules/server/cqrs.md](../../../docs/rules/server/cqrs.md) |
| Projects, layers, the seam catalogue, startup wiring | [docs/rules/server/structure.md](../../../docs/rules/server/structure.md) |
| EF Core, migrations, soft-delete, audit, config-as-rows, state machines, snapshots, uniqueness | [docs/rules/server/persistence.md](../../../docs/rules/server/persistence.md) |
| Anything on the money path — ledger, refunds, BNPL, payouts, invoices | [docs/rules/server/money.md](../../../docs/rules/server/money.md) |
| Auth, JWE, sessions, field encryption, tenancy | [docs/rules/server/identity.md](../../../docs/rules/server/identity.md) |
| C# style, naming, async, testing | [docs/rules/server/conventions.md](../../../docs/rules/server/conventions.md) |
| The gate, and what "done" means | [docs/rules/shared/git-and-gates.md](../../../docs/rules/shared/git-and-gates.md) |
**Stack:** ASP.NET Core (.NET 10), Clean Architecture, CQRS on `martinothamar/Mediator` (a source generator —
**not MediatR**; there is no `IMediator` anywhere in this codebase), EF Core, FluentValidation, Mapster.
---
## 1. Scope it before writing anything
- **Which area?** The Application feature areas mirror the Domain entity folders — `Identity`, `Geography`,
`Catalog`, `Verification`, `Search`, `Booking` (singular, pre-payment) / `Bookings` (plural, post-payment),
`Payments`, `Refunds`, `Invoices`, `Bnpl`, `Payouts`, `Reviews`, `PatientCareRecords`, `Messaging`,
`PartnerCenters`, `Configuration`, `Audit`, `Analytics`, `Holidays`, `Notifications`, `SupportAlerts`. Full
list and the schema-per-area mapping: [structure.md](../../../docs/rules/server/structure.md) §2.
- **Command or query, or both?** A command mutates; a query reads. Most features are a matched pair (create
+ get, or update + list).
- **Find a sibling to mirror.** Grep the area's existing folder —
`Features/<Area>/{Commands,Queries}/` — for a feature shaped like the one you're adding. Copying a live
pattern beats inventing a new one.
- **Does it touch money?** (ledger, refunds, invoices, BNPL, payouts) → read
[money.md](../../../docs/rules/server/money.md) **first**. The invariants there (integer IRR, balanced
ledger postings, webhook idempotency, snapshot-at-compute-time) are not suggestions.
- **Does it add or change a table?** → read [persistence.md](../../../docs/rules/server/persistence.md) §57
before modeling it (soft-delete filters, forward-only status machines, snapshot fields, uniqueness
patterns all have a house pattern — don't reinvent one).
- **Does it need a new external dependency** (a vendor, a rail)? It becomes an interface in
`Application/Contracts/`, mock in `CrossCutting/Seams/`, real in `CrossCutting/Seams/Real/`, selected by a
`Seams:<rail>:Provider` config key that **falls closed to the mock**. See
[structure.md](../../../docs/rules/server/structure.md) §3.
---
## 2. The Application slice
```
Baya.Application/Features/<Area>/
├── Commands/<VerbNoun>Command/
│ ├── <VerbNoun>Command.cs record : IRequest<OperationResult<T>>
│ ├── <VerbNoun>Command.Handler.cs internal sealed class : IRequestHandler<…>
│ └── <VerbNoun>Command.Validator.cs AbstractValidator<Command> (omit when there is nothing to validate)
└── Queries/<VerbNoun>Query/
├── <VerbNoun>Query.cs
├── <VerbNoun>Query.Handler.cs
└── <VerbNoun>Query.Result.cs record Result(…) ← the DTO returned
```
1. Create the folder, one type per file, file name matching the type name.
2. The request is a `record`; the handler is `internal sealed`; return `OperationResult<T>` — never throw
for an expected failure (`SuccessResult`/`FailureResult`/`NotFoundResult`/`ConflictResult` map to
200/400/404/409). Let a genuinely unexpected exception propagate to the global `ExceptionHandler`.
3. Add a FluentValidation validator if the request takes input. **Never validate a route-supplied id in the
body command** — route values aren't bound into it.
4. Query: `AsNoTracking()` + `.Select()` straight to the DTO — never hydrate an entity graph to map it in
memory. Command: use `Include` only when you need navigation properties loaded to mutate the aggregate,
access the DB through `IUnitOfWork`, and `CommitAsync` once at the end.
Full rules and the validator/OperationResult examples: [cqrs.md](../../../docs/rules/server/cqrs.md) §13.
---
## 3. Persistence — only if you added or changed a table
1. One `IEntityTypeConfiguration<T>` in `Persistence/Configuration/<Area>Config/`.
2. A soft-deletable entity **must** declare `HasQueryFilter(o => !o.IsDeleted)` — without it, deleted rows
leak into every query that doesn't explicitly exclude them.
3. A lifecycle `status` column is a forward-only machine: `const string` codes, a private setter, cohesive
transition methods, a static allowed-edges table. The handler pre-checks and returns a clean `409` — it
never throws for "already moved."
4. A row that represents a past agreement (a price, an address, a policy, a deadline) is a **snapshot**
frozen at compute time, never re-derived from a later edit to its source.
5. Money-critical constants (rates, deadlines, tolerances) are read via `IPlatformConfig.GetConfig<T>`
**never hardcoded**, and never re-read for an already-priced row.
```bash
dotnet ef migrations add <Name> --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
dotnet ef database update --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api
```
Full patterns, with the exact uniqueness/snapshot/state-machine tables:
[persistence.md](../../../docs/rules/server/persistence.md).
---
## 4. The controller
```csharp
[ApiVersion("1")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[Display(Description = "One-line description shown in Swagger")]
[Authorize(ConstantPolicies.DynamicPermission)] // or [Authorize], or omit for public
public sealed class MyFeatureController(ISender sender) : BaseController
{
[HttpPost("[action]")]
[ProducesOkApiResponseType<MyCommandResult>]
public async Task<IActionResult> CreateSomething(MyCommand command, CancellationToken ct)
=> OperationResult(await sender.Send(command, ct));
}
```
- `sealed`, inject `ISender` via the primary constructor, always `base.OperationResult(result)` — never
`Ok()`/`BadRequest()`/`NotFound()` directly.
- Never hardcode a route string. If the method name doesn't read cleanly as the URL segment
`SnakeCaseParameterTransformer` will produce, rename the method instead.
- Pick the narrowest authorization that fits: none (truly public) → `[Authorize]` (any authenticated user) →
`[Authorize(ConstantPolicies.DynamicPermission)]` (role/claim-gated admin action). Table and rate-limiting
notes: [cqrs.md](../../../docs/rules/server/cqrs.md) §4.
---
## 5. Tests
1. **Handler unit test** (xUnit + NSubstitute + FluentAssertions), Arrange-Act-Assert, named
`{MethodUnderTest}_{Scenario}_{ExpectedOutcome}`. Test the handler directly, not the controller.
2. **At least one `WebApplicationFactory<Program>` integration test** in `Baya.Test.Api` for the area,
covering: happy path → 200, unauthenticated → 401, validation failure → 400 with field detail.
3. The recurring-job scheduler is dormant under `Testing`, so a background tick can't make an integration
test flaky — you don't need to account for it.
Examples and the full testing convention: [conventions.md](../../../docs/rules/server/conventions.md) §8.
---
## 6. Docs this feature triggers — in the same change
- **[`docs/integration/domains/<domain>.md`](../../../docs/integration/domains/index.md)** — add the new
endpoint with its verdict (`wired`/`unwired`/`phantom`), matching the client `services/` domain it belongs
to.
- **The OpenAPI snapshot** — regenerate `docs/integration/openapi/swagger.v1.json` per
[openapi/README.md](../../../docs/integration/openapi/README.md) and update its provenance table (date,
commit, path/operation counts) in the same change. A snapshot with stale provenance is what that
convention exists to prevent.
- **[`docs/status/backlog.md`](../../../docs/status/backlog.md)** — tick the row if this closes a filed
item. Never delete a row; a ticked row is the record that it shipped.
- **A reference file in `docs/rules/server/`** — only if the feature introduces a genuinely new reusable
pattern, seam, or base class. Don't add prose for a feature that just follows the existing pattern.
---
## 7. Before you call it done
Run the server gate — `dotnet build Baya.sln` (**zero new warnings**) and `dotnet test Baya.sln` — and read
your diff as if reviewing the PR. Full "what done means" checklist:
[git-and-gates.md](../../../docs/rules/shared/git-and-gates.md) §2.
+107
View File
@@ -0,0 +1,107 @@
---
name: flow-testing
description: >-
Boot both sides of Balinyaar locally and walk a real user journey end to end — the right seeded account,
the right flow doc, and knowing whether you just proved the real path or a mock answering. Use before
claiming a fix or feature works, or when asked to test, verify, or demo a flow.
---
# Balinyaar Flow Testing
Exercising a flow proves something only if you know which half of the stack actually answered. This is the
procedure; the facts it points at (ports, accounts, known failure modes) live in
[docs/flows/testing-setup.md](../../../docs/flows/testing-setup.md) and are kept current there — don't copy
them here, they will drift.
---
## 1. Boot it
The five-minute path, verbatim from [testing-setup.md](../../../docs/flows/testing-setup.md#the-five-minute-path):
```bash
# API — mock SMS or request_otp 500s
cd server
Seams__Sms__Provider=mock dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj
# client
cd client && npm install && npm run dev # http://localhost:3000/fa
# read the OTP — the console does NOT print it
curl http://localhost:5002/api/v1/dev/last_otp/09120000010
```
No database setup: the committed dev config points at an already-seeded remote SQL Server. If anything here
doesn't match reality when you run it, **testing-setup.md is wrong and needs a fix in the same change** — it
carries a `Last verified` stamp for exactly this reason.
---
## 2. Check the mock-vs-real map *before* you conclude anything
A flow "working" through a mocked domain proves the UI, not the server. Before testing:
1. Open [docs/integration/domains/index.md](../../../docs/integration/domains/index.md) — the census table
names which of the 22 client `services/` domains are real vs **mock** (currently 15 real, 7 mock:
`admin`, `bnpl`, `partnerCenter`, `patientRecords`, `payouts`, `refunds`, `verification`).
2. A mocked domain is a `USE_<DOMAIN>_MOCK` flag in `client/src/services/<domain>/constants.ts` — check it
directly if you need certainty for the exact domain you're touching.
3. State your finding in terms of which one you exercised: "the booking flow works end-to-end against the
real server" is a different claim from "the admin console renders correctly against its mock" — never
report the second as if it were the first.
---
## 3. Pick the right seeded account
Demo accounts, their roles, and what each one demonstrates are tabulated in
[testing-setup.md](../../../docs/flows/testing-setup.md#demo-accounts) — read it there rather than assuming
a phone number. One standing gap to route around: **the seeded admin accounts (`…020` `super_admin`,
`…021` `finance`) get 403 on every real admin endpoint** (a `DynamicPermission` / role-literal mismatch).
The admin backoffice is only testable against the client's mock; don't spend time trying to walk it against
the real API without first checking whether that gap has been closed.
---
## 4. Walk the flow
[docs/flows/index.md](../../../docs/flows/index.md) is the atlas — one file per user-meaningful journey,
each answering exactly three questions: what it does, what's mocked *for that journey specifically*, and how
to test it. Open the one file that matches what you're testing rather than guessing the steps; it's the
one place gap numbers and REQ references for that journey are tracked.
---
## 5. Two things that will silently invalidate your test
- **The scheduler is live while you test.** `booking_request_expiry` runs every 60 seconds (hardcoded) and
flips an un-actioned request to `expired_no_response` / `payment_deadline_expired` out from under you. Act
on a request promptly, or create a fresh one rather than trying to reuse an old test artifact.
- **The OTP endpoints are rate-limited together.** `request_otp` and `verify_otp` share one bucket, 5 calls
per 60 s per IP — a login is 2 calls, so that's **two logins per minute, total**. Space scripted logins
≥ 40 s apart (see [testing-setup.md](../../../docs/flows/testing-setup.md#scripting-logins) for a working
script) or you'll 429 and misread it as a bug.
---
## 6. When the seeded world has aged out
There is no in-app reseed — both seeders guard on natural keys, so re-running never refreshes stale dates.
If the scenario you need (an "upcoming" booking, an open dispute window, a pending request) no longer exists
because the world was seeded days ago:
- **Fastest fix:** create the scenario fresh yourself (customer → search → booking request → accept → pay) —
this is the intended way to exercise booking-request and checkout-and-payment anyway.
- **Full reseed:** only against a **local** database — `docker compose down -v && docker compose up -d` under
`server/`, then boot. **Never drop the shared remote database** casually; it backs the live demo deployment
and other people's sessions.
---
## 7. Report what you actually saw
Name the account you used, the domain's mock/real status, and the exact response (status code, error
message) rather than "it worked" — the troubleshooting table in
[testing-setup.md](../../../docs/flows/testing-setup.md#troubleshooting) exists because several failure
modes here look identical to an unrelated bug (a rate limit looks like a crash; `/healthz/ready` failing on
Windows looks like the app is down). Check it before filing something as a new defect.
-33
View File
@@ -1,33 +0,0 @@
# Git hooks
Repo-managed git hooks (they live in version control, unlike `.git/hooks`).
## Enable (once per clone)
```bash
git config core.hooksPath .githooks
```
## `pre-commit` — secret scan
A fast, dependency-free backstop against a credential leaking into a file that shouldn't hold one
(refinement-phase-5). It rejects a commit that stages:
- the retired hardcoded admin password `qw123321`, anywhere,
- private-key material or an AWS access-key id, anywhere,
- the deployment's SQL Server host `87.107.152.16` **outside the declared config files**,
- a **real** connection-string password in any `appsettings*.json` **outside the declared config files**
(elsewhere only the `SET_VIA_USER_SECRETS_OR_ENV` placeholder is allowed).
**Declared config files.** The pre-launch demo deployment configures itself from committed files rather
than a secret store ([DEPLOY.md](../DEPLOY.md)), so a short allow-list — `appsettings.Development.json`,
`docker-compose.yml`, `telegram-otp-bot/.env.example`, `DEPLOY.md` — is exempt from the last two checks.
The list is maintained in the `declared_config` function in the hook and is the honest record of where the
repo's secrets are. **Shrink it, never grow it**: once real users exist, those values must be rotated and
moved out of git.
It scans only staged additions, so it is quick. It is **not** a replacement for a full scanner
(gitleaks / trufflehog) in CI — it is the local first line of defence.
Bypass a false positive with `git commit --no-verify` (use sparingly, and only when you are certain the
flagged line is not a secret).
-82
View File
@@ -1,82 +0,0 @@
#!/usr/bin/env bash
#
# Balinyaar secret-scanning pre-commit hook (refinement-phase-5).
# Blocks a commit that stages an obvious credential. This is a fast, dependency-free backstop for the
# root CLAUDE.md rule "Never commit secrets" — not a replacement for gitleaks/trufflehog in CI.
#
# Enable once per clone: git config core.hooksPath .githooks
# Bypass a false positive: git commit --no-verify (use sparingly, and only when you are certain)
#
set -euo pipefail
# Committed placeholders are allowed — real values are not. Keep in sync with StartupSecretsGuard.
PLACEHOLDER='SET_VIA_USER_SECRETS_OR_ENV'
# Files that deliberately carry live deployment credentials, because the pre-launch demo deployment
# configures itself from committed files rather than a secret store (see DEPLOY.md). They are exempt from
# the connection-string and known-host checks ONLY — the private-key and AWS-key checks still apply to
# them, and every other file in the repo is scanned exactly as strictly as before.
#
# This list is the honest record of where the repo's secrets are. Shrink it, never grow it: the moment
# real users exist, these values must be rotated and moved out of git.
declared_config() {
case "$1" in
server/src/API/Baya.Web.Api/appsettings.Development.json) return 0 ;;
docker-compose.yml) return 0 ;;
telegram-otp-bot/.env.example) return 0 ;;
DEPLOY.md) return 0 ;;
*) return 1 ;;
esac
}
# Only scan added/changed lines in text files that are staged.
staged=$(git diff --cached --name-only --diff-filter=ACM)
[ -z "$staged" ] && exit 0
violations=0
report() { printf ' ✖ %s\n' "$1"; violations=$((violations + 1)); }
while IFS= read -r file; do
# Skip this hook, lockfiles, and binaries.
case "$file" in
.githooks/*) continue ;;
*.png|*.jpg|*.jpeg|*.gif|*.ico|*.pdf|*.dll|*.exe|*.snk) continue ;;
esac
[ -f "$file" ] || continue
added=$(git diff --cached -U0 -- "$file" | grep '^+' | grep -v '^+++' || true)
[ -z "$added" ] && continue
# The retired hardcoded admin password. Applies everywhere, no exemptions.
echo "$added" | grep -Eq 'qw123321' && report "$file: hardcoded admin password 'qw123321'"
if ! declared_config "$file"; then
# The deployment's SQL Server host — outside the declared config files it is a leak.
echo "$added" | grep -Eq '87\.107\.152\.16' && report "$file: SQL Server host 87.107.152.16 outside the declared config files"
# A real (non-placeholder) connection-string password in a committed appsettings file.
case "$file" in
*appsettings*.json)
echo "$added" \
| grep -Ei 'Password=[^;"'"'"' ]+' \
| grep -viq "Password=${PLACEHOLDER}" \
&& report "$file: connection-string password must be '${PLACEHOLDER}' (see DEPLOY.md for where real values live)"
;;
esac
fi
# Private keys and common cloud tokens, anywhere.
echo "$added" | grep -Eq -- '-----BEGIN (RSA|EC|OPENSSH|PRIVATE) .*PRIVATE KEY-----' && report "$file: private key material"
echo "$added" | grep -Eq 'AKIA[0-9A-Z]{16}' && report "$file: AWS access key id"
done <<< "$staged"
if [ "$violations" -gt 0 ]; then
echo ""
echo "Commit blocked: $violations potential secret(s) staged. Real values belong in one of the declared"
echo "config files (see the 'declared_config' list in this hook, and DEPLOY.md); everything else commits"
echo "only the '${PLACEHOLDER}' placeholder."
echo "To override a false positive: git commit --no-verify"
exit 1
fi
exit 0
+2 -6
View File
@@ -8,7 +8,7 @@ side of the stack lives in that project's own `CLAUDE.md`.
> Working in `server/`? Read [server/CLAUDE.md](server/CLAUDE.md). > Working in `server/`? Read [server/CLAUDE.md](server/CLAUDE.md).
> You almost never need both. A frontend change does not touch server files, and vice-versa. > You almost never need both. A frontend change does not touch server files, and vice-versa.
> Last verified: 2026-08-02 against commit `e2db973`. > Last verified: 2026-08-02 against commit `51e86a1`.
--- ---
@@ -55,7 +55,6 @@ package, or solution — each project is built, linted, and run on its own.
| [`archive/`](archive/README.md) | The executed build-chain (was `dev/`) and the cleanup plan that produced `docs/`. **History, not instruction** — nothing to build from it | Markdown | [archive/README.md](archive/README.md) | | [`archive/`](archive/README.md) | The executed build-chain (was `dev/`) and the cleanup plan that produced `docs/`. **History, not instruction** — nothing to build from it | Markdown | [archive/README.md](archive/README.md) |
| [`telegram-otp-bot/`](telegram-otp-bot/) | OTP relay (standalone, the pre-launch demo rail) | Node 18+, zero deps | [telegram-otp-bot/README.md](telegram-otp-bot/README.md) | | [`telegram-otp-bot/`](telegram-otp-bot/) | OTP relay (standalone, the pre-launch demo rail) | Node 18+, zero deps | [telegram-otp-bot/README.md](telegram-otp-bot/README.md) |
| [`deploy/`](deploy/) | Reverse-proxy config | Caddyfile | [DEPLOY.md](DEPLOY.md) | | [`deploy/`](deploy/) | Reverse-proxy config | Caddyfile | [DEPLOY.md](DEPLOY.md) |
| [`.githooks/`](.githooks/README.md) | Repo-managed git hooks (the pre-commit secret scan) | shell | [docs/rules/shared/git-and-gates.md](docs/rules/shared/git-and-gates.md) |
`AGENTS.md` files in this repo are thin pointers to the `CLAUDE.md` in the same folder. **`CLAUDE.md` is the `AGENTS.md` files in this repo are thin pointers to the `CLAUDE.md` in the same folder. **`CLAUDE.md` is the
single source of truth at every level.** single source of truth at every level.**
@@ -82,7 +81,7 @@ are touching.
| --- | --- | --- | | --- | --- | --- |
| **Hard rules** | this file · [client/CLAUDE.md](client/CLAUDE.md) · [server/CLAUDE.md](server/CLAUDE.md) | Constraints whose violation breaks the build, the gate, or a business invariant | | **Hard rules** | this file · [client/CLAUDE.md](client/CLAUDE.md) · [server/CLAUDE.md](server/CLAUDE.md) | Constraints whose violation breaks the build, the gate, or a business invariant |
| **Reference** | [`docs/rules/`](docs/rules/index.md) | The *how* and the *why*, read on demand — 3 shared files, 8 client, 6 server, plus the documentation convention | | **Reference** | [`docs/rules/`](docs/rules/index.md) | The *how* and the *why*, read on demand — 3 shared files, 8 client, 6 server, plus the documentation convention |
| **Procedure** | `.claude/skills/` | Playbooks. The **frontend-designer** skill is the design contract for `client/` UI | | **Procedure** | `.claude/skills/` | Playbooks: **frontend-designer** (the design contract for `client/` UI), **backend-feature** (adding a server feature), **flow-testing** (walking a flow end to end) |
Start at [docs/rules/index.md](docs/rules/index.md) — it maps "working on X" to the one file to open. Start at [docs/rules/index.md](docs/rules/index.md) — it maps "working on X" to the one file to open.
@@ -157,7 +156,4 @@ cd client && npm install && npm run dev # http://localhost:3000
# Backend # Backend
cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj # http://localhost:5002/swagger cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj # http://localhost:5002/swagger
# Once per clone — enable the repo's git hooks
git config core.hooksPath .githooks
``` ```
+2 -2
View File
@@ -35,8 +35,8 @@ machine is now inert and can be deleted. Every value lives in a file in the repo
| Telegram relay config (bot token, chat ids, API key, proxy) | `docker-compose.yml``otp-relay.environment` | | Telegram relay config (bot token, chat ids, API key, proxy) | `docker-compose.yml``otp-relay.environment` |
The placeholder string `SET_VIA_USER_SECRETS_OR_ENV` in the base `appsettings.json` names that removed The placeholder string `SET_VIA_USER_SECRETS_OR_ENV` in the base `appsettings.json` names that removed
store; the *name* is a historical artifact, kept only because it is the sentinel `StartupSecretsGuard` and store; the *name* is a historical artifact, kept only because it is the sentinel `StartupSecretsGuard`
the pre-commit hook both reject. **The mechanism is appsettings files and environment variables** — see rejects. **The mechanism is appsettings files and environment variables** — see
[docs/integration/config-matrix.md](docs/integration/config-matrix.md), which lists every key, its default, [docs/integration/config-matrix.md](docs/integration/config-matrix.md), which lists every key, its default,
and who reads it. and who reads it.
+7 -5
View File
@@ -2,10 +2,12 @@
The entry point. Start here, follow one link, stop reading. The entry point. Start here, follow one link, stop reading.
> **Built by the clarify chain**, phases 06, 2026-07-29 → 2026-08-02. The chain's own plan — inventory, > **Built by the clarify chain**, phases 07, 2026-07-29 → 2026-08-02. The chain's own plan — inventory,
> contradiction log, phase files, and their progress table — is now history, kept at > contradiction log, phase files, and their progress table — is now history, kept at
> [`archive/clarify-chain/`](../archive/clarify-chain/README.md). Phase 7 (skills & guardrails) is still > [`archive/clarify-chain/`](../archive/clarify-chain/README.md). Phase 7 (skills & guardrails) is done:
> open. > three playbooks live in [`.claude/skills/`](../.claude/skills/); the anti-drift convention below is
> enforced by review rather than a git hook (an MVP-stage call — see
> [decisions.md](status/decisions.md)).
--- ---
@@ -55,5 +57,5 @@ Two documents stay outside this tree on purpose:
it would defeat that. it would defeat that.
4. **English throughout**, including in files that describe Persian UI copy. 4. **English throughout**, including in files that describe Persian UI copy.
The full convention is in [docs/rules/documentation.md](rules/documentation.md), to be enforced by a The full convention is in [docs/rules/documentation.md](rules/documentation.md) enforced by review,
pre-commit warning (phase 7). not tooling (an MVP-stage call; see [git-and-gates.md](rules/shared/git-and-gates.md)).
+9 -11
View File
@@ -3,7 +3,7 @@
Every configuration key on both sides of the seam, plus docker and the OTP relay, with where it is set and Every configuration key on both sides of the seam, plus docker and the OTP relay, with where it is set and
who reads it. who reads it.
> Last verified: 2026-07-30 against commit `d3ec723`. Built by **mechanically enumerating** every leaf key > Last verified: 2026-08-02 against commit `51e86a1`. Built by **mechanically enumerating** every leaf key
> in both `appsettings*.json`, every `environment:` entry in `docker-compose.yml`, every assignment in the > in both `appsettings*.json`, every `environment:` entry in `docker-compose.yml`, every assignment in the
> three `client/.env*` files and `telegram-otp-bot/.env.example`, and every `process.env.*` read under > three `client/.env*` files and `telegram-otp-bot/.env.example`, and every `process.env.*` read under
> `client/src/` — then diffing the sets. The gaps that diff found are in > `client/src/` — then diffing the sets. The gaps that diff found are in
@@ -79,7 +79,7 @@ Every one has a working default, so nothing is broken — but none is discoverab
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `OpenTelemetry:Otlp:Endpoint` | `SetupOpenTelemetry` | **OTLP export is not wired at all.** Prometheus `/metrics` still works | Opt-in by design, so no exporter spams an absent collector | | `OpenTelemetry:Otlp:Endpoint` | `SetupOpenTelemetry` | **OTLP export is not wired at all.** Prometheus `/metrics` still works | Opt-in by design, so no exporter spams an absent collector |
| `Search:Backend` | `AddPersistenceServices` | `SqlNurseSearch` | Any value other than `sql`/empty **throws at startup** — Elasticsearch is deferred and fails loudly | | `Search:Backend` | `AddPersistenceServices` | `SqlNurseSearch` | Any value other than `sql`/empty **throws at startup** — Elasticsearch is deferred and fails loudly |
| `Seed:AdminUsername` / `:AdminPassword` / `:AdminEmail` | `SeedDataBase` | no break-glass admin is seeded | The hardcoded `admin`/`qw123321` was removed in refinement-phase-5 and the pre-commit hook blocks its return | | `Seed:AdminUsername` / `:AdminPassword` / `:AdminEmail` | `SeedDataBase` | no break-glass admin is seeded | The hardcoded `admin`/`qw123321` was removed in refinement-phase-5 |
| `Seams:<rail>:Provider` (×11) | `AddCrossCuttingSeams` | **`mock`** | The seam selectors, below | | `Seams:<rail>:Provider` (×11) | `AddCrossCuttingSeams` | **`mock`** | The seam selectors, below |
### The seam selectors ### The seam selectors
@@ -252,13 +252,11 @@ places where the config is not discoverable from the config files.
### The placeholder's name ### The placeholder's name
`SET_VIA_USER_SECRETS_OR_ENV` names a store that no longer exists (contradiction **C-2**). The *behaviour* `SET_VIA_USER_SECRETS_OR_ENV` names a store that no longer exists (contradiction **C-2**). The *behaviour*
is correct — it is a sentinel that `StartupSecretsGuard` and the pre-commit hook both reject — but the name is correct — it is a sentinel that `StartupSecretsGuard` rejects — but the name instructs a reader to use a
instructs a reader to use a removed mechanism. removed mechanism.
It was **not renamed in this phase**, because the string is load-bearing in seven live files: It was **not renamed in this phase**, because the string is load-bearing in several live files:
`appsettings.json` (×6), `StartupSecretsGuard.cs`, `.githooks/pre-commit`, `.githooks/README.md`, `appsettings.json` (×6), `StartupSecretsGuard.cs`, `Baya.Test.Api/StartupSecretsGuardTests.cs` (×2), and
`Baya.Test.Api/StartupSecretsGuardTests.cs` (×2), `docs/rules/shared/git-and-gates.md` and `docs/rules/server/structure.md`. Renaming it is a server-code + test change requiring `dotnet build` and
`docs/rules/server/structure.md`. Renaming it is a server-code + git-hook + test change requiring `dotnet test` to prove the gate still fires — out of scope for a documentation phase. **This section is the
`dotnet build` and `dotnet test` to prove the gate still fires — out of scope for a documentation phase. authoritative statement of the mechanism**; the rename is filed for Phase 4 with that worklist.
**This section is the authoritative statement of the mechanism**; the rename is filed for Phase 4 with that
exact seven-file worklist.
+2 -2
View File
@@ -1,6 +1,6 @@
# Deferred — recorded, not re-decided # Deferred — recorded, not re-decided
> Last verified: 2026-08-02 against commit `cd8144e`. Populated by phase 5 of the > Last verified: 2026-08-02 against commit `51e86a1`. Populated by phase 5 of the
> [documentation clean-up chain](../../archive/clarify-chain/README.md). > [documentation clean-up chain](../../archive/clarify-chain/README.md).
**51 items** carry deferred status in [backlog.md](../status/backlog.md): the 43 in its dedicated **51 items** carry deferred status in [backlog.md](../status/backlog.md): the 43 in its dedicated
@@ -96,7 +96,7 @@ Correct for a single instance / MVP load; each has a concrete, measurable trigge
| Item | Why deferred | Pull-trigger | Size | Decided | | Item | Why deferred | Pull-trigger | Size | Decided |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| [BL-219](../status/backlog.md#minor-115) — rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel | The name is a load-bearing sentinel `StartupSecretsGuard` and the pre-commit hook both check for; renaming touches 7 files for a cosmetic gain | None — cleanup-of-convenience | S | phase 2 (`open-contradictions.md` C-2) | | [BL-219](../status/backlog.md#minor-115) — rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel | The name is a load-bearing sentinel `StartupSecretsGuard` checks for; renaming touches several files for a cosmetic gain | None — cleanup-of-convenience | S | phase 2 (`open-contradictions.md` C-2) |
| [BL-250](../status/backlog.md#deferred-43) — the ESLint unused-vars gate is a repo-wide no-op | Config patches an export path that doesn't carry the rule; fixing it is a dedicated infra task, not a drive-by | A dedicated infra task | S | frontend-phase-13 follow-up | | [BL-250](../status/backlog.md#deferred-43) — the ESLint unused-vars gate is a repo-wide no-op | Config patches an export path that doesn't carry the rule; fixing it is a dedicated infra task, not a drive-by | A dedicated infra task | S | frontend-phase-13 follow-up |
| [BL-253](../status/backlog.md#deferred-43) — payment-webhook confirm path uses two DB commits instead of one transaction | Kept safe today via idempotency + a forward-only guard; a real fix needs `IUnitOfWork` to grow a transaction scope first | `IUnitOfWork` grows a transaction scope | M | backend-phase-10 follow-up | | [BL-253](../status/backlog.md#deferred-43) — payment-webhook confirm path uses two DB commits instead of one transaction | Kept safe today via idempotency + a forward-only guard; a real fix needs `IUnitOfWork` to grow a transaction scope first | `IUnitOfWork` grows a transaction scope | M | backend-phase-10 follow-up |
| [BL-255](../status/backlog.md#deferred-43) — `Bookings`/`Invoices.partner_center_id` have no DB-level FK | Only `nurse_profiles.partner_center_id` got one, per that phase's own Definition of Done | A data-integrity pass on partner-center columns | S | backend-phase 11/15 follow-ups | | [BL-255](../status/backlog.md#deferred-43) — `Bookings`/`Invoices.partner_center_id` have no DB-level FK | Only `nurse_profiles.partner_center_id` got one, per that phase's own Definition of Done | A data-integrity pass on partner-center columns | S | backend-phase 11/15 follow-ups |
+3 -3
View File
@@ -2,7 +2,7 @@
How this repository keeps its own docs from lying. Read before writing or editing any `.md`. How this repository keeps its own docs from lying. Read before writing or editing any `.md`.
> Last verified: 2026-07-30 against commit `d3ec723`. > Last verified: 2026-08-02 against commit `51e86a1`.
--- ---
@@ -43,8 +43,8 @@ regenerate — never hand-edit the HTML. If you add or rename a `.md`, update th
## 2. What to update when X changes ## 2. What to update when X changes
This is the anti-drift contract. Each row is enforced by review, and (from phase 7) warned about by the This is the anti-drift contract. Each row is enforced by review — there is no pre-commit tooling behind
pre-commit hook. it (a deliberate MVP-stage call; see [git-and-gates.md](shared/git-and-gates.md)).
| When you change… | Update, in the same change | | When you change… | Update, in the same change |
| --- | --- | | --- | --- |
+3 -3
View File
@@ -2,7 +2,7 @@
What must never be broken, and nothing else. What must never be broken, and nothing else.
> Last verified: 2026-07-30 against commit `d3ec723`. > Last verified: 2026-08-02 against commit `51e86a1`.
--- ---
@@ -14,7 +14,7 @@ Three tiers, and a rule lives in exactly one of them.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| **Hard rules** | [root CLAUDE.md](../../CLAUDE.md) · [client/CLAUDE.md](../../client/CLAUDE.md) · [server/CLAUDE.md](../../server/CLAUDE.md) | Constraints whose violation breaks the build, the gate, or a business invariant. Imperative, no explanation. | ≤250 lines each | | **Hard rules** | [root CLAUDE.md](../../CLAUDE.md) · [client/CLAUDE.md](../../client/CLAUDE.md) · [server/CLAUDE.md](../../server/CLAUDE.md) | Constraints whose violation breaks the build, the gate, or a business invariant. Imperative, no explanation. | ≤250 lines each |
| **Reference** | `docs/rules/{shared,client,server}/*.md` — here | The *how* and the *why*. Read on demand when you are working in that area. | ≤400 lines per file | | **Reference** | `docs/rules/{shared,client,server}/*.md` — here | The *how* and the *why*. Read on demand when you are working in that area. | ≤400 lines per file |
| **Procedure** | `.claude/skills/` | Step-by-step playbooks for recurring tasks. The [frontend-designer](../../.claude/skills/frontend-designer/SKILL.md) skill is the design playbook. | — | | **Procedure** | `.claude/skills/` | Step-by-step playbooks for recurring tasks: [frontend-designer](../../.claude/skills/frontend-designer/SKILL.md) (design), [backend-feature](../../.claude/skills/backend-feature/SKILL.md) (adding a server feature), [flow-testing](../../.claude/skills/flow-testing/SKILL.md) (walking a flow end to end). | — |
The test: **a rule that only matters once you are already editing theme code is reference.** A rule like The test: **a rule that only matters once you are already editing theme code is reference.** A rule like
"never change `Seams:FieldEncryption:Key`" is hard — it belongs inline where nobody can miss it. "never change `Seams:FieldEncryption:Key`" is hard — it belongs inline where nobody can miss it.
@@ -31,7 +31,7 @@ touching. Not both trees, not every file.
| File | Covers | | File | Covers |
| --- | --- | | --- | --- |
| [shared/naming.md](shared/naming.md) | `Baya*` vs `balinyaar-client`, the `@/*` alias, file and directory conventions | | [shared/naming.md](shared/naming.md) | `Baya*` vs `balinyaar-client`, the `@/*` alias, file and directory conventions |
| [shared/git-and-gates.md](shared/git-and-gates.md) | Branches, commits, the pre-commit secret scan, what "done" means per project | | [shared/git-and-gates.md](shared/git-and-gates.md) | Branches, commits, what "done" means per project |
| [shared/code-quality.md](shared/code-quality.md) | No dead code, comment the *why*, no starter scaffolding, the seam rule for mocks | | [shared/code-quality.md](shared/code-quality.md) | No dead code, comment the *why*, no starter scaffolding, the seam rule for mocks |
### Client — `client/` ### Client — `client/`
+3 -3
View File
@@ -2,7 +2,7 @@
Auth, JWE, sessions, field encryption, tenancy, and the two-stage clinical disclosure rule. Auth, JWE, sessions, field encryption, tenancy, and the two-stage clinical disclosure rule.
> Last verified: 2026-07-30 against commit `d3ec723`. > Last verified: 2026-08-02 against commit `51e86a1`.
--- ---
@@ -101,8 +101,8 @@ The full vocabulary is in `Domain/Entities/User/RoleNames`.
- **`SeedDataBase` always seeds the roles**, and seeds a **bootstrap admin only when - **`SeedDataBase` always seeds the roles**, and seeds a **bootstrap admin only when
`Seed:AdminUsername`/`Seed:AdminPassword` are configured** — break-glass only. There is no committed `Seed:AdminUsername`/`Seed:AdminPassword` are configured** — break-glass only. There is no committed
`admin`/`qw123321` any more (the pre-commit hook rejects that string outright). Day-to-day admins come from `admin`/`qw123321` any more. Day-to-day admins come from the phone-OTP demo seeds or are provisioned
the phone-OTP demo seeds or are provisioned out-of-band. out-of-band.
- **`customer` and `nurse` are self-selectable** via `POST me/select_role` — audited (`granted_by`, - **`customer` and `nurse` are self-selectable** via `POST me/select_role` — audited (`granted_by`,
`granted_at`), idempotent, and **both can be held** by one user (a dual session moves freely between the `granted_at`), idempotent, and **both can be held** by one user (a dual session moves freely between the
family and nurse apps). family and nurse apps).
+9 -34
View File
@@ -2,7 +2,7 @@
What must pass before work is done, and what the repo refuses to let you commit. What must pass before work is done, and what the repo refuses to let you commit.
> Last verified: 2026-07-30 against commit `d3ec723`. > Last verified: 2026-08-02 against commit `51e86a1`.
--- ---
@@ -63,40 +63,15 @@ A change that doesn't pass its own gate is **not done**, regardless of how compl
--- ---
## 3. The pre-commit secret scan ## 3. No pre-commit secret scan (for now)
Repo-managed hooks live in `.githooks/` (in version control, unlike `.git/hooks`). **Enable them once per There is no git hook enforcing anything in this repo — `.githooks/` was removed in phase 7 as an
clone:** MVP-stage call: this is a pre-launch demo project and the mechanical backstop wasn't worth the overhead
yet. The underlying rule is unchanged — **never commit a real secret** — it's just unenforced by tooling.
```bash Root [CLAUDE.md](../../../CLAUDE.md) §6 already documents the repo's actual trade: config lives in
git config core.hooksPath .githooks committed files, including live credentials, until real users exist (see
``` [DEPLOY.md](../../../DEPLOY.md) "Going to Production" for the rotation step that unblocks that). Revisit
adding a hook — or a CI scanner (gitleaks, trufflehog) — if that trade changes before this one does.
`pre-commit` is a fast, dependency-free backstop against a credential leaking into a file that shouldn't
hold one. It scans **only staged additions**, so it is quick. It rejects a commit that stages:
- the retired hardcoded admin password `qw123321`, anywhere;
- private-key material or an AWS access-key id, anywhere;
- the deployment's SQL Server host `87.107.152.16` **outside the declared config files**;
- a **real** connection-string password in any `appsettings*.json` **outside the declared config files**
(elsewhere only the `SET_VIA_USER_SECRETS_OR_ENV` placeholder is allowed).
### The declared-config allow-list
The pre-launch demo deployment configures itself from committed files rather than a secret store (see
[`DEPLOY.md`](../../../DEPLOY.md)), so a short allow-list is exempt from the last two checks:
`appsettings.Development.json` · `docker-compose.yml` · `telegram-otp-bot/.env.example` · `DEPLOY.md`
It is maintained in the `declared_config` function in the hook, and it is **the honest record of where
this repo's secrets are**. **Shrink it, never grow it.** Once real users exist, those values must be
rotated and moved out of git.
### Limits
This is the local first line of defence, **not** a replacement for a full scanner (gitleaks, trufflehog)
in CI. Bypass a false positive with `git commit --no-verify` — sparingly, and only when you are certain
the flagged line is not a secret.
--- ---
+2 -2
View File
@@ -1,6 +1,6 @@
# Backlog — every open item, triaged # Backlog — every open item, triaged
> Last verified: 2026-08-02 against commit `b876490`. > Last verified: 2026-08-02 against commit `51e86a1`.
Reconciled from five ledgers — [hardening/issues.md](../../archive/post-phase/hardening/issues.md) (18 items), Reconciled from five ledgers — [hardening/issues.md](../../archive/post-phase/hardening/issues.md) (18 items),
[for-backend.md](../../archive/build-chain/working-context/frontend/requests/for-backend.md) (67 REQs), 53 [for-backend.md](../../archive/build-chain/working-context/frontend/requests/for-backend.md) (67 REQs), 53
@@ -251,7 +251,7 @@ log is in [decisions.md](decisions.md). The business-area overlay is in [impleme
| BL-216 | contract | No typed address/variant snapshot objects — `BookingDetailDto` still carries opaque JSON strings, forcing the client's defensive multi-key parse in three separate places. | REQ-045, ui-phase-6 follow-up 2, ui-phase-7 follow-up 2 | open | booking-lifecycle-evv, checkout-and-payment | | BL-216 | contract | No typed address/variant snapshot objects — `BookingDetailDto` still carries opaque JSON strings, forcing the client's defensive multi-key parse in three separate places. | REQ-045, ui-phase-6 follow-up 2, ui-phase-7 follow-up 2 | open | booking-lifecycle-evv, checkout-and-payment |
| BL-217 | client | Two admin-only multi-field dialogs (`GrantRoleDialog` in `admin/roles`, `PreviewBatchDialog` in `admin/payouts`) still hold raw `useState` instead of react-hook-form — the only genuine residue of the app-wide form migration. | iteration-2 #7 | open | admin-backoffice | | BL-217 | client | Two admin-only multi-field dialogs (`GrantRoleDialog` in `admin/roles`, `PreviewBatchDialog` in `admin/payouts`) still hold raw `useState` instead of react-hook-form — the only genuine residue of the app-wide form migration. | iteration-2 #7 | open | admin-backoffice |
| BL-218 | client | `H-01` residue: the auth-gate root-cause fix landed, but `client/middleware.ts` was never migrated to Next 16's `src/proxy.ts` as the original fix prescribed, and `outputFileTracingRoot` was never added alongside `turbopack.root` — a production build may still warn from the stray root-level lockfile. | H-01 | open | auth-login-otp | | BL-218 | client | `H-01` residue: the auth-gate root-cause fix landed, but `client/middleware.ts` was never migrated to Next 16's `src/proxy.ts` as the original fix prescribed, and `outputFileTracingRoot` was never added alongside `turbopack.root` — a production build may still warn from the stray root-level lockfile. | H-01 | open | auth-login-otp |
| BL-219 | docs | Rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel now that `user-secrets` is confirmedly removed — filed by phase 2 with an exact 7-file worklist (`appsettings.*.json`, `StartupSecretsGuard.PlaceholderMarkers`, `.githooks/pre-commit`, `.githooks/README.md`, `StartupSecretsGuardTests.cs`, `docs/rules/shared/git-and-gates.md`, `docs/rules/server/structure.md`). Optional cleanup, not urgent — the name is a load-bearing sentinel and the mechanism is already documented correctly elsewhere. | C-2 (open-contradictions.md) | deferred (trigger: none — cleanup-of-convenience) | — | | BL-219 | docs | Rename the `SET_VIA_USER_SECRETS_OR_ENV` placeholder sentinel now that `user-secrets` is confirmedly removed — filed by phase 2 with a worklist (`appsettings.*.json`, `StartupSecretsGuard.PlaceholderMarkers`, `StartupSecretsGuardTests.cs`, `docs/rules/server/structure.md`); the two `.githooks/*` files in the original worklist no longer exist (phase 7 dropped the pre-commit hook — see [decisions.md](decisions.md)). Optional cleanup, not urgent — the name is a load-bearing sentinel and the mechanism is already documented correctly elsewhere. | C-2 (open-contradictions.md) | deferred (trigger: none — cleanup-of-convenience) | — |
## Deferred (43) ## Deferred (43)
+13 -3
View File
@@ -1,6 +1,6 @@
# Decisions — the distilled engineering decision log # Decisions — the distilled engineering decision log
> Last verified: 2026-08-02 against commit `b876490`. > Last verified: 2026-08-02 against commit `51e86a1`.
Non-obvious decisions with a reason, extracted from `dev/`'s ~3MB of build history so they survive Non-obvious decisions with a reason, extracted from `dev/`'s ~3MB of build history so they survive
`dev/`'s move to `archive/` in phase 6. **`product/` wins for business rules** — this file is for `dev/`'s move to `archive/` in phase 6. **`product/` wins for business rules** — this file is for
@@ -91,10 +91,20 @@ Decided ui-phase-13, chosen specifically so `/` stays a stable, shareable, index
## Engineering decisions ## Engineering decisions
**Phase 2 → Phase 4 handoff, "the rename is filed."** The placeholder secret sentinel **Phase 2 → Phase 4 handoff, "the rename is filed."** The placeholder secret sentinel
`SET_VIA_USER_SECRETS_OR_ENV` keeps its name — renaming it needs a server-code + git-hook + test change that `SET_VIA_USER_SECRETS_OR_ENV` keeps its name — renaming it needs a server-code + test change that is out of
is out of a documentation phase's scope, and the string is load-bearing across 7 live files. Filed as a documentation phase's scope, and the string is load-bearing across several live files. Filed as
[backlog.md](backlog.md) BL-219, deferred (cleanup-of-convenience, no urgency). [backlog.md](backlog.md) BL-219, deferred (cleanup-of-convenience, no urgency).
**Phase 7 dropped the pre-commit secret-scan hook — MVP stage, no need for it yet.** `.githooks/pre-commit`
(the `qw123321`/private-key/AWS-key/SQL-host/connection-string scan) and `.githooks/README.md` were deleted
outright, along with every doc reference to them (root `CLAUDE.md`'s repo-layout table and quick start,
[git-and-gates.md](../rules/shared/git-and-gates.md), [documentation.md](../rules/documentation.md),
[identity.md](../rules/server/identity.md), [config-matrix.md](../integration/config-matrix.md),
[DEPLOY.md](../../DEPLOY.md)). The underlying trade this repo already made — committed live credentials,
config in files not a secret store (root [CLAUDE.md](../../CLAUDE.md) §6) — is unchanged; this only removes
the local mechanical backstop against a *new* leak. Revisit before onboarding real users, alongside the
credential rotation already required by that trade.
**Hardening ledger re-verification (C-10) confirms the ledger was right to distrust its own checkboxes.** **Hardening ledger re-verification (C-10) confirms the ledger was right to distrust its own checkboxes.**
All 18 hardening items were re-traced against `b876490` rather than trusted as-filed: 3 were already fixed All 18 hardening items were re-traced against `b876490` rather than trusted as-filed: 3 were already fixed
(client-only, landed in the "manual improvement" commits well before this doc chain started), 4 are (client-only, landed in the "manual improvement" commits well before this doc chain started), 4 are