For ten modules you have had a guided example: Nómada Tasks was built in front of you, lesson by lesson, and every new concept arrived with its slice of code already worked out. That ends here. In this module you build your own product, on your own, with the same method and with nobody handing you the code. Nómada Tasks does not disappear: it becomes the reference — six tasks, 48 hours, rules R1–R10, 124 tests, 58.3 kB in three requests, an LCP of 1.9 s — against which you will compare whatever you build. And this first lesson deliberately does not write a single line of business logic, because the most expensive mistake in a project of your own is not an unexpected undefined: it is starting to code without knowing what you are building. Here you will learn to turn an idea into a defensible plan: choose the product, trim it down to an honest MVP and write down in black and white what you are not going to do; write user stories with acceptance criteria and prioritize them with MoSCoW; design the data model and the business rules — the new R11 to R15 — before touching the editor; decide the layered architecture and the boundaries that are never crossed; set up the repository, the toolchain and the Git workflow; plan in milestones with a Gantt chart and an estimate you do not lie to yourself about; and lock in, from minute zero, the Definition of Done and the performance and accessibility budget. By the end you will have three real deliverables: the scope document, the data model with its rules, and an initialized repository that is green.

Contents

  1. What changes from now on
  2. The default product: Orbita
  3. Three alternative domains with the same method
  4. Setting the scope: the MVP
  5. The list of what will NOT be done
  6. User stories with acceptance criteria
  7. Prioritizing with MoSCoW
  8. The data model, written before you code
  9. The new business rules: R11 to R15
  10. The layered architecture and the boundaries that are never crossed
  11. The environment: repository and toolchain
  12. The starting folder structure
  13. Git for real: branches, commits and reviews
  14. Milestones and honest estimation
  15. The Definition of Done
  16. The performance and accessibility budget
  17. The README.md template
  18. Common Mistakes and Tips
  19. Exercises
  20. Conclusion

  1. What changes from now on

Up to lesson 10-06, the course worked like this: a concept was explained, it was applied to Nómada Tasks, and the code appeared. You read it, you understood it, you adapted it in the exercises. That is the right way to learn the fundamentals, and it is exactly what had to happen.

But there is an enormous difference between understanding somebody else's code and producing your own code from scratch, and you do not close that gap by reading more. You close it by building. This module is that build.

From here on, three things change:

Before (M1–M10) Now (M11)
The code was handed to you solved You write the code; here you get method, templates and criteria
The exercises were practice The exercises are project deliverables
The solutions were the correct code The solutions are acceptance criteria and rubrics
The project was Nómada Tasks The project is yours; Nómada Tasks is the reference
The goal was to learn a concept The goal is to finish a product

That last point deserves emphasis. The skill that separates someone who knows JavaScript from someone who is a developer is not knowing more array methods: it is finishing things. Finishing means it works, it is tested, it can be deployed, another person understands it, and you can defend why it is built the way it is. That is the bar for this module.

And there is an honest warning worth reading slowly. The hard part of a project of your own is not technical. It is that nobody tells you when you are done, nobody corrects your scope when it grows, and nobody warns you that you have spent three days polishing an animation while the core functionality is still half-built. The method in this lesson exists precisely to put up those guardrails before you need them.

  1. The default product: Orbita

The default product for this module is called Orbita. It is a work-management application for small teams: the same family as Nómada Tasks, but clearly more ambitious, with six extensions that were never implemented in the course and that you will have to solve on your own.

A note about data. Orbita, its sample users and any organization that appears in your test data must be fictional. Do not use real names, email addresses or data belonging to colleagues, clients or family members, not even "just to test": the moment that reaches a public repository or a deployment, it stops being a test and becomes processing of personal data. In lessons 11-03 and 11-05 we will come back to this point in detail.

The six extensions on top of Nómada Tasks are these:

# Extension What it adds over Nómada Tasks Course concept it reuses
E1 Users and assignment People stop being a string and become entities with id, role and permissions; assignee and reviewer are references 04-01, 05-02, 05-03
E2 Subtasks A task can be broken down into a tree up to three levels deep, with aggregated hours and progress 03-07 (recursion)
E3 Tags with multiple filtering Filter by several tags at once, with AND / OR modes, combinable with the other filters 03-06, 04-05
E4 Calendar view A monthly grid that places tasks by dueDate, with month-to-month navigation 04-04, 06-05, Intl from 07-06
E5 Exportable workload report Workload per person and per week, exportable to CSV and JSON downloadable from the browser 04-05 (reduce), 07-06 (Blob)
E6 Change history An immutable log of who changed what and when, with an audit view per task 04-08, 05-03

None of the six is decorative. Each one forces you to solve a real problem that Nómada Tasks sidestepped:

  • E1 breaks the assignee: 'Iván' simplification. As soon as there are referenced entities, referential integrity appears (what happens to the tasks of a user who is deleted?) along with permissions.
  • E2 turns a flat list into a tree. Everything that was array.filter(...) now needs recursive traversal, and total hours stop being a straight sum.
  • E3 looks trivial and is not: combining N filters with two different semantics and keeping them in the URL (06-04, router) forces you to think about state.
  • E4 is the first view that is not a list, and therefore the first one you cannot solve by copying BoardView.
  • E5 is the first feature that produces a file, with everything that implies: format, encoding, filenames, and the fact that a badly escaped CSV breaks the moment a title contains a comma.
  • E6 introduces immutable, ever-growing data, which is exactly the kind of data that blows up localStorage (lesson 11-03).

Orbita in one sentence, which is how you should be able to describe any product:

Orbita is a web application that lets a small team plan its work in tasks and subtasks, see who is overloaded and when things are due, and know at all times who changed what.

If you cannot write that sentence for your product, you do not yet know what you are building.

  1. Three alternative domains with the same method

If the task domain bores you — a perfectly legitimate reason; you are going to spend weeks with this — pick another one. The method is identical and the whole module works the same way. These are the three proposed alternatives, each with the six extensions mapped onto it:

Extension Orbita (tasks) Capacity (room bookings) Streak (habits) Shelf (inventory)
E1 · Users Assignee and reviewer Who books and who authorizes Person and support group Warehouse owner
E2 · Tree Subtasks Room → subspaces (desk, booth) Habit → daily steps Category → subcategory → item
E3 · Multiple filter Tags Equipment (projector, whiteboard) Areas (health, study) Product tags
E4 · Calendar Due date The central view: occupancy by time slot Monthly consistency grid Expiry dates and restocking
E5 · Report Workload per person Occupancy per room and week Streak and completion percentage Stock valuation
E6 · History Who changed what Booking changes and cancellations Daily log (it already is a history) Stock in and out movements
Characteristic hard rule No closing with open subtasks No two overlapping bookings in the same room One entry per habit per day Stock can never go negative

Look at the last row: every domain has one characteristic hard rule that gives the model its personality and that will need the most tests. In Capacity it is interval overlap, a classic problem and considerably subtler than it looks. In Streak it is uniqueness per day, which forces you to think about time zones. In Shelf it is a numeric invariant you have to defend in every operation.

Criteria for choosing, in order of importance:

  1. That you care about it. You are going to spend many hours on it and nobody is going to force you. Motivation is as real a project resource as time.
  2. That you can show it to someone. If in an interview you can explain the domain in thirty seconds, it works.
  3. That it has at least one non-trivial hard rule. A CRUD with no rules proves nothing; it is the project everybody does.
  4. That it fits. If your idea is "like Jira but better", it does not fit. Trim it until it fits, which is exactly the next section.

Throughout the rest of the module I will use Orbita in every example, but each template is directly reusable with any of the other three domains. When you see a table or a template, swap the names and carry on.

  1. Setting the scope: the MVP

MVP stands for Minimum Viable Product. Both words matter and one of the two is almost always misunderstood.

  • Minimum does not mean "shoddy". It means there is nothing you can remove without it ceasing to solve the problem.
  • Viable does not mean "a demo". It means somebody could genuinely use it for what it promises.

An Orbita MVP that did not let you mark a task as done would be minimum but not viable. One with color themes, customizable keyboard shortcuts and email notifications would be viable but not minimum.

The test I use to decide whether something belongs in the MVP is a single question:

If this were not here, would the product still do what its sentence says?

If the answer is "yes, awkwardly, but it works", it is not MVP. It goes into version 1.1.

Applied to Orbita, this is the cut:

