Skip to content

feat: first-class clients (clients table + projects.client_id) - #402

Merged
thewrz merged 12 commits into
mainfrom
feat/issue-391
Jul 7, 2026
Merged

feat: first-class clients (clients table + projects.client_id)#402
thewrz merged 12 commits into
mainfrom
feat/issue-391

Conversation

@thewrz

@thewrz thewrz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Why

The operating hierarchy is Client → Project → Package → Issuance (→ Revision), but SpecR had no Client entity — "client" existed only as a library tier (libraries.tier='client') and could only be approximated by inspecting a project's client-tier source libraries. #209/ADR-025 deferred firm/client tables as YAGNI ("firm/client as future links in the resolution chain"). This PR is that future link, now needed: a client (e.g. a hyperscale-datacenter owner) runs many campuses/projects, and grouping, defaults, and custody all want a real edge from a project to the organization that owns it.

What

A first-class clients entity, exposed over REST and MCP, plus project↔client association. Additive and back-compat.

  • Migration 040 (reversible): clients (id, name UNIQUE non-empty, library_id → libraries ON DELETE SET NULL, timestamps) + projects.client_id → clients ON DELETE RESTRICT + index.
  • DB: new src/db/queries/clients.ts (createClient / listClients / getClient — the latter joins its active projects as full ProjectSummary rows via a single LATERAL json_agg). ProjectSummary gains clientId/clientName; updateProject gains a validated clientId set/clear path (unknown client → ClientNotFoundError → 422).
  • REST: POST/GET /clients, GET /clients/{id} (client + its projects); PATCH /projects/{id} accepts clientId (uuid | null — associate/disassociate, echoes it back); dup name → 409, unknown client → 422, unknown libraryId → 422. openapi.yaml updated in lockstep (both contract-gate directions pass).
  • MCP: list_clients / get_client (read), create_client (write); update_project tool gains clientId. Tools never throw.
  • ADR-054 records the four locked decisions (link-not-merge; RESTRICT/SET-NULL delete semantics; firm forward-compat as prose only; clients are organizations, not actors) and supersedes the ADR-025 deferral.

Delete semantics are governed entirely by the FKs — there is deliberately no DELETE /clients endpoint (a client with projects can't be hard-deleted; disassociate first). Non-goals (follow-ups): a firms tier; auth/tenancy (#43); migrating existing scoped profiles onto the client scope.

Testing

  • Unit tests pass (clients query module + updateProject client path; full unit suite: 1428 pass, only the pre-existing start-specr-shell failure inherited from base commit 19343eb)
  • Integration tests pass (full suite: 977 pass / 126 skipped / 0 fail — DB, REST, and MCP client tests + both contract gates green)
  • Lint clean (pnpm lint: eslint + tsc + prettier)
  • Migration up/down/up round-trips cleanly
  • CI green

🤖 Co-authored by Claude Fable 5. Closes #391.

Summary by CodeRabbit

  • New Features

    • Added first-class client management, including listing, viewing, and creating clients.
    • Projects can now be linked to or unlinked from a client.
    • Client details now include associated active projects.
  • Bug Fixes

    • Improved validation so invalid or unknown client links return clear app errors instead of failing unexpectedly.
    • Prevents deleting clients that still have linked projects.
  • Documentation

    • Added design and implementation notes for the new client workflow.

thewrz and others added 5 commits July 7, 2026 08:59
Reversible migration 040: clients (id, name UNIQUE non-empty, library_id
SET NULL, timestamps) + projects.client_id RESTRICT + index. Commits the
approved design doc and implementation plan for #391.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New src/db/queries/clients.ts (createClient/listClients/getClient +
assertClientExists, ClientNotFoundError/ClientLibraryNotFoundError). getClient
joins its active projects as full ProjectSummary rows (LATERAL json_agg for
sources). ProjectSummary gains clientId/clientName; updateProject gains a
validated clientId set/clear path (unknown client → ClientNotFoundError → 422).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST/GET /clients + GET /clients/{id} (src/api/clients.ts, wired in router).
PATCH /projects/{id} accepts clientId (uuid | null): associate/disassociate,
echoes clientId, unknown client -> 422. openapi.yaml documents the endpoints,
ClientSummary/ClientDetail schemas, ClientId param, and ProjectSummary +
PATCH-response clientId/clientName; both directions of the contract gate pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
list_clients/get_client (read) + create_client (write) mirror the REST surface;
registered in tools.ts with OP_TO_TOOL + TOOL_TIERS entries (contract gate green).
update_project tool gains clientId (uuid | null): associate/disassociate, echoes
clientId, unknown client -> isError. Tools never throw.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the four locked decisions (link-not-merge, RESTRICT/SET-NULL delete
semantics, firm forward-compat as prose, clients-are-organizations-not-actors)
and supersedes the ADR-025 firm/client-tier deferral.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ee5019dc-33a3-45e9-aaf3-9ca598aa6ec6

