|
| 1 | +## ADDED Requirements |
| 2 | + |
| 3 | +### Requirement: Fruit entity storage |
| 4 | +The system SHALL persist Fruit entities to Redis using the key pattern `cf:u:{userId}:fruit:{fruitId}` (Hash) and maintain a sorted index `cf:u:{userId}:seed:{seedId}:fruits` (ZSet, score=createdAt) for efficient per-seed queries. |
| 5 | + |
| 6 | +#### Scenario: Save a new fruit |
| 7 | +- **WHEN** Agent calls `save_fruits` MCP tool with a valid seedId and fruit payload |
| 8 | +- **THEN** system stores each fruit as a Redis Hash under the user-scoped key |
| 9 | +- **AND** system adds the fruitId to the seed's ZSet index with createdAt as score |
| 10 | +- **AND** system atomically increments the seed's `fruitCount` field |
| 11 | + |
| 12 | +#### Scenario: Query fruits by seed |
| 13 | +- **WHEN** client sends `GET /api/seeds/:seedId/fruits` with a valid X-User-Id header |
| 14 | +- **THEN** system returns all fruits belonging to that seed, ordered by createdAt descending |
| 15 | +- **AND** response contains id, seedId, parentFruitId, generation, status, preview, payload, context, mutation fields |
| 16 | + |
| 17 | +### Requirement: Fruit state machine |
| 18 | +The system SHALL enforce the following state transitions and reject any invalid transition with HTTP 400 / MCP error: |
| 19 | + |
| 20 | +``` |
| 21 | +generated → picked (user picks fruit) |
| 22 | +generated → rejected (user composts fruit) |
| 23 | +picked → published (user publishes, terminal state) |
| 24 | +picked → generated (user revokes pick) |
| 25 | +rejected → generated (user revokes compost) |
| 26 | +published → [no transitions allowed] |
| 27 | +``` |
| 28 | + |
| 29 | +#### Scenario: Valid state transition via HTTP |
| 30 | +- **WHEN** client sends `PATCH /api/fruits/:fruitId/pickup` for a fruit with status `generated` |
| 31 | +- **THEN** system updates the fruit status to `picked` and returns `{ code: 0, data: { id, status: "picked" } }` |
| 32 | + |
| 33 | +#### Scenario: Invalid state transition rejected |
| 34 | +- **WHEN** client sends `PATCH /api/fruits/:fruitId/pickup` for a fruit with status `published` |
| 35 | +- **THEN** system returns HTTP 400 with a descriptive error message |
| 36 | + |
| 37 | +#### Scenario: Publish fruit (terminal state) |
| 38 | +- **WHEN** client sends `PATCH /api/fruits/:fruitId/publish` for a fruit with status `picked` |
| 39 | +- **THEN** system updates status to `published` and returns the updated fruit |
| 40 | +- **AND** no further status transitions are permitted for this fruit |
| 41 | + |
| 42 | +#### Scenario: Compost fruit |
| 43 | +- **WHEN** client sends `PATCH /api/fruits/:fruitId/compost` for a fruit with status `generated` |
| 44 | +- **THEN** system updates status to `rejected` and returns `{ code: 0, data: { id, status: "rejected" } }` |
| 45 | + |
| 46 | +#### Scenario: Revoke compost |
| 47 | +- **WHEN** client sends a status update to `generated` for a fruit with status `rejected` |
| 48 | +- **THEN** system accepts the transition and updates the status |
| 49 | + |
| 50 | +### Requirement: In-place fruit pruning |
| 51 | +The system SHALL allow users to directly edit the `payload` (Markdown string) and optionally `preview` fields of any fruit without creating a new iteration branch. The `payload` field is a plain Markdown string; the system SHALL store and return it verbatim without parsing or validating its structure. |
| 52 | + |
| 53 | +#### Scenario: Prune fruit Markdown content |
| 54 | +- **WHEN** client sends `PUT /api/fruits/:fruitId/payload` with an updated `payload` (Markdown string) and/or `preview` body |
| 55 | +- **THEN** system overwrites the stored payload string and/or preview fields and updates `updatedAt` |
| 56 | +- **AND** fruit status, generation, parentFruitId, and context remain unchanged |
| 57 | + |
| 58 | +#### Scenario: Payload stored and returned verbatim |
| 59 | +- **WHEN** Agent saves a fruit with a Markdown payload containing platform-specific formatting |
| 60 | +- **THEN** system stores the string as-is and returns it unchanged on subsequent reads |
| 61 | +- **AND** system does NOT attempt to parse, validate, or transform the Markdown content |
| 62 | + |
| 63 | +### Requirement: User data isolation |
| 64 | +The system SHALL enforce that all fruit operations are scoped to the authenticated user via the `X-User-Id` header, and SHALL reject requests that attempt to access fruits belonging to another user. |
| 65 | + |
| 66 | +#### Scenario: User can only access own fruits |
| 67 | +- **WHEN** client sends any fruit API request with X-User-Id header |
| 68 | +- **THEN** system only reads/writes Redis keys scoped to that userId |
| 69 | +- **AND** if the fruitId does not exist under that userId, system returns HTTP 404 |
| 70 | + |
| 71 | +#### Scenario: Missing user header rejected |
| 72 | +- **WHEN** client sends a fruit API request without X-User-Id header |
| 73 | +- **THEN** system returns HTTP 401 with message "X-User-Id header is required" |
| 74 | + |
| 75 | +### Requirement: Fruit ID format |
| 76 | +The system SHALL generate fruit IDs in the format `fruit_{YYYYMMDD}_{nanoid(8)}` to ensure global uniqueness and time-sortability. |
| 77 | + |
| 78 | +#### Scenario: New fruit ID generation |
| 79 | +- **WHEN** a new fruit is created via `save_fruits` MCP tool |
| 80 | +- **THEN** each fruit receives a unique ID matching the pattern `fruit_YYYYMMDD_XXXXXXXX` |
0 commit comments