Skip to content

tsk-mz3diu [OPEN] Project create: name uniqueness + dup auto-reject - #2168

Merged
jaylfc merged 1 commit into
devfrom
exec/tsk-mz3diu
Jul 27, 2026
Merged

tsk-mz3diu [OPEN] Project create: name uniqueness + dup auto-reject#2168
jaylfc merged 1 commit into
devfrom
exec/tsk-mz3diu

Conversation

@jaylfc

@jaylfc jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Autonomous build of board card tsk-mz3diu.

Files:
tests/projects/test_project_store.py | 2 +-
tinyagentos/projects/project_store.py | 33 +++++++++++++++++++--
tinyagentos/routes/projects.py | 54 +++++++++++++++++++++++++++++++++++
3 files changed, 86 insertions(+), 3 deletions(-)


Summary by Gitar

  • Project uniqueness:
    • Added case-insensitive name uniqueness check and ProjectConflict exception in ProjectStore
    • Implemented 409 conflict response with free name/slug suggestions in project creation route

This will update automatically on new commits.

Summary by CodeRabbit

  • New Features

    • Project creation now detects duplicate names and slugs, including case-insensitive name matches.
    • Conflict responses identify the conflicting field and value and provide alternative suggestions.
  • Bug Fixes

    • Replaced generic project creation errors with clearer conflict handling for duplicate names and slugs.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Project creation now detects case-insensitive name and slug collisions, raises structured ProjectConflict errors, and returns 409 responses containing the conflicting field, value, and verified alternative suggestions.

Changes

Project collision handling

Layer / File(s) Summary
Store conflict detection
tinyagentos/projects/project_store.py, tests/projects/test_project_store.py
Adds ProjectConflict, performs case-insensitive name checks, translates slug uniqueness failures, and imports the new exception for store tests.
Route conflict suggestions
tinyagentos/routes/projects.py
Generates available numeric, prefixed, and UUID-based alternatives and returns them in structured 409 responses.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant create_project
  participant ProjectStore
  participant SQLite
  Client->>create_project: Submit project name and slug
  create_project->>ProjectStore: Create project
  ProjectStore->>SQLite: Check name and insert project
  SQLite-->>ProjectStore: Conflict or success
  ProjectStore-->>create_project: ProjectConflict
  create_project->>ProjectStore: Check candidate values
  ProjectStore-->>create_project: Available suggestions
  create_project-->>Client: Return 409 conflict response
Loading

Suggested reviewers: hognek

🚥 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 clearly reflects the main change: project creation now enforces name uniqueness and rejects duplicates.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exec/tsk-mz3diu

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gitar-bot

gitar-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Important

You are using the Gitar free plan. Upgrade to unlock code review, CI analysis, auto-apply, custom automations, and more.

Gitar

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Enforce case-insensitive project name uniqueness with actionable 409 conflicts

🐞 Bug fix ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Reject project creates when name matches an existing project (case-insensitive).
• Surface slug/name collisions as structured 409 responses with alternative suggestions.
• Introduce a typed conflict exception to propagate field/value context to the API layer.
Diagram

graph TD
  C((Client)) --> A["Projects API: POST /api/projects"] --> S["ProjectStore.create_project"] --> D{Conflicts?} --> OK["201 Project"]
  D -->|"no"| DB[("SQLite: projects")]
  DB --> S
  D -->|"yes: ProjectConflict"| G["Suggest free name/slug"] --> ERR["409 Conflict JSON"]
  subgraph Legend
    direction LR
    _actor((Actor)) ~~~ _svc["Service/Handler"] ~~~ _dec{Decision} ~~~ _db[(Database)]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. DB-level unique index on normalized name
  • ➕ Stronger invariant: uniqueness enforced even if multiple writers bypass API
  • ➕ Avoids race windows between check and insert
  • ➖ Requires migration strategy for existing duplicates (would fail or need cleanup)
  • ➖ More complex upgrade path (backfill/dedup + index creation)
2. Single insert with retry loop for slug/name suggestions
  • ➕ Eliminates pre-check race by treating DB as source of truth
  • ➕ Naturally supports concurrent creators by retrying until unique
  • ➖ Harder for name uniqueness without a DB constraint
  • ➖ More complexity and potentially more DB writes
3. Move suggestion generation to client/UI only
  • ➕ Keeps API simpler and avoids additional DB reads during error handling
  • ➖ Client may propose still-taken suggestions without server verification
  • ➖ Less consistent across clients (CLI/UI/etc.)

Recommendation: Current approach is a pragmatic fit given the explicit goal to avoid destructive upgrades for existing duplicate names: enforce case-insensitive name uniqueness via a query check and keep UNIQUE(slug) as the schema backstop. If concurrency issues appear (check/insert race), consider adding a normalized-name unique index in a future migration once dedup strategy is defined.

Files changed (3) +86 / -3

Enhancement (1) +54 / -0
projects.pyReturn actionable 409 on project create conflicts with suggestions +54/-0