📥 Commits

Reviewing files that changed from the base of the PR and between 6a752ed and 0dadde4.

📒 Files selected for processing (11)
  • openapi.yaml
  • src/api/contract.integration.test.ts
  • src/api/router.ts
  • src/db/index.ts
  • src/db/migrations/040_create_clients.ts
  • src/db/queries/clients.test.ts
  • src/db/queries/clients.ts
  • src/db/queries/projects.test.ts
  • src/db/queries/projects.ts
  • src/mcp/capabilities.ts
  • src/mcp/contract-map.ts
📝 Walkthrough

Walkthrough

Adds a first-class clients entity: a new database table and migration, a query module for client CRUD/list/get, REST endpoints (POST/GET clients, GET client by id), project PATCH support for clientId, MCP tools (list_clients, get_client, create_client), OpenAPI contract updates, and ADR/design/plan documentation.

Changes

First-class clients feature

Layer / File(s) Summary
Clients table migration
src/db/migrations/040_create_clients.ts
Adds clients table (UUID PK, unique name, optional library_id FK with ON DELETE SET NULL) and projects.client_id FK with ON DELETE RESTRICT plus index; reversible down migration.
Clients query module and project update integration
src/db/queries/clients.ts, src/db/queries/clients.test.ts, src/db/queries/clients.integration.test.ts, src/db/queries/projects.ts, src/db/queries/projects.test.ts, src/db/index.ts
Adds createClient/listClients/getClient/assertClientExists with typed errors, extends ProjectSummary/updateProject/createProject with clientId/clientName, and re-exports via the DB barrel; covered by unit and integration tests.
REST endpoints for clients and project patch
src/api/clients.ts, src/api/router.ts, src/api/projects.ts, src/api/projects.test.ts, src/api/contract.integration.test.ts, src/api/clients.integration.test.ts
Adds POST/GET /clients and GET /clients/{id} handlers/routes, extends PATCH /projects/{id} to accept/validate clientId and map ClientNotFoundError to 422, plus integration/contract test coverage.
MCP client tools and project update wiring
src/mcp/clients-handlers.ts, src/mcp/clients-tools.ts, src/mcp/clients.integration.test.ts, src/mcp/capabilities.ts, src/mcp/contract-map.ts, src/mcp/tools.ts, src/mcp/project-handlers.ts
Adds list_clients/get_client/create_client MCP tools with tier registration and REST↔tool contract mapping, and extends update_project to accept/return clientId.
OpenAPI contract updates
openapi.yaml
Adds /clients paths, ClientId parameter, ClientSummary/ClientDetail schemas, and extends ProjectSummary/PATCH /projects/{id} with clientId.
ADR, design spec, and implementation plan
docs/adr/054-first-class-clients.md, docs/superpowers/specs/2026-07-07-issue-391-clients-design.md, docs/superpowers/plans/2026-07-07-issue-391-clients.md
Documents design decisions, deletion semantics, non-goals, and the end-to-end implementation plan for the clients feature.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant ClientsHandler
  participant ClientsQuery
  participant Postgres
  Client->>Router: POST /clients {name, libraryId}
  Router->>ClientsHandler: createClientHandler
  ClientsHandler->>ClientsQuery: createClient(input)
  ClientsQuery->>Postgres: INSERT INTO clients
  Postgres-->>ClientsQuery: client row
  ClientsQuery-->>ClientsHandler: ClientSummary
  ClientsHandler-->>Client: 201 ClientSummary

  Client->>Router: PATCH /projects/{id} {clientId}
  Router->>ClientsQuery: assertClientExists(clientId)
  ClientsQuery->>Postgres: SELECT client by id
  Postgres-->>ClientsQuery: row or none
  ClientsQuery-->>Router: ok or ClientNotFoundError
  Router->>Postgres: UPDATE projects SET client_id
  Router-->>Client: 200 ProjectSummary with clientId
