Skip to content

feat(domain): T-204 add projects bounded context (closes #72)#96

Merged
mpiton merged 2 commits into
mainfrom
feat/t-204-projects-domain
May 11, 2026
Merged

feat(domain): T-204 add projects bounded context (closes #72)#96
mpiton merged 2 commits into
mainfrom
feat/t-204-projects-domain

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented May 11, 2026

Summary

  • New domain/projects/ bounded context (model + ports + service) for Sprint 2, PRD F-001 multi-project tabs — mirrors the domain/tasks/ shape exactly.
  • Project { id: i64, path, name, vcs, created_at, settings_json } matches the live V001__initial.sql schema (the stale ARCHI §8 UUID-v7 spec is left for a separate cleanup since tasks.project_id is already i64).
  • ProjectRepository::{list, get, find_by_path, create} — no update / delete in Sprint 2 per the issue. find_by_path lives on the port so the libsql adapter (Sprint 2 T-205) can use the UNIQUE index instead of scanning list().

Why

Closes #72. Unblocks the libsql adapter (T-205), projects_list / projects_create Tauri commands, and the project picker UI.

Changes

  • src-tauri/src/domain/projects/{mod,model,ports,service}.rs — new context.
  • src-tauri/src/domain/mod.rs — register pub mod projects; (alphabetical).
  • scripts/codegen-types.ts — REQUIRED list gains \"Project.ts\".
  • src/shared/types/Project.ts + src/shared/types/index.ts — regenerated by pnpm run codegen.
  • CHANGELOG.md[Unreleased] / Added entry.

Security / robustness hardening (from in-PR review)

  • validate_name + parse_absolute_path reject control chars (C0 / C1 / NUL) and Unicode bidi overrides (U+202A..U+202E, U+2066..U+2069). Prevents \\0-induced CString panics in the FS adapter, log-injection via \\n / \\r, and RTL spoofing in the kanban / project picker.
  • PATH_MAX_CHARS = 4096 short-circuit bounds attacker-supplied paths before Path::components() walks.
  • pop_if_normal helper refuses to escape above the root (/foo/../../) and refuses to drop a Windows Prefix (C:, \\\\?\\C:, UNC).
  • Project::new(path, name) takes only business fields — id = 0 sentinel stays internal, mirroring Task::new.
  • Single-mutex InMemoryProjectRepo (no lock-ordering hazard) with with_failing_find_by_path builder so both ? branches in create_project get distinct coverage.
  • ports.rs doc records the TOCTOU contract for T-205.

Testing

  • cargo test --workspace: 211/211 pass (24 new in domain::projects).
  • cargo clippy --workspace --all-targets -- -D warnings: clean.
  • cargo fmt --check: clean.
  • cargo test --test architecture: 2/2 — no tokio / libsql / tauri imports leaked into the new context.
  • pnpm exec tsc -b + pnpm exec oxlint .: clean.
  • Coverage: model.rs 100%, service.rs 94.75% lines / 97.02% regions (above the 90% domain gate from CLAUDE.md). ports.rs 38.46% — same shape as tasks/ports.rs pre-adapter; full coverage lands with T-205 libsql wiring.

Acceptance criteria

  • Domain layer pure: no tokio, no libsql, no tauri imports — T-131 architecture test stays green.
  • RED tests on service.rs with in-memory repo: list empty, create + list returns it, duplicate path → DomainError::Conflict, blank name → DomainError::Validation.
  • ts-rs Project struct exported (src/shared/types/Project.ts).
  • Coverage ≥ 90% on domain/projects/{model,service}.rs.

Out of scope (sibling Sprint 2 tasks)

  • infrastructure/persistence/projects_repo.rs (T-205, libsql adapter).
  • interfaces/tauri_commands/projects.rs (projects_list / projects_create).
  • ARCHI §8 / live-schema id-type reconciliation (UUID v7 spec vs i64 reality).

Test plan

  • CI green on Linux / macOS / Windows.
  • pnpm run codegen produces a clean diff (no drift) — verified by .github/workflows/ci.yml lines 126-139.
  • Spot-check Project.ts shape matches the Rust struct.

Summary by CodeRabbit

  • New Features

    • Added foundational project management: create and list projects with robust path canonicalization, name/path validation, duplicate detection/conflict handling, and cross-platform normalization.
    • Generated TypeScript type definitions for Project to enable shared type safety.
  • Chores

    • Updated code generation verification to require the Project type binding.

Review Change Stack

Mirror the `tasks` context shape for `projects` (Sprint 2, PRD F-001):
`model::Project { id, path, name, vcs, created_at, settings_json }`,
`ports::ProjectRepository` (list/get/find_by_path/create — no update/delete
in this sprint), `service::{list_projects, create_project}` with lexical
path canonicalisation, name/path validation, and pre-check uniqueness
against `find_by_path`.

- Reject control chars + Unicode bidi overrides in name/path at the IPC
  boundary so adapters (`git2`, `portable_pty`) and the kanban UI can trust
  their inputs.
- `canonicalize_path` is split into `parse_absolute_path` (trim, length,
  charset, absolute) + a pure `Component` fold with a named `pop_if_normal`
  guard that refuses to escape above the root (`/foo/../..` → `/`) and
  never drops a Windows `Prefix`.
- `Project::new(path, name)` takes only business fields, mirroring
  `Task::new` ergonomics; `id = 0` sentinel stays internal and the repo
  adapter overwrites on insert.
- Single-mutex `InMemoryProjectRepo { state: Mutex<RepoState> }` with
  `with_failing_create` + `with_failing_find_by_path` builders covers both
  `?` branches in `create_project`.
- ts-rs exports `Project.ts`; `scripts/codegen-types.ts` REQUIRED list
  updated.
- Coverage: `model.rs` 100%, `service.rs` 94.75% lines / 97.02% regions
  (above the 90% domain gate); `ports.rs` 38.46% mirrors `tasks/ports.rs`
  pre-adapter.
- 24 RED-then-GREEN tests across the four files. `cargo test --workspace`
  211/211 pass, `cargo clippy --workspace --all-targets -- -D warnings`
  clean, `cargo test --test architecture` 2/2 pass (no infra imports
  leaked).
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add projects bounded context (T-204) for multi-project tabs support

✨ Enhancement

Grey Divider

Walkthroughs

Description
• New projects bounded context mirrors tasks structure with Project entity,
  ProjectRepository port, and pure use cases
• Lexical path canonicalisation with security hardening: rejects control chars, NUL bytes, Unicode
  bidi overrides, and enforces absolute paths
• create_project validates name/path, checks uniqueness via find_by_path, prevents duplicate
  canonical paths (e.g., /foo/../bar equivalence)
• 24 tests achieve 100% coverage on model.rs, 94.75% on service.rs, with InMemoryProjectRepo
  mock supporting failure injection
• TypeScript types auto-generated via ts-rs and registered in codegen pipeline
Diagram
flowchart LR
  A["IPC Input<br/>path, name"] -->|validate_name<br/>parse_absolute_path| B["Validation<br/>Security Checks"]
  B -->|canonicalize_path| C["Lexical<br/>Normalisation"]
  C -->|find_by_path| D["Uniqueness<br/>Check"]
  D -->|create| E["Project<br/>Entity"]
  E -->|ts-rs export| F["TypeScript<br/>Project.ts"]
  B -->|DomainError| G["Reject<br/>Invalid Input"]
Loading

Grey Divider

File Changes

1. src-tauri/src/domain/projects/mod.rs ✨ Enhancement +11/-0

New projects bounded context module declaration

src-tauri/src/domain/projects/mod.rs


2. src-tauri/src/domain/projects/model.rs ✨ Enhancement +91/-0

Project entity with RFC 3339 timestamps and ts-rs export

src-tauri/src/domain/projects/model.rs


3. src-tauri/src/domain/projects/ports.rs ✨ Enhancement +66/-0

ProjectRepository trait with list/get/find_by_path/create operations

src-tauri/src/domain/projects/ports.rs


View more (6)
4. src-tauri/src/domain/projects/service.rs ✨ Enhancement +460/-0

Pure use cases with path canonicalisation and security validation

src-tauri/src/domain/projects/service.rs


5. src-tauri/src/domain/mod.rs ⚙️ Configuration changes +1/-0

Register projects module in alphabetical order

src-tauri/src/domain/mod.rs


6. scripts/codegen-types.ts ⚙️ Configuration changes +1/-0

Add Project.ts to required types list for codegen

scripts/codegen-types.ts


7. src/shared/types/Project.ts ✨ Enhancement +3/-0

Auto-generated TypeScript type definition for Project

src/shared/types/Project.ts


8. src/shared/types/index.ts ✨ Enhancement +1/-0

Export Project type from barrel index

src/shared/types/index.ts


9. CHANGELOG.md 📝 Documentation +1/-0

Document projects bounded context addition and implementation details

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 11, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. ProjectRepository includes find_by_path ✓ Resolved 📎 Requirement gap ⚙ Maintainability
Description
The new ProjectRepository port exposes find_by_path, but the compliance checklist requires the
Sprint 2 port to include only list, get, and create. This expands the port surface area beyond
the approved contract and can force adapter implementations to support extra behavior prematurely.
Code

src-tauri/src/domain/projects/ports.rs[31]

+    async fn find_by_path(&self, path: &str) -> Result<Option<Project>, DomainError>;
Evidence
PR Compliance ID 3 requires ProjectRepository to expose only list, get, and create. The
added trait includes find_by_path, which violates that constraint.

Define ProjectRepository port with async list/get/create only
src-tauri/src/domain/projects/ports.rs[27-33]

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

## Issue description
`ProjectRepository` currently includes an extra method `find_by_path`, but the Sprint 2 compliance requirement for this PR mandates the port exposes only `list`, `get`, and `create`.
## Issue Context
The compliance checklist for this PR explicitly constrains the repository port surface area (no additional methods beyond `list/get/create`). The current `create_project` implementation depends on `find_by_path`, so removing it will require adjusting the uniqueness check strategy.
## Fix Focus Areas
- src-tauri/src/domain/projects/ports.rs[27-33]
- src-tauri/src/domain/projects/service.rs[119-139]
- src-tauri/src/domain/projects/service.rs[187-222]

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



Remediation recommended

2. PATH_MAX uses char count ✓ Resolved 🐞 Bug ≡ Correctness
Description
parse_absolute_path enforces PATH_MAX_CHARS using a Unicode chars() count even though it is
documented as Linux PATH_MAX (a byte limit), so multi-byte UTF-8 paths can exceed the intended
bound and only fail later when adapters hit OS/db limits. This makes the domain-layer validation
inconsistent with its own contract and can produce late, harder-to-diagnose failures.
Code

src-tauri/src/domain/projects/service.rs[R64-69]

+    // Short-circuit at PATH_MAX_CHARS + 1: bounds an attacker-supplied path
+    // before `Path::components()` walks and `PathBuf` allocates.
+    if trimmed.chars().take(PATH_MAX_CHARS + 1).count() > PATH_MAX_CHARS {
+        return Err(DomainError::Validation(format!(
+            "path exceeds {PATH_MAX_CHARS} chars"
+        )));
Evidence
The code explicitly documents the constant as Linux PATH_MAX but implements the limit using
chars() counting, which is not the same unit as a byte-based limit; this is visible directly in
the constant comment and the conditional used for validation.

src-tauri/src/domain/projects/service.rs[21-70]

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

## Issue description
`PATH_MAX_CHARS = 4096` is documented as matching Linux `PATH_MAX`, but the actual check uses `trimmed.chars().take(...).count()`, which counts Unicode scalar values rather than UTF-8 bytes.
## Issue Context
- This validation is intended to short-circuit overly long attacker-supplied inputs before deeper processing.
- As written, a path containing multi-byte characters can remain under 4096 *chars* while exceeding 4096 *bytes*, diverging from the documented `PATH_MAX` contract.
## Fix Focus Areas
- src-tauri/src/domain/projects/service.rs[21-70]
## Suggested change
- Either (preferred) rename the constant to `PATH_MAX_BYTES` and compare via `trimmed.len()` (UTF-8 bytes), or update the documentation/error message to explicitly state the limit is in characters (and accept the mismatch knowingly).
- Keep the existing `take(...).count()` pattern for the *name* limit if you still want that limit to be character-based.

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


3. Windows paths untested ✓ Resolved 🐞 Bug ☼ Reliability
Description
canonicalize_path implements Windows-specific component handling (Component::Prefix, root/prefix
preservation, and pop_if_normal guarding prefixes), but the unit tests only cover POSIX-style
/... inputs. This leaves Windows drive/UNC/verbatim path normalization behavior unverified and
prone to regressions.
Code

src-tauri/src/domain/projects/service.rs[R105-114]

+    let mut out = PathBuf::new();
+    for component in parsed.components() {
+        match component {
+            Component::Prefix(p) => out.push(p.as_os_str()),
+            Component::RootDir => out.push(Component::RootDir.as_os_str()),
+            Component::CurDir => {} // drop `.`
+            Component::ParentDir => pop_if_normal(&mut out),
+            Component::Normal(n) => out.push(n),
+        }
+    }
Evidence
The implementation includes explicit Windows-prefix handling and prefix-preservation commentary, but
the only canonicalize_path test table uses POSIX paths; therefore Windows-specific behavior is
currently not exercised by tests.

src-tauri/src/domain/projects/service.rs[86-114]
src-tauri/src/domain/projects/service.rs[441-452]

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

## Issue description
`canonicalize_path` contains explicit Windows-aware logic, but the test suite exercises only POSIX path forms. Cross-platform correctness is therefore unproven.
## Issue Context
- The implementation handles `Component::Prefix` and has explicit docs about not dropping Windows prefixes (drive letters, `\\?\` verbatim prefixes, UNC).
- Current `canonicalize_path_normalizes_lexical_forms` cases are all `/...`.
## Fix Focus Areas
- src-tauri/src/domain/projects/service.rs[86-117]
- src-tauri/src/domain/projects/service.rs[441-459]
## Suggested change
- Add `#[cfg(windows)]` unit tests that cover at least:
- `C:\\foo\\..\\bar` -> `C:\\bar`
- `C:\\..\\..` collapses to `C:\\` (does not escape)
- UNC: `\\\\server\\share\\dir\\..` -> `\\\\server\\share`
- Verbatim disk prefix: `\\\\?\\C:\\foo\\..\\bar` preserves the prefix
- Keep existing POSIX tests under `#[cfg(not(windows))]` or leave them as-is and add Windows-only cases in a separate test function.

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


Grey Divider

Qodo Logo

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8e68aa3-ccd3-4ac7-b067-f8110d2cf714

📥 Commits

Reviewing files that changed from the base of the PR and between 4385ca6 and 18dcd1f.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src-tauri/src/domain/projects/ports.rs
  • src-tauri/src/domain/projects/service.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src-tauri/src/domain/projects/service.rs

📝 Walkthrough

Walkthrough

Adds a new projects bounded context: Project domain model, an async ProjectRepository port, domain service functions (list_projects, create_project) with lexical path canonicalization and name validation, in-memory tests for many edge cases, and TypeScript type generation plus codegen verification.

Changes

Projects Domain Context

Layer / File(s) Summary
Domain Module Structure
src-tauri/src/domain/mod.rs, src-tauri/src/domain/projects/mod.rs
Module index exports projects; projects re-exports model, ports, and service.
Project Domain Entity
src-tauri/src/domain/projects/model.rs
Project struct with id, path, name, optional vcs/settings_json, RFC3339 created_at, and Project::new(path, name) producing a draft (id = 0). Tests for serde, defaults, and Send/Sync.
ProjectRepository Persistence Port
src-tauri/src/domain/projects/ports.rs
Async ProjectRepository: Send + Sync trait with list(), get(id), and create(&Project) methods returning Result<_, DomainError>. Test stub verifies trait-object Send/Sync.
Domain Service: canonicalize & validate
src-tauri/src/domain/projects/service.rs
list_projects(repo) and create_project(repo, path, name) implement lexical-only canonicalize_path (absolute paths, ./.. resolution without escaping root, separator collapse, Windows prefix handling), validate_name (trim, non-empty, max chars, forbid control and bidi override chars), duplicate pre-check via repo.list(), and error mapping (Validation, Conflict). Extensive in-memory tests cover creation, duplicates, validation bounds, error propagation, and canonicalization edge cases.
TypeScript Codegen & Verification
scripts/codegen-types.ts, src/shared/types/Project.ts, src/shared/types/index.ts
Adds generated Project.ts; codegen verification now requires it; barrel index.ts re-exports the new type.
Changelog
CHANGELOG.md
Documents the new projects bounded context and associated artifacts.

Sequence Diagram

sequenceDiagram
  participant Client
  participant create_project
  participant canonicalize_path
  participant validate_name
  participant ProjectRepository
  participant repo_create
  Client->>create_project: path, name
  create_project->>canonicalize_path: path
  canonicalize_path-->>create_project: canonical_path
  create_project->>validate_name: name
  validate_name-->>create_project: validated_name
  create_project->>ProjectRepository: list() (pre-check for canonical path)
  ProjectRepository-->>create_project: Vec~Project~
  alt conflict detected
    create_project-->>Client: DomainError::Conflict
  else path is unique
    create_project->>repo_create: create(Project { canonical_path, validated_name, ... })
    repo_create-->>create_project: Result~Project~
    create_project-->>Client: Project with assigned id
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • mpiton/forgent#18: Adds shared DomainError types used by the new projects service.
  • mpiton/forgent#14: Changes to the codegen pipeline and script edits touching scripts/codegen-types.ts.
  • mpiton/forgent#5: Earlier domain module scaffolding and exports that the new projects module follows.

Poem

🐰 A new context springs to life,
Paths trimmed and names kept bright,
Canonical steps, no filesystem fright,
Projects lined up in orderly sight,
Hop, test, and ship — the domain feels right!

🚥 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 Title clearly identifies the main change: adding a projects bounded context domain module, matching the actual file additions and structure introduced in the PR.
Linked Issues check ✅ Passed All coding requirements from issue #72 are met: Project model with required fields and TS export, ProjectRepository async trait with list/get/create, service functions with validation and uniqueness checks, domain purity maintained, tests added, coverage ≥90%.
Out of Scope Changes check ✅ Passed All changes are within scope of issue #72: domain/projects module structure, model definition, ports, service logic, and TypeScript codegen. IPC/adapter wiring and libsql implementation correctly marked as out of scope.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 feat/t-204-projects-domain

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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 11, 2026

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing feat/t-204-projects-domain (18dcd1f) with main (c53fb82)

Open in CodSpeed

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

No issues found across 9 files

Comment thread src-tauri/src/domain/projects/ports.rs Outdated
- Drop `find_by_path` from `ProjectRepository` to match issue #72 exactly
  (`list` / `get` / `create` only). `create_project` now scans `list()`
  for the duplicate canonical-path pre-check; the libsql adapter's
  `UNIQUE` constraint on `projects.path` stays the real enforcement.
- Rename `PATH_MAX_CHARS` to `PATH_MAX_BYTES` and check `str::len()`
  instead of `chars().count()` so multi-byte paths cannot slip under the
  documented Linux PATH_MAX byte budget. Added a regression test using
  `é` repeated 2049 times.
- Add a `#[cfg(windows)]` table for `canonicalize_path` covering
  `C:\foo\..\bar`, `C:\..`, mixed `/` and `\`, and `\\?\C:\…\..\bar`
  so the Windows leg of the Tauri build matrix exercises the
  `Component::Prefix` arms.
@mpiton mpiton merged commit e27daea into main May 11, 2026
16 checks passed
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.

T-204: Projects domain — model + ports + service (CRUD)

1 participant