Feature MVP? Reason
Create, edit and delete tasks Yes Without this there is no product
Change status through the valid transitions (R6) Yes It is the entire workflow
Assign an assignee (E1) Yes "Who does what" is in the product sentence
One level of subtasks (E2 partial) Yes Decomposition is the differentiator; three levels can wait
Filter by assignee, status and tags (E3) Yes Without a filter, a board with 60 tasks is useless
Local persistence Yes A manager that loses your data on reload is not viable
Change history (E6) Yes It is an explicit promise of the product sentence
Workload report (E5) Yes, minimal The on-screen table yes; the CSV export no
Calendar view (E4) No Useful, but the list sorted by date covers the need
Export to CSV/JSON No Convenience, not necessity
Three levels of subtasks No One level demonstrates the tree; three only add edge cases
Synchronization with an API No The MVP is single-device; 11-03 extends it
Multi-user real time No Very high cost, low value for a three-person team
Full roles and permissions No The MVP assumes all users are trusted
Dark mode, themes, animations No Cosmetic

Out of 15 candidates, 8 make it in. That ratio (roughly half) is normal and healthy. If your MVP contains 90 % of everything you thought of, you have not trimmed: you have written a wish list.

A practical, road-tested size rule: a personal-project MVP should be something you can build in 6 to 10 weeks putting in 6 to 10 hours a week. If your honest estimate (section 14) comes out higher, trim now, because your estimate is optimistic, not pessimistic.

  1. The list of what will NOT be done

This is the section of the scope document most people skip and the one that saves the most projects. An MVP defined only by what it includes is ambiguous: everything not mentioned sits in a gray zone where, on a Tuesday afternoon, adding "just one little thing" will seem reasonable.

Explicitly writing down what will not be done turns every later addition into a conscious decision instead of a drift.

The template has three columns and lives in docs/scope.md:

## Out of scope (v1.0)

| What will not be done | Why | When it gets reconsidered |
|---|---|---|
| Calendar view | The date-sorted list covers the main need | v1.1, after milestone H5 |
| CSV/JSON export | Convenience; no acceptance criterion depends on it | v1.1 |
| Subtasks deeper than one level | One level already demonstrates the tree; more levels only add edge cases | v1.2, if a real user asks for it |
| Server synchronization | Requires a backend; the MVP is single-device | v2.0 (lesson 11-03 prepares the boundary) |
| Real time / collaboration | Disproportionate cost for three users | Not planned |
| User authentication | There is no server; client-only authentication is security theater | v2.0, alongside the backend |
| Email notifications | Requires a server and an email provider | Not planned |
| Internationalization into several languages | A single language; formats will use `Intl` | v1.2 |
| Native mobile app | The PWA covers the mobile use case | Not planned |
| Themes and visual customization | Cosmetic; affects no acceptance criterion | Not planned |

Three details of the template that are not decoration:

  • The "Why" column protects you from yourself. Three weeks from now you will not remember the reasoning, only the temptation.
  • "When it gets reconsidered" avoids the resentful "never". Deferring is not rejecting, and saying it that way makes the cut far easier to accept.
  • "Not planned" is a legitimate answer and you should use it without guilt. Not everything deferred has to come back.

A specific note about the authentication line, because it is a classic mistake in portfolio projects: a login screen without a server protects nothing. If the data lives in localStorage, anyone with developer tools can see it. Putting a password form in front of it is worse than not putting one, because it communicates a security that does not exist. If your project genuinely needs authentication, it needs a server, and that is version 2.0 (lesson 11-07).

  1. User stories with acceptance criteria

A user story describes a need from the point of view of the person who has it, not the person implementing it. Its value is not the pretty format: it is that it forces you to say for whom and what for, and those two words often decide the technical design.

The classic template:

**As a** <role>
**I want** <action>
**so that** <benefit>

And what really makes a story useful are its acceptance criteria: verifiable conditions that let you say "done" without argument. They are written in Given / When / Then form, which has an enormous and entirely deliberate advantage: it translates almost literally into a test of the kind you learned in 08-03 and 08-06.

Here are three real Orbita stories, in full.

6.1 Story H-04: breaking a task down

### H-04 · Break a task down into subtasks

**As a** team coordinator
**I want** to split a large task into subtasks
**so that** I can spread the work across several people and see partial progress

**Acceptance criteria**

1. **Given** an existing task in `pending` or `in-progress` status,
   **when** I add a subtask with a title and hours,
   **then** the subtask appears nested under its parent task
   **and** the parent's hours become the sum of its subtasks' hours.

2. **Given** a task with two subtasks, one `done` and one `pending`,
   **when** I try to mark the parent as `done`,
   **then** the application blocks it with a message that names
   the open subtask (rule R13).

3. **Given** a task with subtasks,
   **when** I look at its progress,
   **then** I see the percentage of finished subtasks
   **and** that percentage is announced with `aria-live` when it changes.

4. **Given** a subtask,
   **when** I try to make it the parent of its own parent task,
   **then** the operation is rejected with `RuleError` (no cycles, R12).

**Technical notes**: tree traversal is recursive (03-07).
Maximum depth is 3; the MVP only exposes 1 level.

Notice criterion 2. It does not say "you cannot do it": it says exactly what happens, with which message, citing the rule. That precision is what turns the criterion into a five-line Jest test.

And notice criterion 3: accessibility is inside the acceptance criterion, not in a separate list reviewed at the end. That is the only way it does not get forgotten. I will come back to this in section 16 and in lesson 11-02.

6.2 Story H-07: filtering by several tags

### H-07 · Filter by several tags at once

**As a** team member
**I want** to filter the board by several combined tags
**so that** I can quickly find the work in a specific area

**Acceptance criteria**

1. **Given** a board with tagged tasks,
   **when** I select two tags in *any* mode (OR),
   **then** I see the tasks carrying at least one of the two.

2. **Given** the same selection in *all* mode (AND),
   **then** I see only the tasks carrying both.

3. **Given** an active filter,
   **when** I copy the URL and open it in another tab,
   **then** the filter is applied the same way (state lives in the URL).

4. **Given** a filter that returns no tasks,
   **then** I see an explanatory empty state with a
   "Clear filters" button, not a blank list.

5. **Given** any filter,
   **when** the number of results changes,
   **then** it is announced via `aria-live` ("12 tasks visible of 47").

Criterion 3 is the one with architectural consequences: it forces the filter to be part of the application state and of the router (06-04 and the router.js of Nómada Tasks), rather than a local variable of the view. A well-written acceptance criterion decides design.

Criterion 4 is the one most often forgotten in portfolio projects: empty states. An application that shows a blank area when there are no results looks broken even when it works perfectly.

6.3 Story H-11: seeing who is overloaded

### H-11 · See the team's workload

**As a** coordinator
**I want** to see how many open hours each person has per week
**so that** I can spread the work before somebody gets swamped

**Acceptance criteria**

1. **Given** a board with assigned tasks that have due dates,
   **when** I open the workload report,
   **then** I see a table of people by ISO weeks
   with the open hours in each cell.

2. **Given** a person with more than 40 open hours in a week,
   **then** their cell is highlighted visually **and** with text
   (not by color alone), and the report states how many people are
   above the limit (R7 + R15).

3. **Given** a task with no `dueDate`,
   **then** its hours appear in a "No date" column
   and are never spread arbitrarily across a week.