Loading

Possibly related PRs

  • wrzonance/SpecR#334: Extends the same tier-gated MCP infrastructure in src/mcp/capabilities.ts (TOOL_TIERS) and src/mcp/contract-map.ts (OP_TO_TOOL) that this PR wires new client tools into.
  • wrzonance/SpecR#336: Overlaps directly on the MCP update_project handler/schema that this PR extends to accept and return clientId.
  • wrzonance/SpecR#341: Extends the same TOOL_TIERS/OP_TO_TOOL mapping mechanism to expose new REST operations as MCP tools, for templates rather than clients.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: first-class clients with a clients table and project client linkage.
Linked Issues check ✅ Passed The changes cover the required migration, REST and MCP client operations, project clientId support, ProjectSummary fields, and the ADR decisions.
Out of Scope Changes check ✅ Passed The added documentation files are directly tied to the first-class clients feature and do not appear unrelated to the requested scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-391

Comment @coderabbitai help to get the list of available commands.

@thewrz

thewrz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up on the Test check: the unit file src/start-specr-shell.test.ts fails on a stale assertion — it expects examples/web_ui_demo/Start-SpecR.sh to contain the literal docker compose up -d postgres, which base commit 19343eb ("updated specr shell script to heal docker container") removed when it reworked the heal path. This PR touches neither file (git diff main...HEAD on both is empty), so the failure is pre-existing on main, not introduced here. Everything this PR adds — DB/REST/MCP client tests and both contract gates — is green (full local integration run: 977 pass / 0 fail). Recommend fixing the stale assertion in its own PR rather than folding it in here.

Fold in the lint fixes that were left uncommitted: prettier reformat of the
clients API integration test, and the DB duplicate-name test asserting via the
typed getPgCode helper (avoids an expect.objectContaining any).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thewrz

thewrz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

The patch handler returns 422 via ClientNotFoundError when a syntactically
valid but unknown clientId is supplied (src/api/projects.ts), and the clientId
description already promises it ('an unknown client is rejected 422'), but the
responses list omitted the 422 entry. Add it, referencing the shared
UnprocessableEntity response — consistent with POST /clients. Keeps the
authoritative contract in sync with the emitted status (the contract gate
validates documented responses but cannot enumerate an undocumented one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thewrz

thewrz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Codex (GPT-5.5, xhigh) adversarial review — backup gate

CodeRabbit is rate-limited on this PR (bare "Review finished" ack, no walkthrough), so Codex ran as the review gate. Two P2 findings, both evaluated against ADR-054 and the code:

[P2] Document the new 422 patch response on PATCH /projects/{id} — FIXED in 6a752ed.
The handler returns 422 via ClientNotFoundError for a syntactically valid but unknown clientId (src/api/projects.ts:255-257), and the clientId field description already promised it ("an unknown client is rejected 422"), but the OpenAPI responses list omitted the 422 entry. Added '422': #/components/responses/UnprocessableEntity, consistent with POST /clients. The contract gate validates documented responses but cannot enumerate an undocumented-but-emitted status, so this drift slipped through CI. Both contract gates (REST + MCP) green after the fix.

[P2] Reject non-client-tier library links in createClient — DECLINING.
ADR-054 Decision #1 deliberately locks clients.library_id as an optional pointer to the client's master library — "a link, not a merge" — and specifies existence-only validation (unknown libraryId → 422 via ClientLibraryNotFoundError, already implemented). Enforcing tier = 'client' at this boundary would couple the organizational link back to the content-provenance taxonomy — the exact conflation ADR-054's Context section warns against ("Approximating the client by library membership conflates content provenance with organizational ownership — two different questions"). No contract rule states the library must be client-tier; "client-tier master library" is descriptive intent prose, not a schema constraint. This is a scope addition beyond the approved design, not a defect — declining per ADR-054.

@thewrz
thewrz marked this pull request as ready for review July 7, 2026 17:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/db/queries/projects.ts (1)

395-416: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Same TOCTOU pattern as createClient: assertClientExists then UPDATE isn't race-safe.

If the client is deleted between the assertClientExists check (line 403) and the UPDATE (line 404), the UPDATE fails with a raw FK violation (23503) rather than a clean ClientNotFoundError (→ 422), surfacing an ambiguous 500 instead. Same fix as suggested for createClient in clients.ts: catch the FK-violation code and map it to ClientNotFoundError.

🔒️ Proposed fix
+import { ClientNotFoundError, assertClientExists } from './clients.js';
+import { getPgCode } from '../../lib/pg-errors.js';
-import { assertClientExists } from './clients.js';
...
   } catch (err) {
     if (err instanceof DatabaseError) throw err;
+    if (getPgCode(err) === '23503' && typeof input.clientId === 'string') {
+      throw new ClientNotFoundError(`client ${input.clientId} not found`, { cause: err });
+    }
     throw new DatabaseError(`updateProject: update failed for ${id}`, { cause: err });
   }

