# Open contradictions

> Opened 2026-07-29 by [phase 0](phase-0-inventory-and-scaffold.md), against commit `c99e3f4`.
> **Phase 0 only finds these. It resolves none of them.**
> Last updated 2026-07-30 by [phase 2](phase-2-integration.md), against commit `d3ec723`:
> **C-2, C-6, C-7, C-8 and C-9 resolved**, C-3's contract-layer half resolved, **C-15 and C-16 added**.

Each row is *claim A* vs *claim B* with a location for both, and the phase that owns the resolution.
When a phase resolves one, it moves the row to **Resolved** at the bottom with the decision, and — per
the chain's non-negotiable #5 — records the decision in `docs/status/decisions.md`.

Three of the five contradictions the plan seeded turned out to be **narrower than stated**. Those are
written up as they actually are, with the evidence, in [§ Corrections to the seeded list](#corrections-to-the-seeded-list).

---

## Open

| # | Claim A | Claim B | Owner | Status |
| --- | --- | --- | --- | --- |
| C-1 | **Set the crypto keys with `dotnet user-secrets`** — [manual-testing-plan.md:22](../../dev/post-phase/manual-testing-plan.md), [:233](../../dev/post-phase/manual-testing-plan.md) | **`user-secrets` is not used; `<UserSecretsId>` was removed so the store is not read** — [CLAUDE.md:90](../../CLAUDE.md), [DEPLOY.md:20](../../DEPLOY.md), [server/CLAUDE.md:70](../../server/CLAUDE.md), and `Baya.Web.Api.csproj` (no `UserSecretsId` element) | 3 | open |
| C-3 | **The server listens on `https://localhost:5002`** — [CLAUDE.md:62](../../CLAUDE.md), [:135](../../CLAUDE.md), [api-conventions.md:6](../../dev/contracts/conventions/api-conventions.md), [RUNBOOK.md:7](../../dev/post-phase/refinement/RUNBOOK.md), [:84](../../dev/post-phase/refinement/RUNBOOK.md), [:112](../../dev/post-phase/refinement/RUNBOOK.md), + 12 more | **It listens on `http://localhost:5002`** — `launchSettings.json:25` (`"applicationUrl": "http://localhost:5002"`), and `client/.env.development:20` (`NEXT_PUBLIC_API_URL = http://localhost:5002`) | 2 + 3 | **partly resolved by phases 1 + 2** — the three rule-file occurrences now say `http`; phase 2's [api-contract.md](../integration/api-contract.md) states `http` and banner-marks `api-conventions.md` as history. **Phase 2 also found a new occurrence the survey missed: `client/.env.sample:20`**, the file a fresh clone copies. `RUNBOOK.md` + the remaining occurrences are phase 3 |
| C-4 | **RUNBOOK's `dotnet dev-certs https --trust` step is required**, because the browser would otherwise reject the API — [RUNBOOK.md:26](../../dev/post-phase/refinement/RUNBOOK.md) | **The API is plain HTTP locally** (C-3's B side), so there is no certificate to trust | 3 | open — likely a dead step |
| C-5 | **Bring-up starts a local SQL Server in Docker on `localhost:1433`** — [RUNBOOK.md:18](../../dev/post-phase/refinement/RUNBOOK.md), [:35–48](../../dev/post-phase/refinement/RUNBOOK.md), [:63](../../dev/post-phase/refinement/RUNBOOK.md) | **The committed dev config points at a remote SQL Server** — `appsettings.Development.json` (`Server=87.107.152.16,1433`), and [manual-testing-plan.md:20](../../dev/post-phase/manual-testing-plan.md) calls the remote one "currently" the target | 3 | open — the two bring-up paths give *different worlds*: the remote DB is already seeded, a fresh local one is not |
| C-10 | **18 hardening items are open** — [issues.md](../../dev/post-phase/hardening/issues.md), 18 of 18 checkboxes unticked, last touched 2026-07-17 | **Fourteen UI phases, two manual-testing iterations, a Telegram integration and a deploy commit ran afterwards** (`12ce7fa` → `96b57eb`, 07-20 → 07-28) without ticking any box | 4 | open — Phase 4 must re-verify each item against code, not trust the checkbox |
| C-12 | **The skill is current** | **It is exactly one iteration behind the code.** SKILL.md's last commit is `baa3cc6` ("manual improvement 1"); `client/CLAUDE.md`'s is `e6a8f93` ("manual improvement 2"), which changed **44 files, +3419/−2449** under `client/src`. Anything iteration 2 changed is absent from the skill | 7 | **partly resolved by phase 1** — the design-language half is corrected (see R-2); phase 7 still owns the skill's own workflow/procedure content |
| C-13 | **`dev/` is "the plan for building Balinyaar"**, written in the imperative — [dev/README.md:3](../../dev/README.md) | **It is a record of work already done.** `dev/phases/` last touched 2026-06-28; the code it describes shipped weeks ago | 6 | open — resolved by the archive banner, not by editing 199 files |
| C-14 | **`temp/swagger.json` is a stale committed duplicate** — [_plan/README.md](README.md) diagnosis table | **It is not committed at all** — `.gitignore:1` ignores `temp`, and `git ls-files temp/` is empty. It is local clutter, not repo content | 0 | see [§ Corrections](#corrections-to-the-seeded-list) |
| C-15 | **`ui-phase-11-report.md:235` records "REQ-061…064 appended"** to the frontend request ledger | **REQ-061 was never appended.** The ledger goes 060 → 062; **ten live client files** cite REQ-061 for the admin user directory (`admin_users/search`, `admin_users/lookup`), which are 2 of the 24 phantom endpoints | 4 | **found by phase 2** — phase 4 must *file* it, not assume it is tracked |
| C-16 | **Five REQs' filed status** — REQ-050, REQ-063, REQ-066, REQ-067 (open, as filed) and the reasons `admin/constants.ts` gives for the admin mock | **All five are wrong or narrower than filed**, checked against the live swagger rather than against another document. `variantLabel` already ships; `tickets/close`+`reopen` already ship; `search/nurses` and `nurses/{id}/profile` are already anonymous; REQ-029/030 already shipped | 4 | **found by phase 2** — the table is in [phase-2's handoff § B](phase-2-integration.md) |