Return actionable 409 on project create conflicts with suggestions

• Catches ProjectConflict from the store and returns a structured 409 payload including the collided field/value. Adds server-side suggestion generation (numeric suffix, prefixed, random suffix) and verifies each candidate against the store before returning.

tinyagentos/routes/projects.py

Bug fix (1) +31 / -2
project_store.pyAdd ProjectConflict and enforce case-insensitive name uniqueness +31/-2

Add ProjectConflict and enforce case-insensitive name uniqueness

• Introduces ProjectConflict (field + taken value) to represent name/slug collisions. Adds a case-insensitive name lookup and uses it to reject duplicate project names before insert; slug collisions are now re-raised as ProjectConflict when SQLite UNIQUE(slug) triggers.

tinyagentos/projects/project_store.py

Tests (1) +1 / -1
test_project_store.pyImport ProjectConflict for conflict-aware store API +1/-1

Import ProjectConflict for conflict-aware store API

• Updates the test module imports to include the new ProjectConflict exception alongside ProjectStore. Existing tests that expect ValueError on slug duplication remain compatible because ProjectConflict subclasses ValueError.

tests/projects/test_project_store.py

},
status_code=409,
)
except ValueError as e:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Dead except ValueError block — ProjectConflict inherits from ValueError and is caught first, so this handler no longer catches slug collisions.

If store.create_project() no longer raises any other ValueError, this block is unreachable and misleading.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

return await store.get_project_by_slug(value) is None


async def _free_suggestions(store, field: str, taken: str) -> list[str]:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: _free_suggestions lacks error handling around its database calls.

If any query inside the suggestion loops fails (e.g., connection drop, disk I/O error), the exception propagates out of _free_suggestions and turns the intended 409 into a 500. Wrap the DB calls in a try/except and return an empty or fallback suggestions list instead.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

import pytest_asyncio

from tinyagentos.projects.project_store import ProjectStore
from tinyagentos.projects.project_store import ProjectConflict, ProjectStore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SUGGESTION: ProjectConflict is imported but unused in this file.

No test currently references ProjectConflict (the existing duplicate-slug test still expects ValueError). Either remove the unused import or add a test that asserts ProjectConflict is raised for duplicate names.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@jaylfc

jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-orB review

VERDICT: Significant race conditions in name/slug uniqueness checks; missing tests for new conflict handling; fragile error message parsing.

  • TOCTOU race condition in tinyagentos/projects/project_store.py:163-165: get_project_by_name() check and INSERT are not atomic. Two concurrent requests with same name can both pass the check, then one fails on slug constraint (or both succeed if slugs differ). Should use INSERT OR IGNORE with a unique index on LOWER(name) or handle via transaction with SELECT ... FOR UPDATE (not supported by SQLite) / retry logic.

  • Fragile IntegrityError parsing in tinyagentos/projects/project_store.py:178-180: Checking "slug" in str(exc).lower() relies on SQLite error message format which may vary across versions/locales. Should inspect exc.sqlite_errorcode (code 2067 for UNIQUE constraint) and/or constraint name.

  • Race condition in suggestions tinyagentos/routes/projects.py:53-80: _free_suggestions verifies candidates are free at check time, but by the time client retries, another request could take the suggested name/slug. Suggestions are best-effort only — document this.

  • No tests for new behavior in tests/projects/test_project_store.py: Added ProjectConflict import but no tests for: get_project_by_name, case-insensitive name conflict, ProjectConflict raised with correct field/taken, suggestions generation, or concurrent create race conditions.

  • Inconsistent import style in tinyagentos/routes/projects.py:4-7: import asyncio as _asyncio, import json as _json but import uuid (no alias). Pick one convention.

  • Hardcoded magic values in tinyagentos/routes/projects.py:63-73: Prefixes ("team", "new", "app"), range 2-10, 20 random attempts, 5-char hex — should be constants with docstrings explaining rationale.

Automated first-pass review by the nemotron-ultra-orB lane. The lead still reviews before merge.

@kilo-code-bot

kilo-code-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
WARNING 2
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/projects.py 149 Dead except ValueError block
tinyagentos/routes/projects.py 41 _free_suggestions lacks error handling

SUGGESTION

File Line Issue
tests/projects/test_project_store.py 4 Unused ProjectConflict import
Files Reviewed (3 files)
  • tests/projects/test_project_store.py - 1 suggestion
  • tinyagentos/projects/project_store.py
  • tinyagentos/routes/projects.py - 2 warnings

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 71.7K · Output: 14.4K · Cached: 365.4K

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 35 rules

Grey Divider


Action required

1. Racy name uniqueness 🐞 Bug ☼ Reliability
Description
ProjectStore.create_project enforces name uniqueness via a pre-INSERT query, but without a DB
constraint or write lock, concurrent creates can both pass the check and insert duplicate
(case-insensitive) names. This violates the new uniqueness guarantee and can persist duplicate names
in the database.
Code

tinyagentos/projects/project_store.py[R163-168]