4. **Given** a task with subtasks,
   **then** its hours are counted once (the leaves' hours),
   never duplicated between parent and children (R12).

Criterion 4 is the one that will cost you a whole afternoon if you do not write it now. Double-counting hours in trees is one of those bugs that visibly breaks nothing: it just makes every number wrong.

How many stories to write. For an MVP like this one, 12 to 18 stories is the right order of magnitude. Fewer than 10 usually means the stories are too coarse ("as a user I want to manage tasks" is not a story, it is a module). More than 25 usually means you are describing the implementation instead of the need.

  1. Prioritizing with MoSCoW

Having stories is not enough: you need to know in what order. MoSCoW is a simple and surprisingly effective prioritization method, named after the initials of its four categories:

Category Meaning Rule of use
M · Must have Without this there is no product. If it is missing, you do not ship At most 60 % of the total effort
S · Should have Important, but the product works without it ~20 % of the effort
C · Could have Desirable if there is time to spare ~20 % of the effort
W · Won't have Explicitly out of this version It is the list from section 5

The 60 % rule is the important part and the one almost everyone breaks. If your Musts consume 100 % of the available time, you do not have a plan: you have a bet. Any unexpected event — and there is always one — leaves you with nothing to ship. The remaining 40 % in Should and Could is your slack: when time gets tight, they are sacrificed painlessly and the product is still shippable.

Full prioritization of the Orbita MVP:

ID Story MoSCoW Est. (h) Milestone Depends on
H-01 See the task board M 6 H2
H-02 Create a task with validation M 8 H2 H-01
H-03 Change status through valid transitions M 5 H2 H-01
H-04 Break down into subtasks M 10 H3 H-02
H-05 Assign an assignee M 4 H3 H-02
H-06 Edit and delete a task M 6 H3 H-02
H-07 Filter by several tags M 7 H4 H-01
H-08 Filter by assignee and status M 3 H4 H-07
H-09 Local persistence with migrations M 8 H4 H-02
H-10 Change history per task S 9 H5 H-06
H-11 Workload report per person and week S 8 H5 H-05
H-12 Empty state and error states S 4 H5 H-01
H-13 Full keyboard navigation S 5 H5 H-01
H-14 Export the report to CSV C 4 H6 H-11
H-15 Monthly calendar view C 12 H6 H-01
H-16 Installable as an offline PWA C 6 H6 H-09

Split: Must 57 h (57 %), Should 26 h (26 %), Could 22 h (22 %). Total 105 h. The 60 % rule is met with room to spare.

Two warnings about this table:

  • The "Depends on" column is what defines the real order. MoSCoW says what is important; dependencies say what is possible. You cannot do H-11 before H-05, however much you want to.
  • The estimates come from section 14, not out of thin air, and they already have the correction factor explained there baked in.

  1. The data model, written before you code

Here is the most profitable advice in the whole lesson: the data model is written before the code, in a document, and it is reviewed the way a contract is reviewed. Changing a field in a document takes thirty seconds. Changing it once it is in the domain, in the repository, in the view, in the tests and in users' saved data takes an afternoon and a migration (lesson 11-03).

Orbita's model extends the Nómada Tasks one with three new entities.

8.1 Entity diagram

erDiagram
    USER ||--o{ TASK : "is assignee of"
    USER ||--o{ TASK : "reviews"
    USER ||--o{ CHANGE : "makes"
    TASK ||--o{ TASK : "breaks down into"
    TASK ||--o{ CHANGE : "accumulates"
    TASK }o--o{ TAG : "carries"
    BOARD ||--o{ TASK : "contains"
    BOARD ||--o{ USER : "gathers"

    USER {
        string id PK
        string name
        string initials
        string role "coordination|team|guest"
        boolean active
    }
    TASK {
        number id PK
        string title
        string assigneeId FK "null allowed"
        string reviewerId FK "null allowed"
        string priority "high|medium|low"
        string status "pending|in-progress|done"
        number estimatedHours
        string dueDate "ISO or null"
        number parentTaskId FK "null if root"
        string createdAt "ISO"
    }
    TAG {
        string name PK "lowercase"
    }
    CHANGE {
        string id PK
        number taskId FK
        string userId FK
        string date "ISO with time"
        string field
        string before
        string after
    }

8.2 The entities, field by field

User — the most important new entity, because it changes the type of two fields you spent ten modules treating as text.

Field Type Rules Note
id string Unique, stable, never reused Text rather than number: users may come from an external system in v2.0
name string Not empty after trim() Personal data: see the warning at the end of the section
initials string 1–3 characters, uppercase For the text avatar, no images
role string 'coordination' | 'team' | 'guest' Closed set, like priority
active boolean Defaults to true A user is never deleted: they are deactivated. See 8.3

Task — keeps the eight Nómada Tasks fields and adds four:

Field Change with respect to Nómada Tasks
assigneeassigneeId Goes from string to a reference to User.id; still accepts null (R8)
reviewerreviewerId Same
parentTaskId New. null if it is a root; otherwise the id of the parent task (E2)
createdAt New. ISO date with time; needed for R4 and for ordering the history
estimatedHours Now derived if the task has children: the sum of the leaves (R12)
id Still a sequential number (R1), now unique across the whole tree

Change — the history entity (E6). It is append-only: it is added to, never modified or deleted.

Field Type Note
id string A unique identifier; it can be crypto.randomUUID() (07-06)
taskId number Which task it affects
userId string Who did it
date string ISO with time When. With a time, unlike dueDate
field string Which field changed, or 'creation' / 'deletion'
before / after string | null Values serialized to text so the entry is readable without context

That last decision — serializing to text instead of storing the original value — is deliberate and it is worth understanding why: a history entry has to still be readable two years from now, when the model has changed and priority may no longer exist. A history that depends on the current model to be interpreted is not a history: it is a partial copy of the state.

8.3 The three modeling decisions you have to make now

1 · What happens when you delete a user who has tasks assigned?

Three options, and you have to pick one explicitly:

Option What it does Consequence
Cascade delete Deletes their tasks Unacceptable: the team loses work
Set the tasks to null Unassigns them Acceptable, but historical information is lost
Deactivate (chosen) active: false; their tasks keep the reference Everything is preserved; the interface shows "(inactive)"

The chosen option is the third, and it is the one used in almost all professional software: actually deleting referenced entities is almost never the right thing. This decision deserves an ADR (lesson 11-06).

2 · Are tags an entity or an array of strings?

In Nómada Tasks they were an array of normalized strings (R9). For the Orbita MVP they stay that way, with a function that derives the tag catalog from the board. Turning them into an entity with its own id only pays off if you need to rename a tag across all tasks at once, and that is out of scope.

Write it down in the document: a decision made explicitly and justified is not technical debt; it is a decision. What creates debt is not noticing there was a choice.

3 · Is the tree stored nested or flat with a reference?

Shape How Advantage Drawback
Nested task.subtasks = [...] Reads and renders naturally Moving a subtask means cutting and pasting in two places; looking up by id forces a full traversal
Flat with parentTaskId (chosen) Flat list; the tree is built on read Lookup by id is direct; moving is changing one field; it saves and serializes trivially You have to build the tree in memory and detect cycles

The flat shape is what databases use and what will save you trouble in lesson 11-03. Building the tree is a fifteen-line recursive function (03-07); keeping a duplicated nested structure in sync is a permanent problem.

Data protection notice. The moment your model contains people's name, you are modeling personal data. In a learning project with fictional data there is no legal issue at all, but build the habit now: note in the scope document which fields are personal, what they are used for and how long they are kept. If one day this product handled real people's data, GDPR requires a legal basis, information for the data subject, minimization and retention periods — and that requires specific legal advice, not a JavaScript lesson. We will come back to it in 11-03 and 11-05.

  1. The new business rules: R11 to R15

Rules R1 to R10 from Nómada Tasks are still in force. I will run through them briefly because they are the base the new ones rest on:

Rule Description
R1 The id is unique and sequential; the application assigns it, never the user
R2 The title cannot be empty or contain only whitespace
R3 estimatedHours greater than 0 and no higher than 40
R4 dueDate cannot be earlier than the creation date
R5 A new task is always born in 'pending' status
R6 Only the valid status transitions are allowed
R7 Nobody can exceed 40 estimated hours assigned in the same week
R8 A task with no assignee uses null, never ''
R9 Tags are stored in lowercase and without duplicates
R10 An overdue task (dueDate in the past and status other than 'done') is highlighted

And these are the five new ones, the ones that give Orbita its character:

Rule Description Where it is enforced Error it throws
R11 assigneeId and reviewerId must refer to an existing and active user, or be null. One person cannot be both assignee and reviewer of the same task Domain, on creation and on assignment ValidationError with .field
R12 The subtask tree allows no cycles and has a maximum depth of 3. The estimatedHours of a task with children are the sum of the leaves, never a value of its own Domain, on linking and on calculation RuleError
R13 A task with at least one unfinished subtask cannot move to 'done'. Closing a parent task records the closing of all its leaves Domain, in changeStatus RuleError
R14 Every domain modification generates an immutable history entry. The history is not edited or deleted; a correction is a new entry Application, after every mutation — (invariant, not validation)
R15 A user with the 'guest' role can only read. The R7 workload is calculated per user and ISO week, counting only open tasks and only the leaves' hours Domain (workload) and application (permissions) RuleError

Five observations about these rules, because each one hides a decision:

R11 and referential integrity. "Existing and active" is stronger than "existing". It means that when you deactivate a user you have to decide what happens to their future assignments: the chosen answer is that existing ones are preserved (they are history) but no new ones can be created. That asymmetry is intentional and deserves an explicit test.

R12 and the sum of the leaves. It is the rule that prevents the double-counting from criterion 4 of H-11. Its practical consequence: estimatedHours stops being an editable field as soon as a task has children, and the interface must reflect that (a disabled field with an explanation, not a field that accepts a value which is silently ignored).

R13 and cascading closure. Forbidding closing a parent with open children is the strict option; allowing it by closing the children automatically is the convenient one. The chosen rule does the strict thing by default and offers the cascade as an explicit action ("Also close the 3 pending subtasks"). Never cascade silently: deleting or closing things the user has not looked at is the recipe for distrust.

R14 and immutability. "A correction is a new entry" is the age-old accounting principle, and it is what makes a history worth anything. As soon as it can be edited, it stops being evidence of anything.

R15 and the ISO week. Choosing the ISO 8601 week (Monday to Sunday, with the Thursday rule for week 1) instead of "the last seven days" is a decision with consequences: you have to implement it properly, and it is a beautiful test case because 1 January sometimes falls in week 52 of the previous year. Note it down: it will be one of your parameterized tests in lesson 11-04.

Where the rules live. All of them, without exception, in the domain layer. The view may anticipate them to give the user good information (disabling a button, warning before submitting), but the view is never the one that decides. If a rule only exists in the form, it does not exist: calling the method from somewhere else is enough to bypass it. This is exactly what 06-07 explained about client-side validation, and the same thing lesson 11-05 will repeat about server-side validation.

  1. The layered architecture and the boundaries that are never crossed

Nómada Tasks had four folders — model/, data/, view/, util/ — and that organization was not aesthetic: it was a layered architecture, with rules about who is allowed to call whom. Orbita formalizes it with four layers plus one cross-cutting layer.

flowchart TD
    subgraph APP["application/ — orchestrates use cases"]
        A1["createTask()"]
        A2["changeStatus()"]
        A3["generateReport()"]
    end
    subgraph VIEW["view/ — DOM, events, accessibility"]
        V1["BoardView"]
        V2["TaskForm"]
        V3["ReportView"]
    end
    subgraph DATA["data/ — persistence and network"]
        D1["MemoryRepository"]
        D2["LocalRepository"]
        D3["ApiRepository"]
    end
    subgraph DOM["domain/ — rules R1-R15, no dependencies"]
        M1["Task"]
        M2["Board"]
        M3["User"]
        M4["rules.js"]
    end

    VIEW -->|"events"| APP
    APP -->|"reads and writes"| DOM
    APP -->|"saves and loads"| DATA
    DATA -->|"rebuilds"| DOM
    APP -->|"notifies state"| VIEW

    style DOM fill:#dcfce7,stroke:#16a34a
    style DATA fill:#dbeafe,stroke:#2563eb
    style VIEW fill:#fef3c7,stroke:#d97706
    style APP fill:#f3e8ff,stroke:#9333ea

The four layers, with their responsibility and their prohibitions:

Layer Responsibility May import Never imports
domain/ The entities, their invariants and rules R1–R15 Only pure util/ data/, view/, application/, fetch, document, localStorage
data/ Save, load, talk to the network, migrate formats domain/, util/ view/, application/
view/ Render, listen to events, accessibility, focus util/, domain/ types for reading only data/, application/ (it receives functions, it does not import them)
application/ Use cases: orchestrates domain + data + state and notifies the view The three above
util/ Pure functions: dates, formatting, debounce Nothing Everything else

The three boundaries that are never crossed, and what happens to you if you cross them:

  1. domain/ does not know a browser exists. No document, no localStorage, no fetch, no alert. Immediate practical consequence: the domain is tested in Node without jsdom, in milliseconds, and that is what makes hundreds of fast tests possible (11-04). If one day you need Date.now() inside the domain, it gets passed in as a parameter or as an injected clock — never called directly, because then your tests depend on what day you run them.

  2. view/ does not know where the data comes from. It receives a state and some functions (onCreateTask, onChangeStatus) and calls them. It does not know whether behind them there is memory, localStorage or an API. Practical consequence: changing the storage in lesson 11-03 will not touch a single view file.

  3. data/ does not know what gets rendered. It returns domain entities or throws typed errors. It does not return HTML, or user messages, or "strings ready to display". Practical consequence: translating an error into friendly text is the view's responsibility, which is why the same RuleError can be shown in three different ways on three screens.

Why this architecture and not another. Two references from the course itself:

  • 05-04 (modules) taught that an import is a declared dependency: by writing it you are saying "this does not work without that". A layered architecture is, literally, a rule about which imports are allowed. And because it is a mechanical rule, it can be automated: ESLint (08-02) has import-restriction rules that break the build if somebody writes import { save } from '../data/...' inside domain/. Configuring it is exercise 3.

  • 10-06 showed with data that domain/rules.js was identical across the four versions of the same screen (plain JavaScript, React, Vue and Angular). That fact is the decisive argument: if the domain does not depend on the view, it survives a change of view. And views change. Always.

A note of honesty: for a 100-hour project, four layers may look like ceremony. They are not, for a very concrete reason: the cost of introducing them at the start is one hour; the cost of introducing them when there are already 4,000 mixed-up lines is a week. And in this project in particular you need them, because lesson 11-03 is going to replace the entire data layer and 11-04 is going to test each layer with a different technique.

  1. The environment: repository and toolchain

Now, yes, keyboard. The goal of this section is a repository that, with a single command, verifies everything and comes out green.

11.1 Initialize

# 1 · Folder and repository
mkdir orbita && cd orbita
git init -b main

# 2 · package.json (no questions)
npm init -y

# 3 · Development and build environment
npm install --save-dev vite

# 4 · Code quality (08-02)
npm install --save-dev eslint @eslint/js prettier eslint-config-prettier

# 5 · Tests (08-03, 08-05)
npm install --save-dev jest jest-environment-jsdom \
  @testing-library/dom @testing-library/user-event @testing-library/jest-dom

# 6 · End to end (08-06)
npm install --save-dev cypress

# 7 · Git hooks
npm install --save-dev husky lint-staged
npx husky init

Each line, explained:

  • git init -b main creates the repository with the main branch called main from the outset, which is what GitHub expects and what the deployment workflow in lesson 11-05 will use.
  • vite is the dev server and the bundler. It gives you instant reload on save and, in production, the build with minification and hashing you will see in 11-05. It is the tool Nómada Tasks used from Module 9 onwards.
  • ESLint and Prettier separate responsibilities as 08-02 explained: ESLint looks for errors, Prettier enforces formatting. eslint-config-prettier turns off the ESLint rules that would fight with Prettier.
  • Jest with jsdom for unit and integration tests; Testing Library to query the DOM by role and by text, like a person (08-05).
  • Cypress for the three end-to-end journeys (08-06).
  • Husky and lint-staged run the checks before every commit, on the files you are committing. It is the difference between "CI will tell me" and "I cannot commit broken code".

11.2 The full package.json

{
  "name": "orbita",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "description": "Work manager for small teams. Final project of the JavaScript course.",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "format": "prettier --write .",
    "format:check": "prettier --check .",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "e2e": "cypress run",
    "e2e:open": "cypress open",
    "verify": "npm run lint && npm run format:check && npm run test:cov && npm run build"
  },
  "lint-staged": {
    "*.js": ["eslint --fix", "prettier --write"],
    "*.{css,html,json,md}": ["prettier --write"]
  }
}

The key script is verify. It is the project's contract in one line: lint, formatting, tests with coverage and build. It is exactly what continuous integration will run (11-04) and what you should be able to launch at any moment to know whether the project is healthy. The associated rule is simple and admits no exceptions:

npm run verify has to be green before every push. Always.

The order of the four steps is not accidental: they go from fastest to slowest, so that the first one to fail is the cheapest to detect.

11.3 ESLint configuration with the architecture boundaries

This is the eslint.config.js file, and it contains the part that makes the architecture of section 10 enforce itself:

import js from '@eslint/js';
import prettier from 'eslint-config-prettier';

export default [
  js.configs.recommended,
  prettier,

  // General project rules
  {
    files: ['src/**/*.js'],
    languageOptions: {
      ecmaVersion: 2023,
      sourceType: 'module',
      globals: { window: 'readonly', document: 'readonly', localStorage: 'readonly' }
    },
    rules: {
      'no-console': ['warn', { allow: ['warn', 'error'] }],
      'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      eqeqeq: ['error', 'always'],
      'prefer-const': 'error'
    }
  },

  // BOUNDARY 1: the domain knows nothing about the browser or the other layers
  {
    files: ['src/domain/**/*.js'],
    languageOptions: {
      globals: {} // no window, no document, no localStorage
    },
    rules: {
      'no-restricted-imports': ['error', {
        patterns: [
          { group: ['**/data/**'], message: 'The domain cannot import from data/.' },
          { group: ['**/view/**'], message: 'The domain cannot import from view/.' },
          { group: ['**/application/**'], message: 'The domain cannot import from application/.' }
        ]
      }],
      'no-restricted-globals': ['error',
        { name: 'document', message: 'The domain does not touch the DOM.' },
        { name: 'localStorage', message: 'The domain does not persist; that is data/.' },
        { name: 'fetch', message: 'The domain does not talk over the network; that is data/.' }
      ]
    }
  },

  // BOUNDARY 2: the view knows nothing about persistence
  {
    files: ['src/view/**/*.js'],
    rules: {
      'no-restricted-imports': ['error', {
        patterns: [
          { group: ['**/data/**'], message: 'The view receives data, it does not go looking for it.' }
        ]
      }]
    }
  }
];

What you have just done is important and deserves to be seen clearly: you have turned an architectural decision into an automatic rule. From now on the boundary does not depend on you remembering it on a Friday afternoon. If somebody — you, three weeks from now — writes import { LocalRepository } from '../data/local-repository.js' inside domain/task.js, npm run lint fails, the commit is rejected and CI goes red.

Architectures that cannot be verified automatically always erode. It is not a matter of discipline: it is a matter of nobody remembering a two-month-old document at eleven at night.

11.4 The pre-commit hook

# .husky/pre-commit
npx lint-staged
npm test -- --onlyChanged --passWithNoTests

Two steps: format and check only the files you are committing (lint-staged), and run only the affected tests (--onlyChanged). Deliberately fast: a slow hook is a hook you will end up skipping with --no-verify, and a hook that gets skipped is worth nothing. The full suite is CI's responsibility.

  1. The starting folder structure

orbita/
├── .github/workflows/ci.yml       ← continuous integration (11-04)
├── .husky/pre-commit
├── cypress/
│   ├── e2e/                       ← the 3 journeys (11-04)
│   └── support/
├── docs/
│   ├── scope.md                   ← MVP + what will NOT be done
│   ├── data-model.md              ← entities + R1-R15
│   ├── stories.md                 ← stories with criteria
│   └── adr/                       ← architecture decisions (11-06)
│       └── 0001-layered-architecture.md
├── public/
│   ├── manifest.json
│   └── icons/
├── src/
│   ├── main.js                    ← the single entry point: wires everything up
│   ├── styles/
│   │   ├── base.css
│   │   └── components.css
│   ├── domain/                    ← NO external dependencies
│   │   ├── task.js
│   │   ├── user.js
│   │   ├── board.js
│   │   ├── tree.js                ← recursive traversal (03-07)
│   │   ├── history.js
│   │   ├── rules.js               ← R1-R15 in a single place
│   │   └── errors.js              ← ValidationError, RuleError, DataError
│   ├── data/
│   │   ├── repository.js          ← the CONTRACT (the boundary)
│   │   ├── memory-repository.js
│   │   ├── local-repository.js
│   │   ├── migrations.js          ← (11-03)
│   │   └── seed.js                ← fictional sample data
│   ├── application/
│   │   ├── state.js               ← the single source of truth
│   │   ├── use-cases.js
│   │   └── events.js
│   ├── view/
│   │   ├── dom.js                 ← buildElement, $, $$
│   │   ├── board-view.js
│   │   ├── task-form.js
│   │   ├── report-view.js
│   │   ├── history-view.js
│   │   └── router.js
│   └── util/
│       ├── dates.js               ← including the ISO week for R15
│       ├── format.js              ← Intl
│       └── time.js                ← debounce, throttle
├── test/
│   ├── domain/
│   ├── data/
│   └── view/
├── .gitignore
├── .prettierrc
├── eslint.config.js
├── index.html
├── jest.config.js
├── package.json
├── README.md
└── vite.config.js

Three decisions in this structure, with their rationale:

  • src/ as the code root. It separates code from configuration, which already amounts to ten files at the root. Vite expects it that way by default.
  • docs/ versioned with the code. If the scope document lives in a loose note or an external service, it drifts out of sync within two weeks. Inside the repository, it changes in the same commit as the code it reflects.
  • test/ outside src/, mirroring its structure. There is an alternative of putting tests next to the code (task.test.js beside task.js); both work. The advantage of separating them is that the production build has nothing to exclude and per-layer coverage can be read at a glance.

  1. Git for real: branches, commits and reviews

Until now you may have used Git as a backup: git add ., git commit -m "changes", git push. That works until you need to answer one of these questions, and then it does not work at all:

  • When did the workload report stop working and which change broke it?
  • Why is this odd chunk that I do not dare touch written the way it is?
  • Can I undo the subtasks feature without undoing the three that came after?
  • What exactly went into the version I deployed on Tuesday?

Git history is documentation that writes itself if you give it thirty seconds per commit. These are the project's rules.

13.1 Short branches

One branch per user story. It is born from main, lives between a few hours and three days, and gets merged.

git switch -c feat/h04-subtasks        # born from main
# ... work, several small commits ...
npm run verify                         # mandatory before pushing
git push -u origin feat/h04-subtasks
# review (13.3), merge, and delete the branch

Naming convention, with the same prefixes as the commits:

Prefix What for Example
feat/ New functionality feat/h07-tag-filter
fix/ Fixing a bug fix/duplicated-hours-in-tree
refactor/ Internal change with no behavior change refactor/extract-workload-rules
docs/ Documentation only docs/adr-0003-repository
chore/ Tooling, dependencies, configuration chore/update-vite

Why short. A three-week branch accumulates conflicts, drifts away from main and reaches a point where merging it is scary. A two-day branch merges without a second thought. If a story does not fit in three days, the story is too big: split it (H-04 can be split into "create subtask", "aggregate hours" and "closing rule R13").

13.2 Conventional commits

The Conventional Commits format is a widely adopted standard:

<type>(<optional scope>): <description in the imperative, lowercase, no period>

<optional body: the WHY, not the what>

<optional footer: BREAKING CHANGE, references>

Real examples from this project, from worst to best:

❌ fixes
❌ Changes to the board
❌ fix bug

✅ feat(domain): add subtask linking with cycle detection
✅ fix(report): count only the hours of the tree's leaves
✅ test(domain): cover R13 with the four status combinations
✅ refactor(view): extract row building into taskRow()
✅ docs(adr): record the decision to use a flat repository with parentTaskId

And a commit with a body, which is where the real value lives:

fix(report): count only the hours of the tree's leaves

The workload report was adding the parent task's hours plus those of its
subtasks, duplicating the total. With the sample board it gave 71 h
where the real total is 48 h.

The cause is that a parent's estimatedHours is derived (R12) but
accumulateWorkload() walked every flat task without distinguishing leaves.

Adds isLeaf() in domain/tree.js and filters before accumulating.
Regression test in test/domain/workload.test.js.

Refs: R12, H-11 criterion 4

Nobody writes this to show off. You write it because six months from now, when the report gives an odd number again, this message shows up in git log and saves you two hours. It is the same argument as the R14 change history, applied to code instead of data.

The golden rule of commit size: if you need the word "and" to describe what it does, it is two commits.

What goes in the description and what goes in the body:

Part Content Question it answers
Description What changes, in the imperative, ≤ 72 characters What
Body Context, cause, discarded alternatives Why
Footer References to stories, rules, breaking changes What it relates to

The what is already in the diff. The why only exists in your head, and your head cannot be consulted six months from now.

13.3 Reviews, even when you work alone

"I review my own code" sounds like theater. It is not, if it is done with a procedure that forces a shift in perspective:

  1. Open a Pull Request against main, always. Even if you are the one who will approve it.
  2. Let time pass. Ideally until the next day; at minimum, an hour doing something else. An immediate review sees nothing because your head still fills in the gaps.
  3. Read the full diff in the web interface, not in your editor. The visual context switch is surprisingly effective: you will see things that were invisible in your editor.
  4. Apply the checklist from the next section and write the comments in the PR, not on a piece of paper.
  5. Fix with new commits on the same branch, not by rewriting. Let the correction be visible.

Review checklist (we will extend it in 11-02 and 11-06):

# Question If the answer is "no"…
1 Does the change do one thing? Split it
2 Is there a test that fails without this change? Write it first
3 Does it respect the layer boundaries? ESLint already told you; listen to it
4 Do the names say what they do? Rename; it is cheap now
5 Is there commented-out code, console.log or an undated TODO? Delete it
6 Are the edge cases handled (empty, null, error)? Add them
7 Is it accessible by keyboard and with a screen reader? Go back to the accessibility section
8 Does the commit message explain the why? Rewrite it

And a rule worth gold when somebody else reviews your code or you review theirs: criticize the code, never the person. "This function does three things" instead of "you made a mess". Apply it to yourself too: the goal is a better product, not a verdict.

  1. Milestones and honest estimation

14.1 The estimation problem

Everybody estimates badly and always in the same direction: too low. The cause is well studied and is called the planning fallacy: when we estimate we imagine the path where everything goes well, because it is the only one we can imagine in detail. Setbacks, by definition, cannot be enumerated.

The honest method has three steps:

  1. Estimate each story in hours of effective work, assuming there are no interruptions and that you know how to do it.
  2. Multiply by a correction factor according to how familiar the problem is to you.
  3. Add 20 % of buffer to the project total for whatever is in no story at all (configuring things, fixing the deployment, an odd Jest failure).
Familiarity level Factor Example in Orbita
I have done it several times × 1.3 Form with validation (06-07)
I have done it once, guided × 2 Persistence with migrations (07-01, but the migrations are new)
I understand it but have never done it alone × 3 Subtask tree with rules R12/R13
I do not know where to start × 4 or research first CSV export with special characters

A full example, story H-04:

Item Hours
Naive estimate ("a tree, half a day") 4
Factor × 3 (you have never implemented a tree with business rules) 12
Downward adjustment: the MVP only exposes one level 10

The 10 hours in the table in section 7 come from there, not from a hunch.

Orbita total: 105 h of stories + 21 h of buffer (20 %) = 126 hours. At 8 hours a week, about 16 weeks. If that seems like a lot, you have two legitimate ways out: put in more hours per week, or cut the Coulds (22 h) and land at 13 weeks. What is not a way out is deciding that it will really be 60 hours.

14.2 The milestones

A milestone is not a date: it is a state of the product that can be shown. Each one ends in something demonstrable.

Milestone What can be shown when it is finished Stories Hours Lesson
H1 · Foundations Green repository: npm run verify passes, with a trivial test and an empty deployable page 10 11-01
H2 · Living domain The domain with R1–R15 and its tests green, exercisable from the console H-01…H-03 25 11-02
H3 · Full vertical slice Creating, viewing, assigning and breaking down a task end to end in the browser H-04…H-06 24 11-02
H4 · Data that lasts Filters and local persistence with migrations; reloading loses nothing H-07…H-09 22 11-03
H5 · Usable product History, report, empty states and keyboard navigation H-10…H-13 26 11-04
H6 · Published Deployed with HTTPS, CI green, README and demo H-14…H-16 19 11-05, 11-06
gantt
    title Orbita — 16-week plan at 8 h/week
    dateFormat YYYY-MM-DD
    axisFormat W%W

    section Foundations
    H1 · Repository, tooling, CI            :h1, 2026-09-21, 10d
    section Domain
    H2 · Entities, rules R11-R15, TDD       :h2, after h1, 22d
    section Product
    H3 · Full end-to-end vertical slice     :h3, after h2, 21d
    H4 · Filters and persistence            :h4, after h3, 19d
    H5 · History, report, accessibility     :h5, after h4, 23d
    section Delivery
    H6 · Deployment and documentation       :h6, after h5, 17d
    20 % buffer                             :res, after h6, 21d

Two observations about the Gantt chart:

  • The buffer appears in the diagram. If the buffer is not drawn, it does not exist: the first setback will eat it and you will write the plan off. Drawn, it is a part of the plan being consumed, and you can see it.
  • The milestones do not overlap. It is tempting to draw tasks in parallel, but you work alone: parallelism is an illusion whose only function is to make the plan look shorter.

How the plan is used. Not as a promise, but as a measuring instrument. At the end of each week, note the hours spent and the stories closed. If by the end of H2 you are 40 % over the projected time, your correction factor is too low: raise it for the rest of the plan instead of hoping to catch up. Recovering lost time almost never happens; adjusting the model does.

  1. The Definition of Done

The Definition of Done answers a question that sounds silly and is not: when is a story finished? With no written answer, "done" means "it works on my machine when I do what I expect", and that is the origin of 80 % of a personal project's debt.

It is written once, at the start, and applied to every story without exception:

## Definition of Done (v1.0)

A story is DONE when all of the following is true:

### Functionality
- [ ] All its acceptance criteria are met and have been checked by hand
- [ ] Edge cases have defined behavior: empty list, `null` value,
      very long text, out-of-range number, network error
- [ ] The empty, loading and error states are implemented

### Code
- [ ] `npm run verify` is green
- [ ] It respects the layer boundaries (ESLint does not complain)
- [ ] No `console.log`, no commented-out code, no undated `TODO`
- [ ] Names are in the project's language and are consistent

### Tests
- [ ] Unit tests for the affected domain
- [ ] At least one integration test per acceptance criterion
      that involves the interface
- [ ] Every business rule touched has a test for the case that
      DOES pass and the case that does NOT
- [ ] Domain coverage does not drop below 90 %

### Accessibility
- [ ] Full keyboard navigation, with focus always visible
- [ ] Correct semantic elements (no clickable `<div>`s)
- [ ] Relevant changes are announced with `aria-live`
- [ ] Minimum contrast of 4.5:1 on normal text
- [ ] axe reports no serious-level issues

### Performance
- [ ] Within the budget from section 16
- [ ] No memory leaks after entering and leaving the screen three times

### Documentation
- [ ] README updated if installation or usage changes
- [ ] ADR written if an architectural decision has been made
- [ ] Commit message with the why

Yes, it is long. And yes, it gets followed, for two reasons:

  1. Most of the boxes are checked by a machine: npm run verify covers seven of them. Only a few require you to do something by hand.
  2. It is cheaper than the alternative. Every box on this list represents a real problem that shows up when it is not checked, and all of them are more expensive to fix later.

A practical tip: save it as a Pull Request template in .github/pull_request_template.md. That way it shows up by itself, rendered as a task list, every time you open a PR. What does not show up by itself does not get done.

  1. The performance and accessibility budget

Lesson 09-01 left behind an idea you have to apply now, not at the end: measure before optimizing. And its corollary, which is what this section is about: a goal without a number is not a goal.

Nómada Tasks measured its baseline at the end, with the application already built, and found ten problems all at once. You are going to do the opposite: set the budget now, with the application empty, and let CI watch over it from day one. The difference is enormous: a budget set at the start is broken on the day it breaks, by a single suspicious change; set at the end, it is broken by the accumulation of thirty changes and nobody knows which one did it.

16.1 Performance budget

The reference column holds the measured Nómada Tasks numbers from lesson 09-01 (baseline with 600 tasks, after all the Module 9 optimizations):

# Metric How it is measured Orbita budget Nómada Tasks (ref.)
1 Initial JS (compressed) Vite build / Network ≤ 60 kB 58.3 kB
2 Requests for the first screen Network ≤ 4 3
3 LCP (simulated mobile, Slow 4G, 4× CPU) Lighthouse CI ≤ 2.5 s 1.9 s
4 CLS Lighthouse CI ≤ 0.1 0.02
5 INP when filtering Performance, Interactions ≤ 200 ms 42 ms
6 render() with 500 tasks User Timing ≤ 50 ms 31 ms (600 tasks)
7 DOM nodes in the document Performance, counter ≤ 1,500 1,194
8 Memory retained after 3 navigation cycles Memory, 3 snapshots ≈ 0 no leaks
9 Lighthouse performance score Lighthouse CI ≥ 90

The budgets are somewhat looser than the Nómada Tasks numbers, and that is deliberate: Orbita has more screens and more functionality. An impossible budget is ignored in the first week; an achievable but demanding one is respected.

The budget rule: if a change breaks it, there are three legitimate ways out — optimize the change, drop the feature, or raise the budget consciously and write down why. The fourth way out, "I will look at it later", is what produces 2 MB applications.

16.2 Accessibility budget

# Criterion How it is checked Threshold
1 No serious axe issues Automated axe in CI 0
2 Lighthouse accessibility score Lighthouse CI ≥ 95
3 Everything reachable by keyboard Manual, per screen 100 %
4 Focus always visible Manual + CSS :focus-visible Always
5 Normal text contrast axe / DevTools ≥ 4.5:1
6 Large text and UI element contrast axe / DevTools ≥ 3:1
7 Usable at 200 % zoom Manual No loss of content or function
8 Important changes announced Manual with a screen reader aria-live present
9 No information conveyed by color alone Manual review 0 cases

Criterion 9 has an immediate consequence in Orbita and it is worth seeing now: the overdue task from R10 cannot be highlighted in red alone, and the overloaded cell from R7 cannot be highlighted in amber alone. Both need text or an icon with alternative text. If you know it now, you design it right; if you discover it in the accessibility review, you rebuild it.

Thresholds 1, 2, 5 and 6 are verified by the machine. The rest are manual and belong in the Definition of Done. Lesson 11-04 sets up the full automation.

  1. The README.md template

The README.md is the first thing anybody landing on your repository sees: a reviewer, a recruiter, or you a year from now. It is created now, with gaps, and filled in as the project advances. A README written on the last day shows, and not in a good way.

# Orbita

Work manager for small teams: tasks, subtasks, workload per person and
change history. Final project of the JavaScript course, built
**without frameworks**.

🔗 **Demo:** https://<user>.github.io/orbita/
📸 *(screenshot or 10 s GIF here)*

---

## What problem it solves

A team of three or four people needs to know who is doing what, what is
blocked and who is overloaded. The big tools are overkill; a spreadsheet
falls short as soon as there are rules.

## What it does

- Tasks with status, priority, tags, assignee and due date
- **Subtasks** with aggregated hours and progress
- **Multiple filtering** by tags (*all* / *any* mode),
  assignee and status, with the filter saved in the URL
- **Workload report** per person and ISO week, with an overload warning
- **Immutable history** of every change
- Works **offline** and installable as a PWA

## Technical decisions

| Decision | Why | ADR |
|---|---|---|
| No framework | The product is a single SPA with rich domain logic; the cost of maintaining our own infrastructure is acceptable | [ADR-0002](docs/adr/0002-no-framework.md) |
| Layered architecture | The domain must be testable without a browser and survive a change of view | [ADR-0001](docs/adr/0001-layered-architecture.md) |
| Flat tree with `parentTaskId` | Lookup and move are O(1); it serializes trivially | [ADR-0003](docs/adr/0003-flat-tree.md) |
| `localStorage` with numbered migrations | Expected volume < 1 MB; migrate without losing data | [ADR-0004](docs/adr/0004-persistence.md) |

## How to run it

git clone https://github.com//orbita.git cd orbita npm install npm run dev # http://localhost:5173

## How to test it

npm test # unit and integration npm run test:cov # with coverage report npm run e2e # end-to-end journeys npm run verify # all of the above + lint + build

## Architecture

src/domain/ Rules R1-R15. No dependencies. Tested in Node. src/data/ Persistence and network. One interface, three implementations. src/view/ DOM, events and accessibility. Does not know where data comes from. src/application/ Use cases and state. Joins the three above.

## Known limitations

- Single device: no synchronization between browsers (v2.0)
- No authentication: data lives in the browser and **is not private**
- Subtask depth limited to one level in the interface
- Tested on recent Chrome, Firefox and Safari; no IE support

## Roadmap

- [ ] v1.1 · Calendar view and CSV export
- [ ] v1.2 · Three levels of subtasks
- [ ] v2.0 · Backend with authentication and synchronization

## License

MIT — see [LICENSE](LICENSE).

The sections almost nobody writes and that impress reviewers the most are "Technical decisions" and "Known limitations". The first demonstrates judgment; the second demonstrates honesty and awareness of the limits of your own work, which is exactly what is looked for in someone you are going to work with. Lesson 11-06 develops the README in depth.

Common Mistakes and Tips

Starting to code before the data model is written. It is the most expensive mistake of all, because it does not hurt at first: it hurts in week four, when you discover that assignee should have been a reference and it is already in twenty places. Half an hour of document saves two days of refactoring. Write the model, read it out loud, and only then open the editor.

Confusing MVP with "shoddy version". An MVP has fewer features, not less quality. The ones that are there are finished: tested, accessible and with their empty and error states. An application with three impeccable features demonstrates vastly more than one with twelve half-done — and in an interview, the second works against you.

Not writing the list of what will NOT be done. Without that list, every new idea seems reasonable because nothing says otherwise. With the list, adding something requires explicitly crossing it off, and that small act of friction filters out 90 % of Tuesday-afternoon ideas.

Estimating for the best case. Your naive estimate is the time it would take if everything went well and you already knew how to do it. Multiply by the factor in the table in section 14 without negotiating with yourself, and add the 20 % buffer. A plan that is met motivates you; one that is broken from week two gets abandoned.

Leaving the architecture as an intention. "I will be careful not to import data/ from domain/" is a sentence with a two-week expiry date. Configure the ESLint rule from section 11.3 on day one: turn your good intention into a build error, which is the only thing that does not get forgotten.

Deferring accessibility until "when it works". Adding accessibility at the end means rebuilding half the view: clickable <div>s have to become <button>s, focus has to be managed, announcements have to be introduced. Done from the start it costs practically nothing, because almost all of it comes down to using the right HTML element.

An empty README for months. Write the first version today, even if half of it is gaps. Every time you make a decision, add a row to the technical decisions table. Reconstructing the why of twenty decisions on the last day is impossible: you will not remember them.

Long branches. A three-week branch is a parallel project you will one day have to reconcile. If a story does not fit in three days, split it. If you do not know how to split it, that is a sign you do not understand it well enough yet — and that is valuable information too.

Tip · Write the scope document for "you, three months from now". That reader remembers nothing, has no context and only has ten minutes. If the document works for them, it works for you and it will work for anybody.

Tip · Set a fixed day and time to work, and protect them. The number one cause of abandoning personal projects is not technical difficulty: it is losing the rhythm. Two fixed three-hour sessions a week yield vastly more than "whenever I can".

Tip · Do the first deployment with the application empty. In milestone H1, before writing any logic. You will discover the configuration problems when they cost nothing, instead of in the last week with everything on the line. It is the same principle as the pilot phase of the migrations in 10-06.

Exercises

These three exercises are milestone H1 of your project. When you finish them you will have the scope document, the data model and a green repository: everything you need to start building in the next lesson.

Exercise 1 — The scope document.

Create docs/scope.md in your repository with these seven sections:

  1. The product in one sentence (the format from section 2): who uses it, what it does, what problem it solves.
  2. The MVP: the feature table with the "MVP?" column and its reason. At least 12 candidates, and at least 5 with "No".
  3. Out of scope: the three-column table from section 5, with at least 8 rows.
  4. User stories: between 12 and 18, using the As a / I want / so that template. At least four fully developed with Given / When / Then criteria (at least 3 criteria each), including at least one accessibility criterion.
  5. MoSCoW prioritization: the table from section 7 with estimate, milestone and dependencies. Verify the 60 % rule.
  6. Milestone plan: the six milestones with their demonstrable deliverable and their Mermaid Gantt chart, with the buffer drawn in.
  7. Definition of Done: adapted to your project, with at least 18 checkboxes spread across the six categories.

Exercise 2 — The data model and the rules.

Create docs/data-model.md with:

  1. An entity diagram in Mermaid (erDiagram) with all your entities and their relationships.
  2. A table per entity with field, type, whether it accepts null, rules and a note justifying the chosen type.
  3. The table of rules R1–R15 (or whichever correspond to your domain, if you chose another one) with: description, layer where it is enforced and error it throws.
  4. The three modeling decisions from section 8.3 applied to your domain, each with its discarded alternatives and the reasoning.
  5. A complete example of fictional data in JSON with at least 8 entities, including an edge case for each new rule.
  6. A "Personal data" section listing which fields are personal, what they are used for and how long they are kept.

Exercise 3 — The green repository.

Leave the repository in the exact state lesson 11-02 starts from:

  1. A repository with main, a first conventional commit and a correct .gitignore (node_modules, dist, .env, coverage, cypress/videos).
  2. package.json with the ten scripts from section 11.2, including verify.
  3. eslint.config.js with both architecture boundaries configured and checked.
  4. .prettierrc, jest.config.js (with testEnvironment: 'jsdom' and coverage thresholds) and vite.config.js.
  5. A pre-commit hook with Husky and lint-staged, verified with a real commit.
  6. The folder structure from section 12, with a .gitkeep in the empty ones.
  7. A test that proves the boundaries work: write src/domain/boundary-test.js with import ... from '../data/repository.js', check that npm run lint fails with your custom message, and delete it. Document the result in ADR-0001.
  8. README.md with the template from section 17 (the gaps get filled in later).
  9. npm run verify green.

Solutions

There are no solutions in the form of code, because the project is yours. These are the rubrics each deliverable is assessed against. Score yourself honestly: the goal is not the grade, it is spotting what is missing before it costs you dearly.

Rubric for exercise 1 — Scope document (30 points)

Criterion 0 · Insufficient 1 · Acceptable 2 · Good 3 · Excellent
Product sentence Does not exist or describes the implementation Says what it does Says who, what and what for Also distinguishes it from obvious alternatives
MVP trimming Everything is MVP Something is discarded ≥ 5 reasoned "No"s Every "No" passes the question test from section 4
Out of scope Does not exist A list of things A table with reasons Also with a realistic "when it gets reconsidered"
Stories Vague or described as technical tasks Correct format ≥ 4 with verifiable criteria The criteria translate directly into tests
Accessibility in criteria Absent Mentioned separately ≥ 1 acceptance criterion Present in every story with an interface
MoSCoW Not prioritized Categorized With estimates and dependencies Meets the 60 % rule and proves it with the arithmetic
Estimation No correction factor Uniform factor Factor by familiarity Also with the 20 % buffer drawn in the Gantt chart
Milestones Dates only With deliverables Every milestone is demonstrable Ordered by real dependency, with no fake overlaps
Definition of Done Does not exist Short list ≥ 18 checkboxes across 6 categories Most of them machine-verifiable
Usefulness to a third party Incomprehensible without you Understandable with effort Understandable on its own Somebody could continue it without talking to you

Pass threshold: 20/30, with at least 2 points in "MVP trimming" and in "Stories". If you fall short there, everything else is built on sand.

Rubric for exercise 2 — Data model (24 points)

Criterion Insufficient (0) Acceptable (1) Good (2) Excellent (3)
Entities Relationships missing All present With correct cardinalities Readable, annotated Mermaid diagram
Types Ambiguous or "string" for everything Defined With justification Also with explicit closed sets
Nullability Unspecified Marked With consistent criteria Distinguishes "not applicable" from "unknown"
New rules Fewer than 3 5 rules stated With layer and error assigned Each with its edge case documented
Deletion decision Not considered Chosen With discarded alternatives With interface consequences described
Tree structure Undecided Chosen Justified With the cost of the operations analyzed
Sample data Absent Present With edge cases Directly reusable as a test seed
Personal data Not mentioned Identified With a purpose Also with retention period and minimization

An unmistakable warning sign: if your rules table has fewer than five new rules specific to your domain, your product is a CRUD. Add hard rules — they are what makes the domain interesting, what gives the 11-04 tests meaning and what tells well in an interview.

Rubric for exercise 3 — Repository (binary verification)

There are no shades of gray here: each point is either met or not met.

# Check Command Expected
1 The repository exists and has main git branch --show-current main
2 The first commit is conventional git log --oneline -1 Starts with chore: or feat:
3 .gitignore is correct git status --short No node_modules/ or dist/
4 The scripts exist npm run The 10 from section 11.2
5 Lint passes npm run lint No errors
6 Formatting is applied npm run format:check No differences
7 The tests pass npm test At least 1 test, 0 failures
8 The build works npm run build dist/ generated
9 The hook fires commit with a lint failure The commit is rejected
10 The boundary works add the forbidden import npm run lint fails with your message
11 Everything together npm run verify Green

Point 10 is the one that really matters and the one almost nobody checks. A badly configured ESLint rule does not fail: it simply does nothing, and you believe you are protected for three months. Trigger the error on purpose, check the message, and only then delete it.

Final self-assessment for milestone H1. Answer honestly before moving on to the next lesson:

Question Yes / No
Can I describe my product in one sentence without hesitating?
Do I know exactly what I am NOT going to build?
Could I write the first test for a rule today without inventing the model?
Does my plan have a buffer, or have I assumed everything will go well?
Is npm run verify green right now?
Could I explain my layer boundaries to somebody else in two minutes?

Six "yes"es and you can move on. A single "no" and it is worth going back before writing code, because every one of those "no"s multiplies into weeks of work later on.

Conclusion

You have done what almost nobody does in a personal project: planned before coding. And not with a decorative plan, but with instruments that will be used every week of this module.

You know that from here on you write the code: Nómada Tasks stops being the example handed to you solved and becomes the reference to compare against, with its six tasks, its 48 hours, its rules R1–R10 and its measured numbers. You have a default product — Orbita — with six extensions that force you to solve problems the course sidestepped: users as entities, a subtask tree, multiple filtering, a view that is not a list, a report that produces a file and an immutable history that grows without end. And you have three alternative domains — Capacity, Streak, Shelf — with the six extensions fully mapped, because the method is the same and what matters is that you care about it.

You know how to trim: the MVP is what cannot be removed without breaking the product sentence, and the test is a single question. Out of fifteen candidates, eight made it in. And you know that the half left out has to be written down, with its why and its when-it-gets-reconsidered, because a feature that is on no list is a feature that will show up on a Tuesday afternoon.

You know how to write user stories that say for whom and what for, with Given / When / Then criteria so precise that they translate into tests almost word for word — including the accessibility criteria, which go inside the story and not in a separate list reviewed at the end. And you know how to prioritize them with MoSCoW respecting the 60 % rule, which is what turns a plan into a plan and not a bet.

You have the data model written before the code, with its entity diagram, its justified types and the three decisions that had to be made explicitly: deactivating users instead of deleting them, tags as normalized text, and a flat tree with parentTaskId rather than a nested one. And you have the new rules R11 to R15 — referential integrity with active users, a cycle-free tree with hours from the leaves, closure blocked by open subtasks, an immutable history, and permissions with workload per ISO week — all living in the domain, because a rule that only exists in the form does not exist.

You have a layered architecture with three boundaries that are never crossed — the domain does not know there is a browser, the view does not know where data comes from, the data does not know what gets rendered — and, more importantly, you have them verified by ESLint: you have turned an intention into a build error, which is the only thing that does not erode. The argument is not theoretical: in 10-06, domain/rules.js was identical across the four versions of the same screen.

You have the environment set up: Vite, ESLint and Prettier, Jest with jsdom and Testing Library, Cypress, Husky and lint-staged, with a verify script that sums up the project's contract in one line and a rule with no exceptions. You have Git for real: short branches per story, conventional commits whose body explains the why — the only thing the diff cannot tell you six months from now — and reviews of your own code with distance, in the web interface and with a checklist.

You have an honest plan: 105 hours of stories, a correction factor by familiarity, 20 % buffer drawn in the Gantt chart, six milestones each ending in something that can be shown. You have a Definition of Done whose boxes are mostly checked by a machine, saved as a Pull Request template so that it shows up by itself. And you have the performance and accessibility budget set at the start, with the Nómada Tasks numbers as the reference: ≤ 60 kB, LCP ≤ 2.5 s, ≤ 1,500 nodes, 0 serious axe issues, 4.5:1 contrast and nothing communicated by color alone.

Milestone H1 is closed: scope document, data model with its rules, and a repository that answers green. The page is empty, and that is exactly right, because now you know what you are going to put on it and why.

What comes next is putting it there. And there is a way of doing that which is not the intuitive one — from the inside out, starting with the rules and ending with the pixels, and one complete feature before all of them half-done: it is Building the Project.

JavaScript Course: From Beginner to Advanced

Module 1: Introduction to JavaScript

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved