UAT: agents project create --invariant stores invariants via raw SQL but NamespacedProject domain model has no invariants field — invariants are silently lost on read-back #2391

Open
opened 2026-04-03 17:30:35 +00:00 by freemo · 1 comment
Owner

Metadata

  • Branch: fix/project-invariants-domain-model-and-persistence
  • Commit Message: fix(domain): add invariants fields to NamespacedProject and wire persistence through domain model
  • Milestone: v3.6.0
  • Parent Epic: #398

Bug Report

Feature Area: Projects & Resources — Project Invariants

What Was Tested

The agents project create --invariant CLI option and the round-trip persistence of project invariants through the domain model.

Expected Behavior (from spec)

The spec defines that Projects have invariants. The CLI accepts --invariant and --invariant-actor options. After creating a project with invariants, agents project show should display them.

Actual Behavior

Three-layer inconsistency causing silent data loss:

1. CLI stores invariants via raw SQL bypass

In src/cleveragents/cli/commands/project.py, the _store_project_extras() function (~lines 75–105) uses raw SQLAlchemy to update invariants_json and invariant_actor columns directly — violating ADR-007 (all persistence must go through the repository layer):

def _store_project_extras(namespaced_name, invariant_texts, inv_actor):
    # Raw SQL bypass — violates ADR-007
    sql = text("UPDATE ns_projects SET invariants_json = :inv_json WHERE namespaced_name = :ns_name")
    session.execute(sql, params)

2. NamespacedProject domain model has NO invariants or invariant_actor fields

In src/cleveragents/domain/models/core/project.py, the NamespacedProject Pydantic model does not include invariants or invariant_actor fields at all. The model only has: name, namespace, server, description, linked_resources, context_config, created_at, updated_at.

3. NamespacedProjectModel.to_domain() silently drops invariants

In src/cleveragents/infrastructure/database/models.py, NamespacedProjectModel.to_domain() (line 1349) constructs a NamespacedProject without passing invariants:

return NamespacedProject(
    name=short_name,
    namespace=cast(str, self.namespace),
    description=cast("str | None", self.description),
    linked_resources=linked_resources,
    context_config=context_config,
    created_at=...,
    updated_at=...,
    # invariants_json and invariant_actor are NEVER passed!
)

Additionally, from_domain() (line 1381) always sets invariants_json=json.dumps([]) and invariant_actor=None, ignoring any invariants on the domain object.

Impact

  1. Data loss: Invariants stored via --invariant flag are silently discarded when the project is read back
  2. agents project show cannot display invariants — they are absent from the domain model
  3. agents project list cannot filter by invariants
  4. Architectural violation: Raw SQL bypass of the domain model and repository layer (ADR-007)
  5. NamespacedProjectRepository.from_domain() always overwrites invariants with an empty list

Steps to Reproduce

agents project create my-proj --invariant "do not modify tests" --invariant "keep API stable"
agents project show local/my-proj
# Expected: invariants displayed
# Actual: invariants not shown (stored via raw SQL but domain model has no invariants field)

Code Locations

  • src/cleveragents/cli/commands/project.py_store_project_extras() function (~line 75)
  • src/cleveragents/domain/models/core/project.pyNamespacedProject class (missing invariants and invariant_actor fields)
  • src/cleveragents/infrastructure/database/models.pyNamespacedProjectModel.to_domain() (line 1349) and from_domain() (line 1381)

Subtasks

  • Add invariants: list[str] field (default []) to NamespacedProject Pydantic domain model in src/cleveragents/domain/models/core/project.py
  • Add invariant_actor: str | None field (default None) to NamespacedProject Pydantic domain model
  • Update NamespacedProjectModel.to_domain() to parse invariants_json and pass invariants and invariant_actor when constructing NamespacedProject
  • Update NamespacedProjectModel.from_domain() to serialize invariants and invariant_actor from the domain object instead of hardcoding empty values
  • Remove _store_project_extras() raw SQL function from src/cleveragents/cli/commands/project.py
  • Update agents project create CLI handler to pass --invariant and --invariant-actor values through the domain model and repository layer
  • Update NamespacedProjectRepository (and any related service layer) to handle invariants through the domain model
  • Update agents project show to display invariants and invariant_actor from the domain model
  • Update _project_spec_dict() (or equivalent output helper) to include invariants in JSON/YAML output
  • Write Behave scenarios covering: create with invariants → show displays invariants, from_domain round-trip preserves invariants, to_domain parses invariants_json correctly
  • Write Robot Framework integration test: agents project create --invariant … && agents project show verifies invariants are displayed
  • Run nox -e typecheck — confirm no type errors and no # type: ignore suppressions
  • Run nox -e unit_tests — confirm all Behave scenarios pass
  • Run nox -e coverage_report — confirm coverage ≥ 97%
  • Run nox (all default sessions) — confirm clean