Based on learnings, "treat the FK-violation error from the DELETE as the authoritative signal" for check-then-act races — the same principle applies to this check-then-update path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/queries/projects.ts` around lines 395 - 416, The updateProject flow
still has a check-then-act race: `assertClientExists` can pass and the later
`pool.query` UPDATE in `updateProject` can still fail with a foreign-key
violation if the client was deleted in between. Update `updateProject` to treat
the FK-violation from the UPDATE as the authoritative signal, similar to
`createClient` in `clients.ts`: catch the database error code for missing client
and rethrow `ClientNotFoundError` instead of letting it fall through as a
generic `DatabaseError`.

Source: Learnings

src/db/queries/clients.ts (1)

97-116: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

TOCTOU: assertLibraryExists check then INSERT is not race-safe.

If the referenced library is deleted between the existence check (line 102) and the INSERT (lines 103-106), the insert fails with a raw FK violation (23503), which falls through to the generic DatabaseError wrap (line 114) instead of the intended ClientLibraryNotFoundError (→ 422). Per an established pattern in this codebase, the FK-violation error from the write itself should be treated as the authoritative signal, not just the pre-check.

getPgCode (already used in clients.integration.test.ts from ../../lib/pg-errors.js) can classify this without an unsafe cast.

🔒️ Proposed fix
+import { getPgCode } from '../../lib/pg-errors.js';
...
   } catch (err) {
     // ClientLibraryNotFoundError / DatabaseError re-throw unwrapped; a raw pg error
     // (e.g. 23505 unique name) is wrapped with its cause so getPgCode → 409 at the handler.
     if (err instanceof DatabaseError) throw err;
+    if (getPgCode(err) === '23503') {
+      throw new ClientLibraryNotFoundError(`library ${input.libraryId} not found`, { cause: err });
+    }
     throw new DatabaseError(`createClient: insert failed for "${input.name}"`, { cause: err });
   }

Based on learnings, "treat the FK-violation error from the DELETE as the authoritative signal" for check-then-act races applies analogously here to check-then-insert.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/queries/clients.ts` around lines 97 - 116, The createClient flow in
clients.ts is race-prone because it relies on assertLibraryExists before the
INSERT, so a deleted library can turn into a generic DatabaseError instead of
ClientLibraryNotFoundError. Update createClient to treat the FK violation from
the db.query INSERT as the authoritative signal: inspect the caught pg error
with getPgCode, and when it is the library foreign-key code map it to
ClientLibraryNotFoundError; keep other errors wrapped as they are now. Preserve
the existing DatabaseError handling for non-FK failures and locate the change in
createClient / assertLibraryExists.

Source: Learnings

src/db/migrations/040_create_clients.ts (1)

11-17: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider indexing clients.library_id.

library_id is a FK with ON DELETE SET NULL; without an index, deleting a library forces a full scan of clients to null out references, and any future lookup by library_id (e.g., checking if a library is already linked to a client) would also scan. Worth adding an index alongside the FK.