+        # Enforce case-insensitive name uniqueness via a query check (not a
+        # schema constraint) so existing duplicate names are not destructively
+        # rejected on upgrade. A UNIQUE(slug) constraint remains as the
+        # backstop for slug collisions caught via IntegrityError below.
+        if await self.get_project_by_name(name) is not None:
+            raise ProjectConflict("name", name)
Relevance

⭐⭐⭐ High

Team previously accepted that SELECT pre-checks are race-prone; prefers DB-constraint/IntegrityError
approach.

PR-#260

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code performs a separate SELECT-based name check and then an INSERT/COMMIT with no schema
constraint on name, so there is a check-then-act window where another concurrent create can insert
the same name. This is the same race pattern previously called out for slugs (fixed by relying on
UNIQUE + IntegrityError).

tinyagentos/projects/project_store.py[26-41]
tinyagentos/projects/project_store.py[152-183]
PR-#260

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ProjectStore.create_project()` checks name availability with `get_project_by_name()` and then inserts, but this is not atomic; concurrent requests can create duplicate names.

## Issue Context
Slug uniqueness is protected by a schema UNIQUE constraint and handled via `IntegrityError`, but name uniqueness is only an application-level pre-check.

## Fix Focus Areas
- tinyagentos/projects/project_store.py[26-41]
- tinyagentos/projects/project_store.py[152-183]
- tinyagentos/projects/project_store.py[203-211]

## Suggested fix
Implement an atomic enforcement strategy for case-insensitive name uniqueness, e.g.:
1) **Preferred (if migration allows):** add a unique index on `LOWER(name)` (case-insensitive) and catch `sqlite3.IntegrityError` to raise `ProjectConflict("name", name)` similarly to slug.
2) **If you cannot add the index due to existing duplicates:** wrap the `get_project_by_name()` check + INSERT in a transaction that prevents concurrent writers (SQLite: `BEGIN IMMEDIATE` / `COMMIT`, and rollback on failure) so only one creator can pass the check.

Also consider adding/adjusting tests to cover concurrent creates with the same name using separate tasks/connections.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. ProjectConflict imported from store 📜 Skill insight ⌂ Architecture
Description
tinyagentos/routes/projects.py directly imports ProjectConflict from
tinyagentos.projects.project_store, which violates the requirement that routes should not directly
import store modules/classes. This increases coupling and bypasses the intended
request.app.state.<store> access pattern.
Code

tinyagentos/routes/projects.py[R25-26]

+from tinyagentos.projects.project_store import ProjectConflict
from tinyagentos.projects.task_store import _ELEMENT_CLEAR
Relevance

⭐⭐ Medium

No close precedent found for banning route→store imports; could be enforced, but acceptance
uncertain.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185099 forbids direct imports of store modules/classes in route modules. The route
now imports ProjectConflict from tinyagentos.projects.project_store instead of only interacting
with the store via request.app.state.project_store.

tinyagentos/routes/projects.py[20-27]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Route modules must not directly import store modules/classes. `tinyagentos/routes/projects.py` imports `ProjectConflict` from `tinyagentos.projects.project_store`.

## Issue Context
The compliance rule requires stores be accessed via `request.app.state` and avoids direct store imports in route modules to reduce coupling.

## Fix Focus Areas
- tinyagentos/routes/projects.py[25-26]
- tinyagentos/projects/project_store.py[14-25]

## Suggested approach
- Create a small non-store module for shared exceptions, e.g. `tinyagentos/projects/errors.py`.
- Move `ProjectConflict` there.
- Update imports:
 - Store: `from tinyagentos.projects.errors import ProjectConflict`
 - Route: `from tinyagentos.projects.errors import ProjectConflict`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. No test for name uniqueness 📜 Skill insight ▣ Testability
Description
The PR adds case-insensitive project name uniqueness enforcement, but no regression test was added
to ensure duplicate names (including case variations) are rejected. This risks the behavior silently
regressing in future changes.
Code

tinyagentos/projects/project_store.py[R163-168]

+        # Enforce case-insensitive name uniqueness via a query check (not a
+        # schema constraint) so existing duplicate names are not destructively
+        # rejected on upgrade. A UNIQUE(slug) constraint remains as the
+        # backstop for slug collisions caught via IntegrityError below.
+        if await self.get_project_by_name(name) is not None:
+            raise ProjectConflict("name", name)
Relevance

⭐⭐⭐ High

Adding a regression test is a low-risk, standard hardening change; usually accepted even without
precedent.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185285 requires a regression test for bug fixes. The PR introduces new uniqueness
enforcement (if await self.get_project_by_name(name) ... raise ProjectConflict("name", name)), but
the tests file changes only adjust imports and does not add any new test covering duplicate project
names.

tinyagentos/projects/project_store.py[152-183]
tests/projects/test_project_store.py[1-38]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A bug-fix/behavior-fix was added (case-insensitive project name uniqueness), but there is no regression test that would fail without this change.

## Issue Context
The store now checks `get_project_by_name(name)` before insert and raises `ProjectConflict("name", name)` on collision. This should be covered by tests, especially for case-insensitive collisions.

## Fix Focus Areas
- tinyagentos/projects/project_store.py[152-183]
- tests/projects/test_project_store.py[1-50]

## Suggested approach
- Add a new test, e.g.:
 - Create project with `name="MyProj"`
 - Attempt to create `name="myproj"` (same slug different) and assert it raises `ProjectConflict` (or at least `ValueError`) and that `e.field == "name"`.
- Optionally add a route-level test asserting `/api/projects` returns 409 and includes `suggestions`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Invalid slug suggestions 🐞 Bug ≡ Correctness
Description
The 409 handler generates slug suggestions by prefixing/appending to the taken slug without
validating candidates against the API’s slug regex/length limit. For near-max-length slugs, returned
suggestions can be unusable because the create endpoint will reject them during request validation.
Code

tinyagentos/routes/projects.py[R41-70]

+async def _free_suggestions(store, field: str, taken: str) -> list[str]:
+    """Generate 2-3 free suggestions for a collided name or slug.
+
+    Each candidate is verified against the store so it is genuinely free.
+    Formats: numeric suffix (<value>-2), prefixed (<prefix>-<value>),
+    and short random suffix (<value>-<shortid>).
+    """
+    suggestions: list[str] = []
+
+    # 1. Numeric suffix: <value>-2, <value>-3, ...
+    for i in range(2, 10):
+        cand = f"{taken}-{i}"
+        if await _is_field_free(store, field, cand):
+            suggestions.append(cand)
+            break
+
+    # 2. Prefixed: <prefix>-<value>
+    for prefix in ("team", "new", "app"):
+        cand = f"{prefix}-{taken}"
+        if await _is_field_free(store, field, cand):
+            suggestions.append(cand)
+            break
+
+    # 3. Short random suffix: <value>-<shortid>
+    for _ in range(20):
+        shortid = uuid.uuid4().hex[:5]
+        cand = f"{taken}-{shortid}"
+        if await _is_field_free(store, field, cand):
+            suggestions.append(cand)
+            break
Relevance

⭐⭐ Medium

Suggestion validation vs slug regex/length seems reasonable, but no matching historical pattern
found.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API validates incoming slugs with _SLUG_RE (max 63 chars), but _free_suggestions()
constructs candidates by adding prefixes/suffixes and only checks DB availability. Candidates longer
than allowed (e.g., when the taken slug is already near the limit) will be suggested but then
rejected by request validation.

tinyagentos/routes/projects.py[31-86]
tinyagentos/routes/projects.py[41-72]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`_free_suggestions()` returns candidate slugs that may not satisfy the `CreateProjectIn.slug` validator (regex + max length). This can produce conflict responses containing suggestions that the API will reject.

## Issue Context
Slug validity is enforced by `_SLUG_RE` and the `CreateProjectIn` validator, but `_free_suggestions()` only checks for availability in the DB.

## Fix Focus Areas
- tinyagentos/routes/projects.py[31-86]
- tinyagentos/routes/projects.py[41-72]

## Suggested fix
When `field == "slug"`:
- Filter candidates by `_SLUG_RE.fullmatch(cand)` before calling `_is_field_free()`.
- Ensure candidates stay within the 63-character limit. For suffix/prefix generation, truncate the base portion to leave room for `-2`, `team-`, `-<shortid>`, etc.
- If no valid suggestions can be generated, return an empty list (or only valid ones) rather than invalid suggestions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. 409 response returns raw dict 📜 Skill insight ✧ Quality
Description
The new ProjectConflict handling returns a raw dict via JSONResponse instead of a Pydantic
response model. This weakens validation/contract clarity for the API error payload.
Code

tinyagentos/routes/projects.py[R138-148]

+    except ProjectConflict as e:
+        suggestions = await _free_suggestions(store, e.field, e.taken)
+        return JSONResponse(
+            {
+                "error": str(e),
+                "field": e.field,
+                "taken": e.taken,
+                "suggestions": suggestions,
+            },
+            status_code=409,
+        )
Relevance

⭐ Low

Similar request to model route responses with Pydantic was rejected previously; raw dict responses
are tolerated.

PR-#2122

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 2185155 requires Pydantic models for route request/response payloads. The added
conflict handler returns an untyped dict payload (`{"error": ..., "field": ..., "taken": ...,
"suggestions": ...}) via JSONResponse`.

tinyagentos/routes/projects.py[138-148]
Skill: taos-development-skill

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Route request/response payloads must use Pydantic models. The new 409 conflict response is constructed as an untyped dict inside `JSONResponse`.

## Issue Context
FastAPI supports documented/typed error responses via `responses={...}` and Pydantic models. This improves schema docs and prevents accidental shape drift.

## Fix Focus Areas
- tinyagentos/routes/projects.py[75-163]

## Suggested approach
- Add a Pydantic model, e.g.:
 - `class ProjectConflictOut(BaseModel): error: str; field: str; taken: str; suggestions: list[str]`
- Update the route decorator to declare the 409 model:
 - `@router.post("/api/projects", responses={409: {"model": ProjectConflictOut}})`
- Return `ProjectConflictOut(...)` (or raise `HTTPException(status_code=409, detail=ProjectConflictOut(...).model_dump())` if you standardize on `detail`, but prefer returning the model directly).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +25 to 26
from tinyagentos.projects.project_store import ProjectConflict
from tinyagentos.projects.task_store import _ELEMENT_CLEAR

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. projectconflict imported from store 📜 Skill insight ⌂ Architecture

tinyagentos/routes/projects.py directly imports ProjectConflict from
tinyagentos.projects.project_store, which violates the requirement that routes should not directly
import store modules/classes. This increases coupling and bypasses the intended
request.app.state.<store> access pattern.
Agent Prompt
## Issue description
Route modules must not directly import store modules/classes. `tinyagentos/routes/projects.py` imports `ProjectConflict` from `tinyagentos.projects.project_store`.

## Issue Context
The compliance rule requires stores be accessed via `request.app.state` and avoids direct store imports in route modules to reduce coupling.

## Fix Focus Areas
- tinyagentos/routes/projects.py[25-26]
- tinyagentos/projects/project_store.py[14-25]

## Suggested approach
- Create a small non-store module for shared exceptions, e.g. `tinyagentos/projects/errors.py`.
- Move `ProjectConflict` there.
- Update imports:
  - Store: `from tinyagentos.projects.errors import ProjectConflict`
  - Route: `from tinyagentos.projects.errors import ProjectConflict`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +163 to +168
# Enforce case-insensitive name uniqueness via a query check (not a
# schema constraint) so existing duplicate names are not destructively
# rejected on upgrade. A UNIQUE(slug) constraint remains as the
# backstop for slug collisions caught via IntegrityError below.
if await self.get_project_by_name(name) is not None:
raise ProjectConflict("name", name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. No test for name uniqueness 📜 Skill insight ▣ Testability

The PR adds case-insensitive project name uniqueness enforcement, but no regression test was added
to ensure duplicate names (including case variations) are rejected. This risks the behavior silently
regressing in future changes.
Agent Prompt
## Issue description
A bug-fix/behavior-fix was added (case-insensitive project name uniqueness), but there is no regression test that would fail without this change.

## Issue Context
The store now checks `get_project_by_name(name)` before insert and raises `ProjectConflict("name", name)` on collision. This should be covered by tests, especially for case-insensitive collisions.

## Fix Focus Areas
- tinyagentos/projects/project_store.py[152-183]
- tests/projects/test_project_store.py[1-50]

## Suggested approach
- Add a new test, e.g.:
  - Create project with `name="MyProj"`
  - Attempt to create `name="myproj"` (same slug different) and assert it raises `ProjectConflict` (or at least `ValueError`) and that `e.field == "name"`.
- Optionally add a route-level test asserting `/api/projects` returns 409 and includes `suggestions`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +163 to +168
# Enforce case-insensitive name uniqueness via a query check (not a
# schema constraint) so existing duplicate names are not destructively
# rejected on upgrade. A UNIQUE(slug) constraint remains as the
# backstop for slug collisions caught via IntegrityError below.
if await self.get_project_by_name(name) is not None:
raise ProjectConflict("name", name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

4. Racy name uniqueness 🐞 Bug ☼ Reliability

ProjectStore.create_project enforces name uniqueness via a pre-INSERT query, but without a DB
constraint or write lock, concurrent creates can both pass the check and insert duplicate
(case-insensitive) names. This violates the new uniqueness guarantee and can persist duplicate names
in the database.
Agent Prompt
## Issue description
`ProjectStore.create_project()` checks name availability with `get_project_by_name()` and then inserts, but this is not atomic; concurrent requests can create duplicate names.

## Issue Context
Slug uniqueness is protected by a schema UNIQUE constraint and handled via `IntegrityError`, but name uniqueness is only an application-level pre-check.

## Fix Focus Areas
- tinyagentos/projects/project_store.py[26-41]
- tinyagentos/projects/project_store.py[152-183]
- tinyagentos/projects/project_store.py[203-211]

## Suggested fix
Implement an atomic enforcement strategy for case-insensitive name uniqueness, e.g.:
1) **Preferred (if migration allows):** add a unique index on `LOWER(name)` (case-insensitive) and catch `sqlite3.IntegrityError` to raise `ProjectConflict("name", name)` similarly to slug.
2) **If you cannot add the index due to existing duplicates:** wrap the `get_project_by_name()` check + INSERT in a transaction that prevents concurrent writers (SQLite: `BEGIN IMMEDIATE` / `COMMIT`, and rollback on failure) so only one creator can pass the check.

Also consider adding/adjusting tests to cover concurrent creates with the same name using separate tasks/connections.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +41 to +70
async def _free_suggestions(store, field: str, taken: str) -> list[str]:
"""Generate 2-3 free suggestions for a collided name or slug.

Each candidate is verified against the store so it is genuinely free.
Formats: numeric suffix (<value>-2), prefixed (<prefix>-<value>),
and short random suffix (<value>-<shortid>).
"""
suggestions: list[str] = []

# 1. Numeric suffix: <value>-2, <value>-3, ...
for i in range(2, 10):
cand = f"{taken}-{i}"
if await _is_field_free(store, field, cand):
suggestions.append(cand)
break

# 2. Prefixed: <prefix>-<value>
for prefix in ("team", "new", "app"):
cand = f"{prefix}-{taken}"
if await _is_field_free(store, field, cand):
suggestions.append(cand)
break

# 3. Short random suffix: <value>-<shortid>
for _ in range(20):
shortid = uuid.uuid4().hex[:5]
cand = f"{taken}-{shortid}"
if await _is_field_free(store, field, cand):
suggestions.append(cand)
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

5. Invalid slug suggestions 🐞 Bug ≡ Correctness

The 409 handler generates slug suggestions by prefixing/appending to the taken slug without
validating candidates against the API’s slug regex/length limit. For near-max-length slugs, returned
suggestions can be unusable because the create endpoint will reject them during request validation.
Agent Prompt
## Issue description
`_free_suggestions()` returns candidate slugs that may not satisfy the `CreateProjectIn.slug` validator (regex + max length). This can produce conflict responses containing suggestions that the API will reject.

## Issue Context
Slug validity is enforced by `_SLUG_RE` and the `CreateProjectIn` validator, but `_free_suggestions()` only checks for availability in the DB.

## Fix Focus Areas
- tinyagentos/routes/projects.py[31-86]
- tinyagentos/routes/projects.py[41-72]

## Suggested fix
When `field == "slug"`:
- Filter candidates by `_SLUG_RE.fullmatch(cand)` before calling `_is_field_free()`.
- Ensure candidates stay within the 63-character limit. For suffix/prefix generation, truncate the base portion to leave room for `-2`, `team-`, `-<shortid>`, etc.
- If no valid suggestions can be generated, return an empty list (or only valid ones) rather than invalid suggestions.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@jaylfc

jaylfc commented Jul 27, 2026

Copy link
Copy Markdown
Owner Author

nemotron-ultra-kilo review

VERDICT: Request changes — race condition in name uniqueness check, missing tests for new conflict handling, and suggestion generation has TOCTOU issues.

  • tinyagentos/projects/project_store.py:160-164 — Race condition: get_project_by_name check and INSERT are not atomic; concurrent requests can both pass the check and hit UNIQUE constraint on slug (or create duplicate names if slug differs). Should use INSERT with ON CONFLICT or handle IntegrityError for name too (requires UNIQUE index on LOWER(name)).

  • tinyagentos/projects/project_store.py:175-179 — Inconsistent error handling: slug conflicts raise ProjectConflict via IntegrityError catch, but name conflicts raise ProjectConflict via explicit check — different code paths for same semantic error.

  • tinyagentos/routes/projects.py:33-77 — _free_suggestions has TOCTOU: suggestions verified free at generation time may be taken by the time client retries. Acceptable for UX hints but should be documented as non-guaranteed.

  • tinyagentos/routes/projects.py:33-77 — Suggestion generation stops at first match per strategy (break), yielding at most 3 suggestions but often fewer; comment says "2-3" but logic doesn't guarantee minimum.

  • tests/projects/test_project_store.py — No tests added for ProjectConflict, get_project_by_name, case-insensitive name uniqueness, or the new 409 response with suggestions.

  • tinyagentos/projects/project_store.py:10-19 — ProjectConflict inherits from ValueError; routes catch ProjectConflict before ValueError (correct), but this hierarchy is fragile — consider a dedicated base exception.

  • tinyagentos/projects/project_store.py:200-210 — get_project_by_name uses LOWER(name) = LOWER(?) but SQLite's default collation is case-insensitive for ASCII only; non-ASCII names may not match correctly. Consider explicit COLLATE NOCASE.
    VERDICT: Request changes — race condition in name uniqueness check, missing tests for new conflict handling, and suggestion generation has TOCTOU issues.

  • tinyagentos/projects/project_store.py:160-164 — Race condition: get_project_by_name check and INSERT are not atomic; concurrent requests can both pass the check and hit UNIQUE constraint on slug (or create duplicate names if slug differs). Should use INSERT with ON CONFLICT or handle IntegrityError for name too (requires UNIQUE index on LOWER(name)).

  • tinyagentos/projects/project_store.py:175-179 — Inconsistent error handling: slug conflicts raise ProjectConflict via IntegrityError catch, but name conflicts raise ProjectConflict via explicit check — different code paths for same semantic error.

  • tinyagentos/routes/projects.py:33-77 — _free_suggestions has TOCTOU: suggestions verified free at generation time may be taken by the time client retries. Acceptable for UX hints but should be documented as non-guaranteed.

  • tinyagentos/routes/projects.py:33-77 — Suggestion generation stops at first match per strategy (break), yielding at most 3 suggestions but often fewer; comment says "2-3" but logic doesn't guarantee minimum.

  • tests/projects/test_project_store.py — No tests added for ProjectConflict, get_project_by_name, case-insensitive name uniqueness, or the new 409 response with suggestions.

  • tinyagentos/projects/project_store.py:10-19 — ProjectConflict inherits from ValueError; routes catch ProjectConflict before ValueError (correct), but this hierarchy is fragile — consider a dedicated base exception.

  • tinyagentos/projects/project_store.py:200-210 — get_project_by_name uses LOWER(name) = LOWER(?) but SQLite's default collation is case-insensitive for ASCII only; non-ASCII names may not match correctly. Consider explicit COLLATE NOCASE.

Automated first-pass review by the nemotron-ultra-kilo lane. The lead still reviews before merge.

@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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tinyagentos/projects/project_store.py (1)

163-181: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Enforce case-insensitive project name uniqueness in the database.

create_project checks get_project_by_name separately from the INSERT, so concurrent create requests can both pass the lookup and then both insert duplicate LOWER(name) values. Add a normalized-name unique key/index in a migration after resolving existing duplicates, and translate its IntegrityError to ProjectConflict; the existing name lookup can then remain only as a 409 hint for existing legacy duplicates.

🤖 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 `@tinyagentos/projects/project_store.py` around lines 163 - 181, Update the
project schema migration and create_project flow to enforce case-insensitive
name uniqueness at the database level: resolve existing duplicate LOWER(name)
values before adding a normalized-name unique key or index, then handle its
sqlite3.IntegrityError as ProjectConflict("name", name) while retaining
get_project_by_name as the legacy-duplicate hint. Keep the existing slug
uniqueness handling unchanged.
🤖 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.

Inline comments:
In `@tinyagentos/projects/project_store.py`:
- Around line 203-210: Replace the ASCII-only LOWER(name) comparison used by
get_project_by_name and project creation collision checks with Unicode-aware
casefolded key handling. Add or reuse a persisted normalized casefold key for
each project, ensure it is populated and uniquely constrained, and query it
consistently so names such as straße and STRASSE collide.

---

Outside diff comments:
In `@tinyagentos/projects/project_store.py`:
- Around line 163-181: Update the project schema migration and create_project
flow to enforce case-insensitive name uniqueness at the database level: resolve
existing duplicate LOWER(name) values before adding a normalized-name unique key
or index, then handle its sqlite3.IntegrityError as ProjectConflict("name",
name) while retaining get_project_by_name as the legacy-duplicate hint. Keep the
existing slug uniqueness handling unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e666950-10e4-4ab4-8782-27f07008c243

📥 Commits

Reviewing files that changed from the base of the PR and between d3ccf22 and f687244.

📒 Files selected for processing (3)
  • tests/projects/test_project_store.py
  • tinyagentos/projects/project_store.py
  • tinyagentos/routes/projects.py

Comment on lines +203 to +210
async def get_project_by_name(self, name: str) -> dict | None:
async with self._db.execute(
"SELECT * FROM projects WHERE LOWER(name) = LOWER(?)", (name,)
) as cur:
row = await cur.fetchone()
if row is None:
return None
return _row_to_project(row, cur.description)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -eu

ast-grep outline tinyagentos/projects/project_store.py --items all
rg -n -C3 'enable_load_extension|load_extension|create_collation|casefold|class CreateProjectIn|name.*(pattern|max_length)' tinyagentos

Repository: jaylfc/taOS

Length of output: 6645


🏁 Script executed:

#!/usr/bin/env bash
set -eu

printf '--- project_store.py relevant sections ---\n'
sed -n '1,180p' tinyagentos/projects/project_store.py

printf '\n--- routes/projects.py CreateProjectIn section ---\n'
sed -n '1,140p' tinyagentos/routes/projects.py

printf '\n--- project route create project call sites ---\n'
rg -n -C4 'CreateProjectIn|create_project\(' tinyagentos/routes/projects.py tinyagentos -g '*.py'

Repository: jaylfc/taOS

Length of output: 240


🏁 Script executed:

#!/usr/bin/env bash
set -eu

printf '%s\n' '--- project_store.py relevant sections ---'
sed -n '1,180p' tinyagentos/projects/project_store.py

printf '%s\n' '--- routes/projects.py CreateProjectIn section ---'
sed -n '1,140p' tinyagentos/routes/projects.py

printf '%s\n' '--- project route create project call sites ---'
rg -n -C4 'CreateProjectIn|create_project\(' tinyagentos/routes/projects.py tinyagentos -g '*.py'

Repository: jaylfc/taOS

Length of output: 17422


🌐 Web query:

SQLite LOWER() function ASCII only UNICODE NOT IGNORE_CASE documentation

💡 Result:

The SQLite lower function, by default, is designed to convert only ASCII characters (the 26 letters of the English alphabet) to lowercase [1][2][3]. It does not provide full Unicode support for case conversion [2][4]. Key points regarding its behavior include: 1. Scope: The default built-in lower function handles 7-bit ASCII characters only [1][2][4]. Non-ASCII characters (such as accented characters or those from other scripts) remain unchanged when passed to this function [4]. 2. Unicode Support: SQLite does not include full Unicode case folding by default because the necessary tables are very large and Unicode case-folding rules can evolve, which could disrupt applications [2]. 3. Enabling Full Unicode: To perform case conversions on non-ASCII Unicode characters, you must compile SQLite with the SQLITE_ENABLE_ICU option and link it against the International Components for Unicode (ICU) library [5][2]. When this extension is loaded, the lower function is enhanced to support the full range of Unicode characters [5]. 4. Extension Usage: The ICU extension allows for both general case mapping (one argument: lower('ABC') -> 'abc') and language-specific case mapping (two arguments: lower('I', 'tr_tr') -> 'ı') [5]. For more information on the ICU extension and its implementation, you can refer to the official SQLite documentation on built-in scalar functions and the ICU extension files [1][5].

Citations:


🌐 Web query:

SQLite LOWER('ß') ASCII only behavior

💡 Result:

In SQLite, the default built-in LOWER function operates exclusively on ASCII characters [1][2]. Because the character 'ß' (German Eszett) is a non-ASCII Unicode character, the default LOWER function will not transform it and will return the character unchanged [1][3]. This behavior is a deliberate design choice in SQLite to maintain a lightweight footprint and avoid the complexities and potential database corruption issues that could arise if built-in case-folding rules changed alongside evolving Unicode standards [4][5]. To achieve full Unicode-aware case folding for characters like 'ß', you have two primary options: 1. Load the ICU (International Components for Unicode) extension: This official SQLite extension provides enhanced versions of the UPPER and LOWER functions as well as the LIKE operator, which support full Unicode case mapping and locale-aware comparisons [1][6][7][8]. 2. Application-level handling: You can manually implement case folding in your application code or use custom user-defined SQL functions and collations registered with the database connection to handle specific requirements, such as mapping 'ß' to 'ss' [4][9][2].

Citations:


🌐 Web query:

Python sqlite3 create_function unicode casefold example

💡 Result:

To create a custom SQL function in Python's sqlite3 module for unicode-aware case-insensitive comparisons, you can use the connection.create_function method to register Python's str.casefold function [1][2]. The casefold method is generally preferred over lower for caseless matching, as it is more aggressive and correctly handles special Unicode characters (such as the German 'ß', which becomes 'ss') [1][2]. Example implementation: import sqlite3 def casefold_func(s): return s.casefold if s is not None else None # Connect to the database conn = sqlite3.connect(":memory:") # Register the function # deterministic=True is recommended for functions that always return # the same output for the same input, allowing SQLite to optimize queries. conn.create_function("CASEFOLD", 1, casefold_func, deterministic=True) # Usage in SQL cursor = conn.cursor cursor.execute("CREATE TABLE users (name TEXT)") cursor.execute("INSERT INTO users (name) VALUES ('Straße')") # Query using the registered function cursor.execute("SELECT * FROM users WHERE CASEFOLD(name) = CASEFOLD(?)", ("STRASSE",)) print(cursor.fetchone) # Output: ('Straße',) conn.close Key details: 1. Method Signature: The create_function method takes the name of the function as it will appear in SQL, the number of arguments (narg), and the Python callable [3][4]. 2. Deterministic Parameter: Setting deterministic=True (available in Python 3.8+) is beneficial for performance if your function's output depends only on its inputs, as it enables SQLite to optimize queries involving the function [1][3]. 3. Handling None: Ensure your Python function handles potential NULL (None) values if your database columns are nullable [3][5]. 4. Alternative approach: If you intend to use this for sorting or ordering, you might consider implementing a custom collation using connection.create_collation instead, which defines how strings are compared rather than transforming them [1][2].

Citations:


Avoid ASCII-only SQLite LOWER() for project name collision checks.

CreateProjectIn.name has no ASCII constraint, so names like straße and STRASSE could both pass the schema and pass LOWER() lookup separately in plain SQLite. Store/collate a Unicode casefolded key, or enforce ASCII-only project names.

🤖 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 `@tinyagentos/projects/project_store.py` around lines 203 - 210, Replace the
ASCII-only LOWER(name) comparison used by get_project_by_name and project
creation collision checks with Unicode-aware casefolded key handling. Add or
reuse a persisted normalized casefold key for each project, ensure it is
populated and uniquely constrained, and query it consistently so names such as
straße and STRASSE collide.

@jaylfc
jaylfc merged commit ca1d2fe into dev Jul 27, 2026
19 checks passed
@jaylfc
jaylfc deleted the exec/tsk-mz3diu branch July 27, 2026 21:21
hognek pushed a commit to hognek/tinyagentos that referenced this pull request Jul 29, 2026
hognek pushed a commit to hognek/tinyagentos that referenced this pull request Jul 30, 2026
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.

1 participant