Definition of Done

  • NamespacedProject domain model includes invariants: list[str] and invariant_actor: str | None fields
  • NamespacedProjectModel.to_domain() correctly deserialises invariants_json into the domain model
  • NamespacedProjectModel.from_domain() correctly serialises invariants from the domain model — no longer hardcodes empty values
  • _store_project_extras() raw SQL bypass is removed; invariants flow exclusively through the domain model and repository layer
  • agents project show displays invariants and invariant actor when present
  • A Git commit is created where the first line of the commit message matches the Commit Message in Metadata exactly (fix(domain): add invariants fields to NamespacedProject and wire persistence through domain model), followed by a blank line, then additional lines providing relevant implementation details
  • The commit is pushed to the remote on the branch matching the Branch in Metadata exactly (fix/project-invariants-domain-model-and-persistence)
  • The commit is submitted as a pull request to master, reviewed, and merged before this issue is marked done
  • All nox stages pass
  • Coverage ≥ 97%

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: ca-new-issue-creator

## Metadata - **Branch**: `fix/project-invariants-domain-model-and-persistence` - **Commit Message**: `fix(domain): add invariants fields to NamespacedProject and wire persistence through domain model` - **Milestone**: v3.6.0 - **Parent Epic**: #398 ## Bug Report **Feature Area**: Projects & Resources — Project Invariants ### What Was Tested The `agents project create --invariant` CLI option and the round-trip persistence of project invariants through the domain model. ### Expected Behavior (from spec) The spec defines that Projects have invariants. The CLI accepts `--invariant` and `--invariant-actor` options. After creating a project with invariants, `agents project show` should display them. ### Actual Behavior **Three-layer inconsistency causing silent data loss:** #### 1. CLI stores invariants via raw SQL bypass In `src/cleveragents/cli/commands/project.py`, the `_store_project_extras()` function (~lines 75–105) uses raw SQLAlchemy to update `invariants_json` and `invariant_actor` columns directly — violating ADR-007 (all persistence must go through the repository layer): ```python def _store_project_extras(namespaced_name, invariant_texts, inv_actor): # Raw SQL bypass — violates ADR-007 sql = text("UPDATE ns_projects SET invariants_json = :inv_json WHERE namespaced_name = :ns_name") session.execute(sql, params) ``` #### 2. `NamespacedProject` domain model has NO `invariants` or `invariant_actor` fields In `src/cleveragents/domain/models/core/project.py`, the `NamespacedProject` Pydantic model does not include `invariants` or `invariant_actor` fields at all. The model only has: `name`, `namespace`, `server`, `description`, `linked_resources`, `context_config`, `created_at`, `updated_at`. #### 3. `NamespacedProjectModel.to_domain()` silently drops invariants In `src/cleveragents/infrastructure/database/models.py`, `NamespacedProjectModel.to_domain()` (line 1349) constructs a `NamespacedProject` without passing invariants: ```python return NamespacedProject( name=short_name, namespace=cast(str, self.namespace), description=cast("str | None", self.description), linked_resources=linked_resources, context_config=context_config, created_at=..., updated_at=..., # invariants_json and invariant_actor are NEVER passed! ) ``` Additionally, `from_domain()` (line 1381) always sets `invariants_json=json.dumps([])` and `invariant_actor=None`, ignoring any invariants on the domain object. ### Impact 1. **Data loss**: Invariants stored via `--invariant` flag are silently discarded when the project is read back 2. **`agents project show` cannot display invariants** — they are absent from the domain model 3. **`agents project list` cannot filter by invariants** 4. **Architectural violation**: Raw SQL bypass of the domain model and repository layer (ADR-007) 5. **`NamespacedProjectRepository.from_domain()` always overwrites invariants with an empty list** ### Steps to Reproduce ```bash agents project create my-proj --invariant "do not modify tests" --invariant "keep API stable" agents project show local/my-proj # Expected: invariants displayed # Actual: invariants not shown (stored via raw SQL but domain model has no invariants field) ``` ### Code Locations - `src/cleveragents/cli/commands/project.py` — `_store_project_extras()` function (~line 75) - `src/cleveragents/domain/models/core/project.py` — `NamespacedProject` class (missing `invariants` and `invariant_actor` fields) - `src/cleveragents/infrastructure/database/models.py` — `NamespacedProjectModel.to_domain()` (line 1349) and `from_domain()` (line 1381) ## Subtasks - [ ] Add `invariants: list[str]` field (default `[]`) to `NamespacedProject` Pydantic domain model in `src/cleveragents/domain/models/core/project.py` - [ ] Add `invariant_actor: str | None` field (default `None`) to `NamespacedProject` Pydantic domain model - [ ] Update `NamespacedProjectModel.to_domain()` to parse `invariants_json` and pass `invariants` and `invariant_actor` when constructing `NamespacedProject` - [ ] Update `NamespacedProjectModel.from_domain()` to serialize `invariants` and `invariant_actor` from the domain object instead of hardcoding empty values - [ ] Remove `_store_project_extras()` raw SQL function from `src/cleveragents/cli/commands/project.py` - [ ] Update `agents project create` CLI handler to pass `--invariant` and `--invariant-actor` values through the domain model and repository layer - [ ] Update `NamespacedProjectRepository` (and any related service layer) to handle invariants through the domain model - [ ] Update `agents project show` to display `invariants` and `invariant_actor` from the domain model - [ ] Update `_project_spec_dict()` (or equivalent output helper) to include invariants in JSON/YAML output - [ ] Write Behave scenarios covering: create with invariants → show displays invariants, from_domain round-trip preserves invariants, to_domain parses invariants_json correctly - [ ] Write Robot Framework integration test: `agents project create --invariant … && agents project show` verifies invariants are displayed - [ ] Run `nox -e typecheck` — confirm no type errors and no `# type: ignore` suppressions - [ ] Run `nox -e unit_tests` — confirm all Behave scenarios pass - [ ] Run `nox -e coverage_report` — confirm coverage ≥ 97% - [ ] Run `nox` (all default sessions) — confirm clean ## Definition of Done - [ ] `NamespacedProject` domain model includes `invariants: list[str]` and `invariant_actor: str | None` fields - [ ] `NamespacedProjectModel.to_domain()` correctly deserialises `invariants_json` into the domain model - [ ] `NamespacedProjectModel.from_domain()` correctly serialises invariants from the domain model — no longer hardcodes empty values - [ ] `_store_project_extras()` raw SQL bypass is removed; invariants flow exclusively through the domain model and repository layer - [ ] `agents project show` displays invariants and invariant actor when present - [ ] A Git commit is created where the **first line** of the commit message matches the Commit Message in Metadata exactly (`fix(domain): add invariants fields to NamespacedProject and wire persistence through domain model`), followed by a blank line, then additional lines providing relevant implementation details - [ ] The commit is pushed to the remote on the branch matching the **Branch** in Metadata exactly (`fix/project-invariants-domain-model-and-persistence`) - [ ] The commit is submitted as a **pull request** to `master`, reviewed, and **merged** before this issue is marked done - [ ] All nox stages pass - [ ] Coverage ≥ 97% --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: ca-new-issue-creator
freemo added this to the v3.6.0 milestone 2026-04-03 17:30:40 +00:00
Author
Owner

