Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

49KB

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<Task, EditTaskFailure> 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<Task | null> 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<Result<Task, EditTas kFailure»; /**

  • 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
  • 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<string | null>(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) => (
    <li
    key={task.id}
    className={task.completed ? “task-list__item task-list__item--done”
    “task-list__item”} > {editingId === task.id ? (
    <input

    aria-label="Edit title” value={draftTitle} onChange={(event) => setDraftTitle(event.target.value)} /> <textarea aria-label="Edit description” value={draftDescription} onChange={(event) => setDraftDescription(event.target.value)} /> {lastError !== null &&

    {lastError}

    } <button type="button” onClick={() => saveEditing(task.id)}>Save <button type="button” onClick={() => setEditingId(null)}>Cancel ) : (

    <> <input type="checkbox” checked={task.completed} aria-label={task.completed ? Reopen ${task.title} : Comp lete ${task.title}} onChange={() => (task.completed ? reopenTask(task.id) : com pleteTask(task.id))} />

    {task.title}

    {task.description.length > 0 &&

    {task.description}

    } <button type="button”

    className="task-list__edit-button” aria-label={Edit ${task.title}} onClick={() => startEditing(task.id, task.title, task.descrip tion)} > Edit <button type="button” className="task-list__delete” aria-label={Delete ${task.title}} onClick={() => confirmAndDelete(task.id, task.title)} > Delete </>

    )} ))} ); } The main.tsx composes the last use case at the boundary. The createTaskStore signature reached six use cases over five laps, and the composition point stays the same, and the only one. // src/main.tsx import { StrictMode } from “react”; import { createRoot } from “react-dom/client”; import { LocalStorageTaskRepository } from “./data/local-storage-task-reposit ory”; import { makeCompleteTask } from “./domain/usecases/complete-task”; import { makeCreateTask } from “./domain/usecases/create-task”; import { makeDeleteTask } from “./domain/usecases/delete-task”;

    import { makeEditTask } from “./domain/usecases/edit-task”; import { makeListTasks } from “./domain/usecases/list-tasks”; import { makeReopenTask } from “./domain/usecases/reopen-task”; import { App } from “./presentation/App”; import { createTaskStore } from “./presentation/store/task-store”; import { TaskStoreProvider } from “./presentation/store/task-store-context”; import “./index.css”; // Composition at the boundary: the concrete repository is instantiated here and // injected into the use cases; only at this point do the layers meet. const repository = new LocalStorageTaskRepository(); const store = createTaskStore( makeCreateTask(repository), makeListTasks(repository), makeCompleteTask(repository),

    makeReopenTask(repository), makeDeleteTask(repository), makeEditTask(repository) ); const rootElement = document.getElementById(“root”); if (rootElement === null) { throw new Error(“Element #root not found in index.html."); } createRoot(rootElement).render(

    ); Tests The EditTask tests include, in second place, exactly the case the refinement created: "keeps its own title when editing only the description (not a duplicate)." It is the test that was missing, and that made the defect visible as red before it turned green. // test/domain/edit-task.test.ts import { describe, expect, it } from "vitest"; import { makeCreateTask } from "../../src/domain/usecases/create-task"; import { makeEditTask } from "../../src/domain/usecases/edit-task"; import { InMemoryTaskRepository } from "../helpers/in-memory-task-repository" ; const seedTask = async (repository: InMemoryTaskRepository, title: string, id : string) => { const createTask = makeCreateTask(repository, () => id); const result = await createTask({ title });

    if (result.kind === “failure”) { throw new Error(“seed failed”); } return result.value; }; describe(“EditTask”, () => { it(“changes the title and description of an existing task”, async () => { const repository = new InMemoryTaskRepository(); const task = await seedTask(repository, “Buy bread”, “id-1”); const editTask = makeEditTask(repository); const result = await editTask({ id: task.id, title: “Buy whole-grain brea d”, description: “at the corner bakery” }); expect(result.kind).toBe(“success”);

    const stored = await repository.getById(task.id); expect(stored?.title).toBe(“Buy whole-grain bread”); expect(stored?.description).toBe(“at the corner bakery”); }); it(“keeps its own title when editing only the description (not a duplicate) “, async () => { const repository = new InMemoryTaskRepository(); const task = await seedTask(repository, “Buy bread”, “id-1”); const editTask = makeEditTask(repository); const result = await editTask({ id: task.id, title: “Buy bread”, descript ion: “at the bakery” }); expect(result.kind).toBe(“success”); const stored = await repository.getById(task.id); expect(stored?.title).toBe(“Buy bread”);

    expect(stored?.description).toBe(“at the bakery”); }); it(“returns title-required when the title becomes empty”, async () => { const repository = new InMemoryTaskRepository(); const task = await seedTask(repository, “Buy bread”, “id-1”); const editTask = makeEditTask(repository); const result = await editTask({ id: task.id, title: " " }); expect(result).toEqual({ kind: “failure”, error: { kind: “title-required” } }); }); it(“returns duplicate-title when the new title is already used by another t ask”, async () => { const repository = new InMemoryTaskRepository(); await seedTask(repository, “Buy bread”, “id-1”);

    const second = await seedTask(repository, “Wash the dishes”, “id-2”); const editTask = makeEditTask(repository); const result = await editTask({ id: second.id, title: “Buy bread” }); expect(result).toEqual({ kind: “failure”, error: { kind: “duplicate-title " } }); }); it(“returns task-not-found when the task does not exist”, async () => { const repository = new InMemoryTaskRepository(); const editTask = makeEditTask(repository); const result = await editTask({ id: “nonexistent”, title: “Anything” }); expect(result).toEqual({ kind: “failure”, error: { kind: “task-not-found” } }); });

    it(“does not change the task's position in the list or the createdAt when e diting”, async () => { const repository = new InMemoryTaskRepository(); let tick = 0; const createTask = makeCreateTask(repository, () => id-${tick}, () => n ew Date(2026, 0, 1, 0, 0, tick++)); await createTask({ title: “First” }); await createTask({ title: “Second” }); await createTask({ title: “Third” }); const original = await repository.getById(“id-1”); const editTask = makeEditTask(repository); await editTask({ id: “id-1”, title: “Second renamed” }); const tasks = await repository.getAll(); expect(tasks.map((task) => task.title)).toEqual([“First”, “Second renamed “, “Third”]); expect(tasks[1].createdAt).toBe(original?.createdAt);

    }); }); The store tests gain two editTask cases (a successful edit in the same position; a duplicate title exposes a message and does not change the list), closing the suite that grew from lap to lap without ever being rewritten. // test/presentation/task-store.test.ts import { describe, expect, it } from “vitest”; import { makeCompleteTask } from “../../src/domain/usecases/complete-task”; import { makeCreateTask } from “../../src/domain/usecases/create-task”; import { makeDeleteTask } from “../../src/domain/usecases/delete-task”; import { makeEditTask } from “../../src/domain/usecases/edit-task”; import { makeListTasks } from “../../src/domain/usecases/list-tasks”; import { makeReopenTask } from “../../src/domain/usecases/reopen-task”; import { createTaskStore } from “../../src/presentation/store/task-store”; import { InMemoryTaskRepository } from “../helpers/in-memory-task-repository” ;

    const buildStore = () => { const repository = new InMemoryTaskRepository(); return createTaskStore( makeCreateTask(repository), makeListTasks(repository), makeCompleteTask(repository), makeReopenTask(repository), makeDeleteTask(repository), makeEditTask(repository) ); }; describe(“task store”, () => { it(“loads the list of existing tasks”, async () => { const store = buildStore();

    await store.getState().createTask(“Task A”, “"); await store.getState().loadTasks(); expect(store.getState().tasks.map((task) => task.title)).toEqual([“Task A “]); }); it(“creating successfully updates the list and clears the error”, async () => { const store = buildStore(); const created = await store.getState().createTask(“New”, “a description”) ; expect(created).toBe(true); expect(store.getState().tasks).toHaveLength(1); expect(store.getState().lastError).toBeNull(); });

    it(“a title-required failure exposes a message without changing the list”, async () => { const store = buildStore(); const created = await store.getState().createTask(” “, “"); expect(created).toBe(false); expect(store.getState().tasks).toHaveLength(0); expect(store.getState().lastError).toBe(“The title is required."); }); it(“a duplicate-title failure exposes a specific message”, async () => { const store = buildStore(); await store.getState().createTask(“Duplicate”, “"); const created = await store.getState().createTask(“Duplicate”, “");

    expect(created).toBe(false); expect(store.getState().lastError).toBe(“A task with this title already e xists."); expect(store.getState().tasks).toHaveLength(1); }); it(“completing a task reflects the completed state in the list”, async () =

    { const store = buildStore(); await store.getState().createTask(“Finish the report”, “"); const taskId = store.getState().tasks[0].id; await store.getState().completeTask(taskId); expect(store.getState().tasks[0].completed).toBe(true); }); it(“reopening a completed task returns it to the open state”, async () => {

    const store = buildStore(); await store.getState().createTask(“Finish the report”, “"); const taskId = store.getState().tasks[0].id; await store.getState().completeTask(taskId); await store.getState().reopenTask(taskId); expect(store.getState().tasks[0].completed).toBe(false); }); it(“the initial view is ‘open’", () => { const store = buildStore(); expect(store.getState().filter).toBe(“open”); });

    it(“setFilter switches the active view without touching the full list”, asy nc () => { const store = buildStore(); await store.getState().createTask(“Task A”, “"); store.getState().setFilter(“completed”); expect(store.getState().filter).toBe(“completed”); expect(store.getState().tasks).toHaveLength(1); }); it(“deleteTask removes the task and reloads the list without it”, async () => { const store = buildStore(); await store.getState().createTask(“Task A”, “"); await store.getState().createTask(“Task B”, “"); const target = store.getState().tasks.find((task) => task.title === “Task A”);

    await store.getState().deleteTask(target!.id); const titles = store.getState().tasks.map((task) => task.title); expect(titles).toEqual([“Task B”]); }); it(“editTask changes the task's text and reloads the list in the same posit ion”, async () => { const store = buildStore(); await store.getState().createTask(“Task A”, “"); await store.getState().createTask(“Task B”, “"); const target = store.getState().tasks.find((task) => task.title === “Task A”); await store.getState().editTask(target!.id, “Task A revised”, “with a not e”); const titles = store.getState().tasks.map((task) => task.title); expect(titles).toEqual([“Task A revised”, “Task B”]);

    expect(store.getState().lastError).toBeNull(); }); it(“editTask with a duplicate title exposes a message and does not change t he list”, async () => { const store = buildStore(); await store.getState().createTask(“Task A”, “"); await store.getState().createTask(“Task B”, “"); const target = store.getState().tasks.find((task) => task.title === “Task B”); await store.getState().editTask(target!.id, “Task A”, “"); const titles = store.getState().tasks.map((task) => task.title); expect(titles).toEqual([“Task A”, “Task B”]); expect(store.getState().lastError).toBe(“A task with this title already e xists."); });

    }); The Second Iteration, in the Order It Happened So far you have read this lap as if it ran in a straight line, from requirement to reviewed code. It did not. Between the green implement and the merge there was a second pass over the same artifacts, and that pass is what completes the shape of the loop: requirement, spec, plan, tasks, code, review, and then back to the start with whatever the review found. Here is the real chronology. The suite was green and npm run build was clean. What was missing was the cheapest step, and the easiest one to skip: opening the app and running the five items of quickstart.md by hand. Items 1, 2, 3 and 5 passed. Item 4, “editing only the description,” returned the message “A task with that title already exists” on screen, for a task whose title nobody had touched. The tempting reflex, in that minute, was to open edit-task.ts and settle it there. The code fix is small and obvious, and you have already seen it: an id comparison in the duplicate check. Half a dozen lines, one commit, move on. What is wrong with that path is not the immediate result, which would be identical, but what it leaves behind. FR-003 would still be written the ambiguous way, T002 would still be missing the case, and the next person to read the spec in order to build anything on top of editing would inherit the same trap, now silently contradicted by the code. So the fix went up. The order of the commits, on the 005-editar- tarefa branch, was: 1. spec.md : clarification Q5 went in (“Does the not-repeated title rule consider the task being edited? No”), FR-003 was

    rewritten to say “among the other tasks,” and Scenario 2 and the matching edge case gained the same precision. 2. plan.md : nothing to change. The plan said “revalidates the title, reusing the creation criterion,” and the criterion was still the one from creation. It was the wording of the requirement that was incomplete, not the design of the construction. Not every fix at the source travels through every artifact, and touching the plan just to keep the symmetry would be noise. 3. tasks.md : T002 gained the case “keeping your own title, editing only the description, returns success.” 4. The test: written first, red, named after the rule and not after the bug. 5. The code: the condition fixed, the test green, the whole suite green again. Notice what that order cost. Some fifteen minutes more than the direct patch, and four files touched instead of one. In exchange, the spec became true again. It is the same move Chapter 9 showed when analyze demanded coherence between artifacts, with one difference that matters: there the conflict surfaced in a formal step, with a report pointing the finger; here it surfaced with the app open on screen, in a ten-second manual test no tool ran for you. The two gates catch different things, and neither excuses the other. That second pass is what kept quickstart.md from being end-of-lap paperwork. It is the last chance to discover that the spec was incomplete while the spec is still the living document of an open branch, and not a historical text nobody reads anymore. The Portrait of the Lap, and of the Five

    This last lap closes the material's argument in two ways. First, it shows that implement is not the mute end of the loop: it discovers things. The uniqueness rule that does not count the task itself was not in anyone's head when the spec was written; it emerged running the app, and climbed back up to the spec instead of becoming a clever hidden if . The artifact stays the truth. Second, and looking at the five laps together: the Task entity gained one field ( completed ) and nothing more; the TaskRepository grew to five methods, each added when a feature needed it and reused by the following ones; the composition point in main.tsx stays single; the test suite only grew, was never rewritten. Five features, five laps of the same loop, and the architecture Chapter 6.5 showed is intact in 10.5. What makes an architecture survive that kind of pressure is the subject of FOCUS Architecture (2026, https://books.kodel.com.br/en/books/focus/), the second volume in this trilogy, which answers where each rule lives and why dependencies point inward. You do not need it to finish this book: the proof that matters here is in the five commits you have just followed. That was the thesis: the method does not guarantee you get it right the first time (005 proves it does not), it guarantees that, when you get it wrong, the fix goes in at the source and the rest stays standing. That movement, fixing at the source, showed up here as the account of one lap. The next chapter takes it as a method: which doors a spec gets corrected through, in what order the artifacts move, and when insisting on the correction stops making sense.

    Powered by TurnKey Linux.