feat(domain): T-204 add projects bounded context (closes #72)#96
Conversation
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).
Review Summary by QodoAdd projects bounded context (T-204) for multi-project tabs support
WalkthroughsDescription• 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 Diagramflowchart 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"]
File Changes1. src-tauri/src/domain/projects/mod.rs
|
Code Review by Qodo
1.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a new ChangesProjects Domain Context
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
- 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.
Summary
domain/projects/bounded context (model + ports + service) for Sprint 2, PRD F-001 multi-project tabs — mirrors thedomain/tasks/shape exactly.Project { id: i64, path, name, vcs, created_at, settings_json }matches the liveV001__initial.sqlschema (the stale ARCHI §8 UUID-v7 spec is left for a separate cleanup sincetasks.project_idis alreadyi64).ProjectRepository::{list, get, find_by_path, create}— noupdate/deletein Sprint 2 per the issue.find_by_pathlives on the port so the libsql adapter (Sprint 2 T-205) can use theUNIQUEindex instead of scanninglist().Why
Closes #72. Unblocks the libsql adapter (T-205),
projects_list/projects_createTauri 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— registerpub mod projects;(alphabetical).scripts/codegen-types.ts— REQUIRED list gains\"Project.ts\".src/shared/types/Project.ts+src/shared/types/index.ts— regenerated bypnpm run codegen.CHANGELOG.md—[Unreleased] / Addedentry.Security / robustness hardening (from in-PR review)
validate_name+parse_absolute_pathreject control chars (C0 / C1 / NUL) and Unicode bidi overrides (U+202A..U+202E, U+2066..U+2069). Prevents\\0-inducedCStringpanics in the FS adapter, log-injection via\\n/\\r, and RTL spoofing in the kanban / project picker.PATH_MAX_CHARS = 4096short-circuit bounds attacker-supplied paths beforePath::components()walks.pop_if_normalhelper refuses to escape above the root (/foo/../..→/) and refuses to drop a WindowsPrefix(C:,\\\\?\\C:, UNC).Project::new(path, name)takes only business fields —id = 0sentinel stays internal, mirroringTask::new.InMemoryProjectRepo(no lock-ordering hazard) withwith_failing_find_by_pathbuilder so both?branches increate_projectget distinct coverage.ports.rsdoc records the TOCTOU contract for T-205.Testing
cargo test --workspace: 211/211 pass (24 new indomain::projects).cargo clippy --workspace --all-targets -- -D warnings: clean.cargo fmt --check: clean.cargo test --test architecture: 2/2 — notokio/libsql/tauriimports leaked into the new context.pnpm exec tsc -b+pnpm exec oxlint .: clean.model.rs100%,service.rs94.75% lines / 97.02% regions (above the 90% domain gate from CLAUDE.md).ports.rs38.46% — same shape astasks/ports.rspre-adapter; full coverage lands with T-205 libsql wiring.Acceptance criteria
tokio, nolibsql, notauriimports — T-131 architecture test stays green.service.rswith in-memory repo: list empty, create + list returns it, duplicate path →DomainError::Conflict, blank name →DomainError::Validation.Projectstruct exported (src/shared/types/Project.ts).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).i64reality).Test plan
pnpm run codegenproduces a clean diff (no drift) — verified by.github/workflows/ci.ymllines 126-139.Project.tsshape matches the Rust struct.Summary by CodeRabbit
New Features
Chores