♻️ Proposed addition
   pgm.addConstraint('clients', 'clients_name_unique', 'UNIQUE (name)');
   pgm.addConstraint('clients', 'clients_name_nonempty', 'CHECK (length(trim(name)) > 0)');
+  pgm.createIndex('clients', 'library_id', { name: 'clients_library_id_idx' });

And in down, drop it before dropping the table (table drop will remove it anyway via cascade, but for symmetry):

 export const down = (pgm: MigrationBuilder): void => {
   pgm.dropIndex('projects', 'client_id', { name: 'projects_client_id_idx' });
   pgm.dropColumns('projects', ['client_id']);
   pgm.dropTable('clients', { cascade: true });
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/db/migrations/040_create_clients.ts` around lines 11 - 17, Add an index
for the clients.library_id foreign key in the 040_create_clients migration so
deletes and lookups don’t scan the whole clients table. Update the migration
that calls pgm.createTable for clients to also create an index on library_id,
and make the corresponding down path remove that index before the table drop for
symmetry.
openapi.yaml (1)

1461-1468: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the PATCH /projects/{id} description to mention clientId.

The anyOf was extended to include clientId (Line 1481) and the description now covers a third alternative, but the prose still only says "At least one of name or sectionNumberFormat must be provided." Update it to include clientId for consistency with the actual contract.

📝 Proposed fix
       description: >
-        Updates a project's mutable settings. At least one of `name` or
-        `sectionNumberFormat` must be provided. The name must be non-empty.
-        There is no uniqueness constraint on project names, so there is no 409
-        path.
+        Updates a project's mutable settings. At least one of `name`,
+        `sectionNumberFormat`, or `clientId` must be provided. The name must
+        be non-empty. There is no uniqueness constraint on project names, so
+        there is no 409 path.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openapi.yaml` around lines 1461 - 1468, The PATCH /projects/{id} description
is out of sync with the request contract because it still says only name or
sectionNumberFormat are allowed, while the schema in the patchProject operation
also accepts clientId. Update the description text in the patchProject section
of openapi.yaml so it explicitly says at least one of name, sectionNumberFormat,
or clientId must be provided, keeping the prose consistent with the anyOf
definition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@openapi.yaml`:
- Around line 1461-1468: The PATCH /projects/{id} description is out of sync
with the request contract because it still says only name or sectionNumberFormat
are allowed, while the schema in the patchProject operation also accepts
clientId. Update the description text in the patchProject section of
openapi.yaml so it explicitly says at least one of name, sectionNumberFormat, or
clientId must be provided, keeping the prose consistent with the anyOf
definition.

In `@src/db/migrations/040_create_clients.ts`:
- Around line 11-17: Add an index for the clients.library_id foreign key in the
040_create_clients migration so deletes and lookups don’t scan the whole clients
table. Update the migration that calls pgm.createTable for clients to also
create an index on library_id, and make the corresponding down path remove that
index before the table drop for symmetry.

In `@src/db/queries/clients.ts`:
- Around line 97-116: The createClient flow in clients.ts is race-prone because
it relies on assertLibraryExists before the INSERT, so a deleted library can
turn into a generic DatabaseError instead of ClientLibraryNotFoundError. Update
createClient to treat the FK violation from the db.query INSERT as the
authoritative signal: inspect the caught pg error with getPgCode, and when it is
the library foreign-key code map it to ClientLibraryNotFoundError; keep other
errors wrapped as they are now. Preserve the existing DatabaseError handling for
non-FK failures and locate the change in createClient / assertLibraryExists.

In `@src/db/queries/projects.ts`:
- Around line 395-416: The updateProject flow still has a check-then-act race:
`assertClientExists` can pass and the later `pool.query` UPDATE in
`updateProject` can still fail with a foreign-key violation if the client was
deleted in between. Update `updateProject` to treat the FK-violation from the
UPDATE as the authoritative signal, similar to `createClient` in `clients.ts`:
catch the database error code for missing client and rethrow
`ClientNotFoundError` instead of letting it fall through as a generic
`DatabaseError`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d45e0d5f-62bc-4b80-bb28-ea55730fae13

📥 Commits

Reviewing files that changed from the base of the PR and between 19343eb and 6a752ed.

📒 Files selected for processing (24)
  • docs/adr/054-first-class-clients.md
  • docs/superpowers/plans/2026-07-07-issue-391-clients.md
  • docs/superpowers/specs/2026-07-07-issue-391-clients-design.md
  • openapi.yaml
  • src/api/clients.integration.test.ts
  • src/api/clients.ts
  • src/api/contract.integration.test.ts
  • src/api/projects.test.ts
  • src/api/projects.ts
  • src/api/router.ts
  • src/db/index.ts
  • src/db/migrations/040_create_clients.ts
  • src/db/queries/clients.integration.test.ts
  • src/db/queries/clients.test.ts
  • src/db/queries/clients.ts
  • src/db/queries/projects.test.ts
  • src/db/queries/projects.ts
  • src/mcp/capabilities.ts
  • src/mcp/clients-handlers.ts
  • src/mcp/clients-tools.ts
  • src/mcp/clients.integration.test.ts
  • src/mcp/contract-map.ts
  • src/mcp/project-handlers.ts
  • src/mcp/tools.ts

thewrz and others added 4 commits July 7, 2026 10:59
assertClientExists / assertLibraryExists are fast-path existence checks, but the
referenced row can be deleted between the check and the write. The FK then raises
pg 23503 on the write, which fell through to a generic DatabaseError → ambiguous
500 at the handler. Catch 23503 (via getPgCode) and re-throw the same typed error
as the fast path — ClientNotFoundError (updateProject) / ClientLibraryNotFoundError
(createClient) — so the race surfaces as the intended clean 422 with its pg cause
chained. A 23505 (duplicate client name) still keeps the generic wrap → 409.

Pinned with unit regressions faking a 23503 pg error on the write for both paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
clients.library_id is ON DELETE SET NULL, so deleting a library scans clients for
referencing rows; without an index that is a sequential scan. Add clients_library_id_idx
in up and drop it in down before the table drop for symmetry. Verified up/down/up
round-trips cleanly on a fresh DB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prose still read 'at least one of name or sectionNumberFormat', omitting the
clientId field the operation now accepts — the handler's own validation message
already lists all three. Align the description with the request schema and handler.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thewrz

thewrz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit body nitpicks (17:56Z review) — all 4 fixed

All four triaged VALID and fixed. Branch updated from main first (PR #399 merged, so the pre-existing start-specr-shell.test.ts known-red is resolved and CI must be fully green).

  1. updateProject TOCTOU (FK 23503 → 500) — Fixed in a15b58a. assertClientExists is a fast-path check; if the client is deleted before the UPDATE, the ON DELETE RESTRICT FK raised 23503 → generic DatabaseError → ambiguous 500. Now caught via getPgCode and re-thrown as ClientNotFoundError (cause chained) → the intended clean 422.

  2. createClient TOCTOU (FK 23503 → 500) — Fixed in a15b58a. Same race on the library_id link: a 23503 on INSERT now maps to ClientLibraryNotFoundError (cause chained) → 422. A 23505 (duplicate name) still keeps the generic wrap → 409, distinguished by code. Both mappings pinned with unit regressions that fake a 23503 pg error on the write.

  3. Missing index on clients.library_id — Fixed in 3861d6b. The FK is ON DELETE SET NULL, so a library delete scans clients for referencers; added clients_library_id_idx in up and drop it in down before the table drop for symmetry. Verified up/down/up round-trips cleanly on a fresh isolated DB.

  4. openapi PATCH /projects/{id} description omitted clientId — Fixed in d8fae38. Prose now reads 'at least one of name, sectionNumberFormat, or clientId', matching the request schema and the handler's own validation message.

Verified locally: pnpm lint clean, unit 1431 pass / 0 fail (incl. the 2 new TOCTOU regressions), integration 977 pass / 0 fail, migration up/down/up clean. No review threads (all four were body-only nitpicks).

@thewrz
thewrz merged commit f4f7329 into main Jul 7, 2026
5 checks passed
@thewrz
thewrz deleted the feat/issue-391 branch July 7, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(db/api): first-class clients — clients table + projects.client_id (lifts the #209 deferral, ADR required)

1 participant