# Spec Driven Development — Chapter-23: 10.5 - Editing a Task in Practice: the Whole Loop, File by File - **Source**: /library/Spec Driven Development/source-file.pdf - **PDF pages**: 415–473 - **Pages without text**: none --- 10.5 - Editing a Task in Practice: the Whole Loop, File by File How to Read This Chapter Chapter 10 followed the fifth and last lap of the cycle with the spotlight on implement . Here you see the whole lap, exactly as it was recorded in the example app's repository, in the 005-editar- tarefa feature. This is the lap where implement discovers something no earlier artifact had noticed. The spec said the edited title should stay "not repeated, on the same criterion as creation." It seemed enough: checklist saw no gap, analyze came out clean, the tests derived from the tasks went green. It was only in the manual validation of the quickstart, editing only the description of a task, that the defect showed up: the naive rule treated keeping your own title as a duplicate, and the edit failed. The fix was not a patch in the code; it went back to the source, the spec gained a fifth clarification and FR-003 was rewritten, and only then did the code change. It is exactly the pedagogical point of Chapter 10: implement is part of the loop, and what it discovers feeds back into the artifacts, instead of becoming a hidden adjustment. As in the earlier laps, the artifacts appear as markdown (with headings demoted) and the code in blocks with a sentence of context before each one. Several files are modifications of files you have already seen; when that is the case, the text points out what changed. The Input: the /speckit-specify Prompt "Edit an existing task in the personal to-do list. The person fixes the text of a task they already created. Out of scope for this feature: an edit history, bulk editing and any new field beyond the ones the task already has." What specify and clarify Returned: spec.md The spec below is the final version, already with the fifth clarification (Q5) and FR-003 rewritten. Notice the note at the end of Q5: it records that the refinement was discovered in implement and that the previous wording ("on the same criterion as creation") was ambiguous. The spec does not hide the fix; it documents it. Feature Specification: Edit a task Feature Branch: 005-editar-tarefa Created: 2026-06-27 Status: Draft Input: User description: "Edit an existing task in the personal to- do list. The person fixes the text of a task they already created. Out of scope for this feature: an edit history, bulk editing and any new field beyond the ones the task already has." Clarifications Session 2026-06-27 Q: Which fields does editing change (just the title, or title and description)? → A: Title and description. It is the textual content the task already has (introduced in 001); editing is changing that text, without inventing a new field (YAGNI). Q: Do the title rules apply again on editing? → A: Yes. The title stays required (it cannot be empty) and not repeated among the tasks, on the same criterion as creation (001). Editing does not loosen the creation rules. Q: Does editing change the task's position in the list? → A: No. Editing changes the text in place; createdAt does not change and creation order is preserved. The repository already updates in place. Q: Editing a task that no longer exists (removed in another tab, for example)? → A: A predictable error (error as value), not an exception that crashes the app, on the same pattern as completing/deleting. Q: Does the "title not repeated" rule consider the task itself being edited? → A: No. Uniqueness is against the other tasks. Saving an edit keeping the same title (for example, when fixing only the description) is NOT a duplicate. (Refinement discovered in implement : the previous wording, "on the same criterion as creation," was ambiguous on this point and made editing only the description fail as a duplicate.) User Scenarios & Testing (mandatory) User Story 1 - Fix the text of a task (Priority: P1) The person notices a typo or wants to detail a task they already created more fully. They open the task, adjust the title and/or the description and save. The list now shows the corrected text, in the same position. Acceptance Scenarios: 1. Given an existing task, When the person changes the title and saves, Then the task now displays the new title, in the same position, and the change survives reopening the app. 2. Given an existing task, When the person changes only the description and saves (keeping the same title), Then the new description is displayed, the title stays intact and the operation succeeds (keeping your own title is not a duplicate). Edge Cases Editing and leaving the title empty (or only spaces) is rejected as a predictable error: the required-title rule applies to editing too. Editing and adopting the title of another task is rejected as a duplicate; keeping your own title (editing only the description, for example) is allowed. Editing a task that no longer exists is handled as a predictable error (error as value), not as an exception. Requirements (mandatory) Functional Requirements FR-001: The system MUST allow editing the textual content (title and description) of an existing task. FR-002: The edited title MUST stay required: an empty title or one with only spaces is rejected, on the same criterion as creation (001). FR-003: The edited title MUST stay not repeated among the other tasks. Keeping the task-in-edit's own title does NOT count as a duplicate (it is the case of fixing only the description). FR-004: Editing a nonexistent task MUST return a predictable error (error as value), not throw an exception across layers. FR-005: Editing MUST change the task in place, without changing its position in the list or its createdAt . FR-006: The edit MUST persist: the corrected text survives closing and reopening the app. Key Entities Task: the already existing entity (001), with title (required, unique) and description (optional). Editing changes those two fields; it introduces no new field. Success Criteria (mandatory) SC-001: After editing a task's title and reopening the app, the task appears with the new title, in the same position. SC-002: Trying to save an edit with an empty title keeps the task unchanged and signals the error. SC-003: Trying to adopt an already-used title keeps the task unchanged and signals the duplicate error. Assumptions The feature extends those for creating/listing (001), completing/reopening (002), filtering (003) and deleting (004), already integrated on main ; it reuses the Task entity, the repository and the existing layered architecture. The To-Do constitution governs the feature: Simplicity/YAGNI, layered architecture, business logic isolated from the UI, error as value and TDD. Editing is deliberately minimal (clarification): no edit history, no bulk editing and no new field. If any of those is ever wanted, it becomes its own feature. The Completeness Check: checklists/requirements.md checklist here is instructive precisely because it did not catch the problem. The notes at the end record it frankly: the duplicate rule was reused from creation "on the same criterion," which "seemed enough on this pass." checklist checks form, and the form was good. The hole was semantic, and only the test would reveal it. Specification Quality Checklist: Edit a task Created: 2026-06-27 Feature: spec.md Content Quality No implementation details (languages, frameworks, APIs) Focused on user value and business needs Written for non-technical stakeholders All mandatory sections completed Requirement Completeness No [NEEDS CLARIFICATION] markers remain Requirements are testable and unambiguous Success criteria are measurable Success criteria are technology-agnostic All acceptance scenarios are defined Edge cases are identified Scope is clearly bounded Dependencies and assumptions identified Feature Readiness All functional requirements have clear acceptance criteria User scenarios cover primary flows Feature meets measurable outcomes defined in Success Criteria No implementation details leak into specification Notes checklist found no form gaps: the title rules and the minimal scope (no history, no bulk, no new field) were already pinned down in clarify . The duplicate rule was reused from creation (001) "on the same criterion," which seemed enough on this pass. What plan Decided: plan.md This lap's plan has one decision worth highlighting: no new method on the repository. getById and update have existed since 002, and update updates in place without reordering, exactly what editing needs. It is the architecture paying dividends: the fifth feature reuses the repository the second one designed. Implementation Plan: Edit a task Branch: 005-editar-tarefa | Spec: spec.md Input: Feature specification in specs/005-editar-tarefa/spec.md Summary Change the textual content (title and description) of an existing task, in place. The spec says what (fix the text, reapplying the title rules and preserving the position); this plan decides how: an EditTask use case in the domain that looks the task up by id, revalidates the title (required and not repeated), writes the changed version via the repository's update and returns Result (error as value for the nonexistent task and for the invalid title). The orchestration lives in the store and the editing in the view. No new method on the repository: getById and update have existed since 002, and update updates in place without reordering the list, exactly what editing needs. No new field enters the task. Technical Context Language/Version: TypeScript 5.x on Node 20+; target: browser (React web). Inherited from 001-004. Main dependencies: React 18, Vite, Zustand. No new dependencies (stack inherited from the 001 plan ). Persistence: the edit is written to localStorage behind TaskRepository.update , which replaces the item in place preserving the order. It is the same path completing/reopening (002) already uses. Testing: Vitest for the EditTask use case (domain, no DOM) and for the store action. The repository's update has had coverage since 002. Project type: single-page, single-user web application, no backend. How-Decisions (what the spec did not pin down) 1. Where editing lives: a new EditTask use case in the domain. It receives { id, title, description } , looks the task up by id; if it does not exist, it returns failure({ kind: "task-not-found" }) (FR- 004); otherwise it revalidates the title (required, FR-002; not repeated, FR-003) and, if valid, assembles the changed task and calls update . The view never talks straight to the repository. 2. Repository reuse: no new method. getById locates the task and update writes the changed version in place (FR-005), without touching createdAt or the order. Both have existed since 002. 3. The title rules: the same as creation (001), a non-empty title after trim and not repeated among the tasks. The revalidation lives in the use case, not the UI. 4. Where editing lives in the interface: in the view. Entering edit mode, filling the fields and saving is a UX gesture; the store only orchestrates: it triggers editTask and reloads the list. Constitution Check Principle How the plan meets it I. Layered Architecture The write sits behind the repository; the rule (does it exist? valid title? then update) sits in the domain use case; the view only collects the text and displays. Dependencies point toward the domain. II. Business Logic Isolated The decision "edit an existing task revalidating the title" is a pure use case, testable without a UI. Edit mode is UX, not a business decision. III. Error as Value A nonexistent task and an invalid title become Result.failure(...) ; no exception crosses layers. IV. TDD Tests of the use case (edits title and description; empty title becomes a failure; duplicate title becomes a failure; nonexistent becomes a failure; preserves the position) before implementation. V. Simplicity/YAGNI No edit history, no bulk editing, no new field, no new method on the repository. One use case, one store action, one edit mode. VI. Technology Agnosticism The spec names no stack; the stack (React/TS/Vite/Zustand) is inherited. The decisions here are about design (where editing lives, who revalidates), not new technology. Result: PASS. No violations; no rejected draft in this feature. Traceability clarify/spec → plan Decision (clarify/spec) Effect on the plan Edit title and description (no new field) EditTask receives { id, title, description } and rebuilds the task with the same fields. Title rules apply The use case revalidates required (FR-002) on editing and not repeated (FR-003), reusing the creation criterion. Position preserved update writes in place; createdAt and order intact (FR-005). Nonexistent is error as value EditTaskFailure includes { kind: "task-not-found" } , handled in the store. Project Structure (additions) src/ ├── domain/ │ ├── failures/edit-task-failure.ts # new: title-required | duplicat e-title | task-not-found │ └── usecases/edit-task.ts # new: makeEditTask(repository) └── presentation/ ├── store/task-store.ts # + editTask(id, title, descrip tion) └── components/TaskList.tsx # + inline edit mode test/ ├── domain/edit-task.test.ts # new: edits; empty/duplicate/n onexistent become failures; preserves position └── presentation/task-store.test.ts # + editTask reloads the list Phase 1 - Design Model of the operation in data-model.md. Manual validation in quickstart.md. The analyze Report: analyze-report.md The report has two parts that tell the whole story of the lap. The first is the normal scan: no conflict, cleared for implement . The second is an addendum, written later, that records the refinement discovered in the quickstart validation, the cause (an incomplete artifact, not a code slip), the cascade fix starting at the source, and the lesson: checklist and analyze check form and consistency, but they do not replace the test and the review, which catch what the wording left implicit. Consistency report ( analyze ): Edit a task Cross-scan of spec.md × plan.md × tasks.md for the 005-editar-tarefa feature, read together with the artifacts of the features already integrated on main (especially 001-criar-tarefa , where the title rules come from). analyze rewrites nothing: it produces the report below and recommends where to fix. Specification Analysis Report ID Category Severity Location Summary Reco A1 Underspecification (minor) LOW 005-editar- tarefa/spec.md FR-001, view The spec does not pin down the editing means (inline mode, modal, its own Acc is a dec pla no screen). Coverage Summary Total tasks: 8 Requirement coverage (≥1 task): 100% (FR-001..FR-006 mapped to T001-T006) Ambiguity count: 1 (LOW) Duplication count: 0 Conflicts: 0 Diagnosis Unlike 004, the editing wording does not conflict with any earlier feature: it reuses the title rules from creation (001) instead of reopening them, and the preserved-position invariant is the same one completing (002) already honors via update . The three artifacts (spec, plan, tasks) are coherent with each other: every FR has a task, every plan decision comes from a clarification, and the TDD order is respected. Next Actions Clean report (no conflicts): cleared for /speckit-implement . The only observation (A1) is a UI detail, resolved in implement ; it does not block. Addendum: refinement discovered in implement analyze came out clean and the tests derived from the tasks went green, but the quickstart validation (item 4, "editing only the description") exposed a real defect: FR-003 inherited from creation the "title not repeated" rule without saying whether the task itself counts. The naive implementation treated keeping the title as a duplicate, and editing only the description failed. The cause was not a code slip, but an incomplete artifact. The fix went in at the source: spec.md : a new clarification Q5, FR-003 rewritten ("among the other tasks; keeping your own title is not a duplicate") and Scenario 2 and the edge case adjusted. tasks.md : T002 gained the "keeping your own title returns success" case. Only then the code: a new test (red) and the other.id !== id filter in EditTask (green). Lesson recorded: checklist and analyze check form and consistency between artifacts; they do not replace the test and the review, which catch what the wording left implicit. The plan 's Model of the Operation: data-model.md As in the two earlier laps, editing adds neither entity nor field; it models an operation on the Task that already exists, reusing the port's getById and update . Data Model: Edit a task The feature adds neither entity nor field. It models an operation on the existing Task entity: changing the textual content in place. Operation Operation Input Output Predictable failures EditTask { id: string; title: string; description?: string } Result title-required (empty title after trim ), duplicate-title (title already used), task-not- found (nonexistent id) Repository reuse Method ( TaskRepository port) Role in editing getById(id): Promise Locates the task to edit; null becomes task-not- found . Has existed since 002. update(task): Promise Writes the changed version in place, without reordering the list. Has existed since 002. No new method is introduced: editing is, in the data layer, the same update that completing/reopening already uses. Preserved invariants createdAt and the position in the list do not change when editing (FR-005): the task is updated in place. The edited title obeys the same rules as creation: required (FR-002) and not repeated (FR-003). id and completed are not changed by editing: it touches only title and description . The Manual Validation: quickstart.md Notice item 4: "editing only the description." It was this step, in the manual validation, that exposed the FR-003 defect. It was here, and in no formal step, that the refinement was born. Quickstart / Validation: Edit a task Automated tests npm test # all green, including edit-task and store.editTask npm run build # clean type-check Manual validation (Success Criteria) 1. SC-001: create a task, edit the title, save, close and reopen the app → the task appears with the new title, in the same position. 2. SC-002: edit and leave the title empty → the task stays unchanged and the error is signaled. 3. SC-003: edit and adopt another task's title → the task stays unchanged and the duplicate error is signaled. 4. Edit only the description → the title stays intact and the new description is displayed (FR-001). 5. The other tasks keep their creation order after the edit (FR- 005). The tasks Execution List: tasks.md This is the version after the fix: T002 already includes the "keeping your own title, editing only the description, returns success (not a duplicate)" case, in bold. The fix cascade came down from the spec to here before touching the code. Tasks: Edit a task Branch: 005-editar-tarefa | Plan: plan.md TDD order: the test for a behavior comes before the implementation that satisfies it. [P] marks tasks that can run in parallel (distinct files, no dependency). Purely mechanical steps (creating a failure type, composing the edit mode in the view) do not require a test of their own. Phase 1 - Domain (use case) T001 src/domain/failures/edit-task-failure.ts : type EditTaskFailure = { kind: "title-required" } | { kind: "duplicate-title" } | { kind: "task-not- found" } . (mechanical step: failure type, no test of its own) T002 test/domain/edit-task.test.ts : tests of EditTask (edits an existing task's title and description and returns success; keeping your own title, editing only the description, returns success (not a duplicate — FR-003); an empty title returns title-required ; a title already used by another task returns duplicate-title ; a nonexistent task returns task-not-found ; editing does not change the position in the list or the createdAt ). Fails first. T003 src/domain/usecases/edit-task.ts : makeEditTask(repository) (looks up by id; nonexistent becomes a failure; revalidates the required and not-repeated title; assembles the changed task and calls update ); implement until T002 passes. Phase 2 - Presentation T004 test/presentation/task-store.test.ts : test of editTask in the store (after editing, the reloaded list shows the new text in the same position). Fails first. T005 src/presentation/store/task-store.ts : editTask(id, title, description) action (triggers the use case, translates the failure into a message and reloads tasks ); implement until T004 passes. T006 src/presentation/components/TaskList.tsx : inline edit mode on each item (enter editing, adjust title/description, save or cancel). (mechanical view step: composition, covered by the manual validation) Phase 3 - Validation T007 npm test (all green) and npm run build (clean type-check). T008 Manual validation of Success Criteria SC-001..SC-003 and of the quickstart. The Code implement Generated The highlight of this lap is in the EditTask use case, and more precisely in the line that changed after the discovery. I show the final code first and, right below, the exact diff of the refinement, so you can see the size of the fix: one condition. Domain The predictable failures of editing, gathering the two title rules (inherited from creation) with the nonexistent task (inherited from completing/deleting). // src/domain/failures/edit-task-failure.ts /** * Predictable failures when editing a task . * * `title-required`: an empty title or only spaces after `trim` (FR-002) . * `duplicate-title`: another task with the same title already exists (FR-003 ). * `task-not-found`: no task with the given id exists (FR-004) . * / export type EditTaskFailure = | { readonly kind: "title-required" } | { readonly kind: "duplicate-title" } | { readonly kind: "task-not-found" }; The EditTask use case, in its final version. Notice the duplicate check: the other.id !== id filter is the heart of the fix, it is what makes uniqueness hold against the other tasks, and not against the task itself. // src/domain/usecases/edit-task.ts import type { Task } from "../entities/task"; import type { EditTaskFailure } from "../failures/edit-task-failure"; import type { TaskRepository } from "../repositories/task-repository"; import { failure, success, type Result } from "../result"; export type EditTaskInput = { id: string; title: string; description?: string }; export type EditTask = (input: EditTaskInput) => Promise>; /** * Edits the textual content of an existing task, revalidating the title an d * returning error as value: nonexistent task (FR-004), required title (FR-00 2) * and a title not repeated among the OTHER tasks (FR-003) — keeping your ow n * title is not a duplicate. The task is updated in place, without changing t he * position in the list or the `createdAt` (FR-005) . * / export const makeEditTask = (repository: TaskRepository): EditTask => async ({ id, title, description = "" }) => { const task = await repository.getById(id); if (task === null) { return failure({ kind: "task-not-found" }); } const trimmedTitle = title.trim(); if (trimmedTitle.length === 0) { return failure({ kind: "title-required" }); } const existing = await repository.getAll(); const isDuplicate = existing.some( (other) => other.id !== id && other.title.trim() === trimmedTitle ); if (isDuplicate) { return failure({ kind: "duplicate-title" }); } const edited: Task = { ...task, title: trimmedTitle, description: descrip tion.trim() }; await repository.update(edited); return success(edited); }; And here is the refinement, isolated. The commit Fix at the source: FR-003 does not count the task itself as a duplicate changed six lines, of which the one that matters is the check's condition. The naive version compared only the title; the corrected one adds other.id !== id . // src/domain/usecases/edit-task.ts - const isDuplicate = existing.some((other) => other.title.trim() === trim medTitle); + const isDuplicate = existing.some( + (other) => other.id !== id && other.title.trim() === trimmedTitle + ); The fix changed one condition, but it walked the whole chain before reaching here, as the artifacts above record. Presentation The store gains editTask and, with it, a second error-translation function, editMessageFor , because editing has a failure creation did not have ( task-not-found ). The orchestrate-and-reload pattern is the same as the earlier laps. // src/presentation/store/task-store.ts import { createStore } from "zustand/vanilla"; import type { Task } from "../../domain/entities/task"; import type { TaskFilter } from "../../domain/entities/task-filter"; import type { CreateTaskFailure } from "../../domain/failures/create-task-fai lure"; import type { EditTaskFailure } from "../../domain/failures/edit-task-failure "; import type { CompleteTask } from "../../domain/usecases/complete-task"; import type { CreateTask } from "../../domain/usecases/create-task"; import type { DeleteTask } from "../../domain/usecases/delete-task"; import type { EditTask } from "../../domain/usecases/edit-task"; import type { ListTasks } from "../../domain/usecases/list-tasks"; import type { ReopenTask } from "../../domain/usecases/reopen-task"; export type TaskStoreState = { readonly tasks: Task[]; readonly filter: TaskFilter; readonly lastError: string | null; loadTasks: () => Promise; createTask: (title: string, description: string) => Promise; completeTask: (id: string) => Promise; reopenTask: (id: string) => Promise; deleteTask: (id: string) => Promise; editTask: (id: string, title: string, description: string) => Promise; setFilter: (filter: TaskFilter) => void; }; const messageFor = (failure: CreateTaskFailure): string => { switch (failure.kind) { case "title-required": return "The title is required."; case "duplicate-title": return "A task with this title already exists."; } }; const editMessageFor = (failure: EditTaskFailure): string => { switch (failure.kind) { case "title-required": return "The title is required."; case "duplicate-title": return "A task with this title already exists."; case "task-not-found": return "This task no longer exists."; } }; /** * Presentation store (Zustand) that orchestrates the domain use cases an d * exposes the state to the UI. It holds no business rule: it only trigger s * `createTask`/`listTasks` and translates the typed failure into a message f or * the screen . * / export const createTaskStore = ( createTask: CreateTask, listTasks: ListTasks, completeTask: CompleteTask, reopenTask: ReopenTask, deleteTask: DeleteTask, editTask: EditTask ) => createStore((set) => ({ tasks: [], filter: "open", lastError: null, loadTasks: async () => { const tasks = await listTasks(); set({ tasks, lastError: null }); }, createTask: async (title, description) => { const result = await createTask({ title, description }); if (result.kind === "failure") { set({ lastError: messageFor(result.error) }); return false; } const tasks = await listTasks(); set({ tasks, lastError: null }); return true; }, completeTask: async (id) => { await completeTask(id); const tasks = await listTasks(); set({ tasks }); }, reopenTask: async (id) => { await reopenTask(id); const tasks = await listTasks(); set({ tasks }); }, deleteTask: async (id) => { await deleteTask(id); const tasks = await listTasks(); set({ tasks }); }, editTask: async (id, title, description) => { const result = await editTask({ id, title, description }); if (result.kind === "failure") { set({ lastError: editMessageFor(result.error) }); return false; } const tasks = await listTasks(); set({ tasks, lastError: null }); return true; }, setFilter: (filter) => { set({ filter }); } })); The TaskList gains inline edit mode: each item toggles between display and a small form with title, description and the Save/Cancel buttons. The draft state ( editingId , draftTitle , draftDescription ) is ephemeral UI state, it lives in the component, not in the store or the domain. Save only closes the edit if editTask returned success; on error, the message appears and the form stays open. // src/presentation/components/TaskList.tsx import { useState } from "react"; import { selectByFilter } from "../../domain/entities/task-filter"; import { useTaskStore } from "../store/task-store-context"; /** * Lists the active view's tasks in creation order; an empty list is a vali d * state. The choice of which tasks appear comes from the domai n * (`selectByFilter`); here we only display the result. A completed task i s * shown struck through, with the action toggling between completing an d * reopening. Each item can enter inline edit mode, which triggers `editTask` in * the store on save . * / export function TaskList() { const tasks = useTaskStore((state) => state.tasks); const filter = useTaskStore((state) => state.filter); const completeTask = useTaskStore((state) => state.completeTask); const reopenTask = useTaskStore((state) => state.reopenTask); const deleteTask = useTaskStore((state) => state.deleteTask); const editTask = useTaskStore((state) => state.editTask); const lastError = useTaskStore((state) => state.lastError); const [editingId, setEditingId] = useState(null); const [draftTitle, setDraftTitle] = useState(""); const [draftDescription, setDraftDescription] = useState(""); const visibleTasks = selectByFilter(tasks, filter); const confirmAndDelete = (id: string, title: string) => { if (window.confirm(`Delete "${title}"? This action cannot be undone.`)) { void deleteTask(id); } }; const startEditing = (id: string, title: string, description: string) => { setEditingId(id); setDraftTitle(title); setDraftDescription(description); }; const saveEditing = async (id: string) => { const saved = await editTask(id, draftTitle, draftDescription); if (saved) { setEditingId(null); } }; if (visibleTasks.length === 0) { return

No tasks in this view.

; } return (
    {visibleTasks.map((task) => (
  • {editingId === task.id ? (
    aria-label="Edit title" value={draftTitle} onChange={(event) => setDraftTitle(event.target.value)} />