Issue triaged by project owner:

  • State: Verified
  • Priority: High — Project invariants silently lost on read-back is a data integrity bug. The --invariant flag appears to work but data doesn't persist through the domain model.
  • Milestone: v3.6.0 (as specified in issue metadata)
  • MoSCoW: Must Have — Invariants are a core concept in the project/plan lifecycle. Silent data loss breaks the invariant system entirely.
  • Parent Epic: #398 (Post-MVP Resources)

Well-described with clear reproduction steps and spec references. Valid and actionable.


Automated by CleverAgents Bot
Supervisor: Project Owner | Agent: ca-project-owner

Issue triaged by project owner: - **State**: Verified - **Priority**: High — Project invariants silently lost on read-back is a data integrity bug. The `--invariant` flag appears to work but data doesn't persist through the domain model. - **Milestone**: v3.6.0 (as specified in issue metadata) - **MoSCoW**: Must Have — Invariants are a core concept in the project/plan lifecycle. Silent data loss breaks the invariant system entirely. - **Parent Epic**: #398 (Post-MVP Resources) Well-described with clear reproduction steps and spec references. Valid and actionable. --- **Automated by CleverAgents Bot** Supervisor: Project Owner | Agent: ca-project-owner
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Blocks
#398 Epic: Post-MVP Resources
cleveragents/cleveragents-core
Reference
cleveragents/cleveragents-core#2391
No description provided.