---

## OpenAPI drift

The fresh snapshot is at [`docs/integration/openapi/swagger.v1.json`](../integration/openapi/swagger.v1.json).
See its [README](../integration/openapi/README.md) for provenance. Measured against the 2026-07-13 snapshot
at `dev/contracts/openapi/swagger.v1.json`:

| | 2026-07-13 | 2026-07-29 |
| --- | --- | --- |
| paths | 177 | **178** |
| operations | 185 | **186** |
| component schemas | 339 | **339** |

**Added (1)**

- `POST /api/v1/webhooks/payouts/{provider}`

**Removed (0)** · **Methods changed on an existing path (0)** · **Schemas added/removed (0)**

**Schema definitions changed (1)**

- `GeneratePayoutBatchCommand` — gained `systemInitiated: boolean` (**C-7**, resolved)

> **This was Phase 2's worklist for the machine contract, and it was two lines long.** The 16-day gap
> between the snapshot and the code produced almost no wire-level drift. Phase 2 therefore spent its
> budget on the **hand-written** contract docs instead. See
> [§ Corrections](#corrections-to-the-seeded-list) C-14b.

### What phase 2 found once it looked

Re-scoping was right, but the conclusion "the hand-written docs are where the drift is" turned out to be
only half true, and the more useful axis was a third one nobody had checked:

| Axis | Result |
| --- | --- |
| Old contract docs → live swagger, at **route** level | **Clean.** Every route named across the 17 files exists in the live API (the two apparent misses are prose counter-examples: "X, *not* Y") |
| Old contract docs → live swagger, at **convention** level | **Drifted.** Body casing ("typically snake_case" vs. proven camelCase), the server's local URL scheme, the envelope's field count (5 vs. 6 — `code` was added), and the enum vocabularies |
| **Client's real API clients → live swagger** | **24 phantom routes** — calls to endpoints the server does not expose, 2 of them reachable today. This axis had never been checked and is where the real gap was |

The third row is the phase's main finding, and it is in
[phase-2's handoff § A](phase-2-integration.md).

---

## Corrections to the seeded list

The plan seeded five contradictions. Two hold as written; three are narrower than stated. Recording
this here so later phases do not budget for work that is already done.

**Holds — `user-secrets`.** The plan says 18 files still instruct you to use it. 32 mentions across 18
files is right, but the split matters: **6 files document the removal correctly** (root `CLAUDE.md`,
`DEPLOY.md`, `server/CLAUDE.md`, `server/CONVENTIONS.md`, and `RUNBOOK.md` twice — including a
troubleshooting row that names the error you now get). **12 are historical** — refinement prompts,
handoffs and reports written before the removal, all headed for `archive/`. Only **one live document
still gives the wrong instruction**: [manual-testing-plan.md](../../dev/post-phase/manual-testing-plan.md)
(last commit `bd06ef0`, the day *before* the removal in `5885280`), plus the archived
[telegram-otp-bot/INTEGRATION-PROMPT.md](../../telegram-otp-bot/INTEGRATION-PROMPT.md).
→ **The plan's statement that "both `manual-testing-plan.md` and `RUNBOOK.md` are stale on secrets
handling" is half wrong. `RUNBOOK.md` is current.** Phase 3 fixes one file, not two.

**Holds — hardening.** C-10 above. 18 of 18 unticked, four later chains, nothing reconciled.

**Narrower — contract drift (C-14b).** "`dev/contracts/` frozen 07-13 vs `server/src` through 07-28"
is true by date but nearly empty in substance at the OpenAPI layer: 1 endpoint, 1 schema. The drift
that matters, if any, is in the 17 hand-written domain markdown files. Phase 2 should re-scope
accordingly.

**Narrower — the design-language skill.** The plan says the skill "predates that overhaul" (mobile-scoped
shell, bottom nav, react-hook-form, new icon set). It does not: SKILL.md was rewritten at `baa3cc6`
during iteration 1 and already covers `AppFrame` (4×), `BottomBar`, `APP_FRAME_MAX_WIDTH` (2×) and the
Lucide-only icon registry with `@mui/icons-material` removed. `react-hook-form` is absent from the skill,
but it is an engineering rule that `client/CLAUDE.md` §Forms owns — not a design-language gap. The real
gap is C-12: the skill stopped at iteration 1 while the code went on to iteration 2.

**Wrong — `temp/swagger.json`.** C-14 above. It is `.gitignore`d and untracked, so it was never part of
the repo's documentation surface. Phase 0's brief says to delete it; that deletes a local file only.

---

## Resolved

Each row records the decision. **These decisions still need folding into `docs/status/decisions.md`
when phase 4 creates it** — that file does not exist yet, so this table is their only home.

| # | Was | Decision | By |
| --- | --- | --- | --- |
| **C-11** | The frontend-designer skill and `client/CLAUDE.md` both claimed the design language, with no stated precedence | **Precedence is now stated in both directions.** The skill is the **design** contract (brand, tone, logo construction, the visual decisions, and the workflow for turning a design into a screen); [`docs/rules/client/`](../rules/client/) is the **engineering** contract and **wins on every overlap** — tokens, typography, the component library, shells, icons. SKILL.md's header carries the precedence statement plus a table pointing at the four files it defers to, and the overlapping detail was removed from the skill rather than duplicated. `client/CLAUDE.md` no longer restates design content at all. | phase 1 |
| **C-2** | The placeholder `SET_VIA_USER_SECRETS_OR_ENV` names a store that was removed, so its *name* instructs a reader to use a removed mechanism | **The name stays; the mechanism is documented authoritatively elsewhere.** The string is a load-bearing sentinel in **seven live files** — `appsettings.json` (×6), `StartupSecretsGuard.PlaceholderMarkers`, `.githooks/pre-commit`, `.githooks/README.md`, `Baya.Test.Api/StartupSecretsGuardTests.cs` (×2), `docs/rules/shared/git-and-gates.md`, `docs/rules/server/structure.md`. Renaming it is a server-code + git-hook + test change needing `dotnet build`/`dotnet test` to prove the gate still fires — outside a documentation phase's scope. [config-matrix.md](../integration/config-matrix.md#the-placeholders-name) is now the single authoritative statement that **config lives in appsettings files and environment variables and `user-secrets` is not read**, and `DEPLOY.md` says the same. **The rename is filed for phase 4 with that exact seven-file worklist.** | phase 2 |
| **C-6** | `POST /api/v1/webhooks/payouts/{provider}` was absent from the frozen contract | **It exists and is now documented** — the payout transferor's reconciliation callback, anonymous, `webhook` rate-limit policy, in [domains/payouts.md](../integration/domains/payouts.md). | phase 2 |
| **C-7** | `GeneratePayoutBatchCommand` changed shape between the two snapshots, unexplained | **It gained `systemInitiated: boolean`** alongside `periodStart`/`periodEnd`. That is the refinement-phase-7 scheduler flag distinguishing a job-generated draft batch from an admin-generated one — which is what keeps the "generation may be automatic, *processing* is always an explicit admin action" rule auditable. Recorded in [domains/payouts.md](../integration/domains/payouts.md). | phase 2 |
| **C-8** | Two live contract files for one domain: a headerless 851-byte `messaging.md` silently amending the 10.6 K `messaging-notifications-admin.md` | **Merged and split three ways along the client's real domain boundaries** — tickets → [domains/tickets.md](../integration/domains/tickets.md), the notification feed → [domains/notifications.md](../integration/domains/notifications.md), config/holidays/audit/alerts → [domains/admin.md](../integration/domains/admin.md). Every REQ-028 amendment is folded in as current fact, not as a change log. Both old files carry a merged-into banner so neither reads as live; phase 6 archives them. | phase 2 |
| **C-9** | The OpenAPI folder claimed documents `v1` **and** `v1.1`; only `v1` was ever committed | **Both documents genuinely are registered** (`AddSwagger("v1","v1.1")` in `Program.cs`), so `/swagger/v1.1/swagger.json` is served — **and it contains zero paths.** `ApiVersionDocumentProcessor` drops every path whose URL lacks the document's version segment, and all **55 controllers are `[ApiVersion("1")]`** on the route template `api/v{version:apiVersion}/…`. The old claim was literally true and substantively empty. `v1` is the contract; only `v1` is worth committing. **Resolved by reading the code — no server boot needed, so the `UNVERIFIED` marker is retired.** | phase 2 |

### Corrections landed by phase 1 that were not on the seeded list

Six rule statements were **false against the code**, not merely duplicated. Each was rewritten against
reality rather than carried over. They are recorded here because a future reader of `dev/`'s history will
find the old wording and needs to know it was checked.

| R- | The stale claim | Reality | Where it was |
| --- | --- | --- | --- |
| R-1 | "Use `ColorSchemeScript` from `@/theme`" | **No such export exists.** The no-flash boot is CSS-only; `theme/index.ts` exports only `ThemeProvider`, `getDirection`, `APP_THEME_*` | `client/CLAUDE.md:857` |
| R-2 | A `Storage.prototype.setItem` intercept writes the theme cookie (3 occurrences) | **There is no such patch.** `ColorSchemeCookieSync` — a `useColorScheme()` effect in `ThemeProvider.tsx` — writes it via `setClientCookie` | `client/CLAUDE.md:698, 711, 741` |
| R-3 | `AppImage` is part of the component library | **No such component** under `src/components/` | `SKILL.md:154` |
| R-4 | `CONTENT_MAX_WIDTH = 800` | It is **480**, mirroring `APP_FRAME_MAX_WIDTH` — iteration 1 changed it and the skill was never updated (this is C-12's concrete shape) | `SKILL.md:159` |
| R-5 | `AppFrame`'s header/`<main>`/footer are flex siblings, so a top bar is `position: static` | Iteration 2 pinned both bars **`position: absolute`** over a single scrolling `<main>`, which reserves their height as padding and publishes `--bal-chrome-top`/`-bottom` | `SKILL.md:181–186` |
| R-6 | `DarkModeButton.tsx` is the `common` namespace's consumer | **Component deleted**; `ThemeModeSetting` (a three-way segmented control in `SettingsPanel`) replaced it | `client/CLAUDE.md:507` |

Two counts were also wrong and are corrected in the new docs: `client/CLAUDE.md:925` said "14 domains are
now REAL" and then listed **15** (verified against `services/*/constants.ts`: 15 real, 7 mocked), and
`:61` described `npm run check` as "type then lint", omitting **`lint:copy`**.
