Capstone: build a small library end to end · Lesson 72 of 77 · concept
The brief, and what 'done' means
About 60 minutes.
By the end of this lesson you can
- Turn a one-sentence brief into acceptance criteria a stranger could check.
- Write down what version 1 will not do, as deliberately as what it will.
- Name the public functions of a library before writing any of their bodies.
- Spot a criterion that cannot be checked and rewrite it as one that can.
The most expensive mistake happens before you type anything
Why it matters
Someone says: build me something to track my reading. You know enough JavaScript now to start typing immediately, and that is exactly the trap. Two hours later you have four functions, none of which you can say is finished, because nobody wrote down what finished means. You keep going because stopping feels like giving up, and the thing grows in every direction at once.
Everything you have built in this course so far came with the target already drawn: the exercise told you what the function had to return. Real work does not arrive that way. It arrives as a sentence, and the first job — before data shapes, before functions, before tests — is to turn that sentence into a short list of statements that are either true of your code or not.
This lesson has no exercise for the sandbox to grade, and that is honest rather than lazy: the deliverable is a written plan. It is still work. Do it badly and the next four lessons have nothing to aim at.
An acceptance criterion is a sentence that can be wrong
Explanation
An acceptance criterion says what a named function does with a named input, in terms someone else can observe. 'summarise of an empty list reports zero pages' is a criterion: you can run it and see. 'The library should be easy to use' is not — two reasonable people will never agree on whether it is met, so it can never be ticked off.
The test for a criterion is simple and mechanical: can you imagine the two outcomes? If you cannot describe what failing it would look like, it is a wish, not a criterion. Rewrite it until you can. 'Easy to use' becomes 'every public function takes one argument and returns a value; none of them needs to be called in a particular order'. That one you can check.
Criteria are also where you say what happens when things go wrong, because that is the half beginners forget. 'An invalid entry is rejected with a message naming the field that is wrong' is a promise about failure, and lesson 4 is nothing but the code that keeps it.
The brief we will build: readlog
Explanation
Here is the sentence: 'I want to keep a log of my reading sessions and see what it adds up to.' That is the whole brief, and it is typical — short, friendly, and nowhere near enough to code from.
Turned into criteria, it becomes this. One entry arrives from a form as messy text and must become one clean record. A list of records must produce a summary: how many sessions, how many pages, how many minutes, which books. The pages must be totalled per tag, so 'how much fiction did I read' has an answer. An entry that is not sensible must be rejected with a message naming the field. A whole list with some rubbish in it must still load the good entries and report the bad ones by position. And the finished thing must produce one line of report text.
That is six public functions: toSession, summarise, pagesByTag, validateSession, createSession, loadSessions, and report — seven, counting report. You will build every one of them across lessons 2 to 5, and review the lot in lesson 6. Nothing else gets added. When you catch yourself thinking 'it would be nice if it also…', that thought belongs in the out-of-scope list, not in the code.
Writing down what it will not do
A mental model
The out-of-scope list is the fence around the work. For readlog v1 it says: no dates (a session has minutes, not a calendar), no saving or loading files, no printing to the screen from inside the library, no fetching anything, no reading anything the caller did not hand in. Some of those are choices; some are the sandbox's limits, stated plainly rather than discovered later.
Useful way to think about it: every feature you promise is a promise to keep it working forever. A small library that does six things perfectly is worth more than a large one that does twenty things you are afraid to touch. Scope is not a limitation you accept reluctantly, it is the thing that makes finishing possible.
And an out-of-scope list is a kindness to whoever reads your code next, including you in three weeks. Without it, every missing feature looks like a bug or an oversight. With it, the absence is a decision someone made on purpose.
The skeleton first: every function exists, none of them works
An analogy
Builders put up the frame of a house before a single wall is finished. You can walk the frame, see how big the rooms are and notice that the kitchen has no door, long before anything is plastered. Code has the same move, and it is called a walking skeleton: write every public function with the right name and the right arguments, and have each one throw an error saying it is not implemented yet.
This costs about ninety seconds and buys two real things. You see the whole surface at once, so a badly named or missing function is obvious now rather than after you have written its body. And every call fails loudly, in the way lesson 7 argued for: nobody can accidentally use a half-built function and get a quietly wrong answer, because a stub that throws cannot be mistaken for one that works.
The stubs come down one at a time as the lessons go on. When the last throw is gone and every criterion is ticked, v1 is done — not perfect, done.
Each criterion becomes exactly one check
Explanation
In module 11 you wrote your own assertion helper: something that compares what happened with what should have happened and prints PASS or FAIL. That helper is the bridge between the plan and the code. One criterion, one call to it, one line of output.
Writing the checks straight from the criteria — before the bodies exist — means every one of them fails at first, and that is the point. A list of red checks is a to-do list that cannot lie to you about how far you have got. When all of them are green, and only then, the brief has been met.
The third worked example below writes three of them by hand. Two pass because they lean on JavaScript you have already met, and one fails on purpose, so you can see what a criterion looks like in the moments before it is true.
Beware the criterion nobody can fail
A common mistake
The most common bad criterion sounds professional and means nothing: 'the code should be clean', 'the library must be performant', 'errors should be handled properly'. Each one feels like a standard and none can be checked, so each becomes an argument later, when arguing is most expensive.
The fix is to replace the adjective with an observation. 'Performant' becomes 'summarise handles a thousand sessions without the page freezing'. 'Errors handled properly' becomes 'no public function returns undefined when given an invalid entry; it either returns a reported failure or throws'. 'Clean' becomes 'no function is longer than about twenty lines and none needs a comment to say what it does'.
If you cannot make an adjective observable, cut it. A criterion that can never fail is not a low bar, it is no bar at all.
Recap
Recap
A brief is a sentence; acceptance criteria are the checkable statements you turn it into, including the ones about failure. A criterion you cannot imagine failing is a wish — rewrite it. The out-of-scope list is written down with the same care as the scope, because it is what lets you finish. A walking skeleton gives you every function name up front, each one throwing until it is real. Every criterion becomes exactly one check, red first, and v1 is done when they are all green.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
The criteria, written down where you can count them
Acceptance criteria are just sentences, so they can live in your project as an array you can read, count and tick off. Notice that each one names a function, an input and a visible result.
1const acceptanceCriteria = [2 "createSession turns one raw entry into a record with a trimmed title.",3 "summarise of an empty list reports zero pages, not undefined.",4 "An invalid entry is rejected with a message naming the field that is wrong.",5 "No function in the library prints anything or reads anything it was not given.",6];7 8for (let index = 0; index < acceptanceCriteria.length; index += 1) {9 console.log("AC" + (index + 1) + ": " + acceptanceCriteria[index]);10}11 12console.log("Criteria to satisfy before v1 is done: " + acceptanceCriteria.length);- line 2
A criterion names the function (createSession), the input (one raw entry) and what you would see (a record with a trimmed title). All three, or it is not checkable.
- line 4
The failure case is a criterion too. If you do not write it down now, the code will decide it for you later, badly.
- line 12
Four. That is the definition of done for version 1 - not a feeling, a number you can count.
What it prints
AC1: createSession turns one raw entry into a record with a trimmed title.AC2: summarise of an empty list reports zero pages, not undefined.AC3: An invalid entry is rejected with a message naming the field that is wrong.AC4: No function in the library prints anything or reads anything it was not given.Criteria to satisfy before v1 is done: 4
Every function exists; none of them works yet
The skeleton of the library. Each stub throws, so the shape is visible and no half-built function can be used by accident. This is ninety seconds of typing that saves an afternoon.
1function createSession(raw) {2 throw new Error("createSession is not implemented yet");3}4 5function summarise(sessions) {6 throw new Error("summarise is not implemented yet");7}8 9try {10 createSession({ title: "Dune" });11} catch (error) {12 console.log("Caught:", error.message);13}14 15console.log("Functions declared so far: " + [createSession, summarise].length);- line 2
An honest stub. new Error(message) builds an error carrying that text, and throw stops the function immediately - nobody gets a wrong answer from an unfinished function.
- line 10
Calling a stub is how you check the name and the arguments feel right before committing to the body.
- line 12
console.log with two arguments prints one line with a space between them: the label, then the error's message.
What it prints
Caught: createSession is not implemented yetFunctions declared so far: 2
Turning a criterion into a check you can run
The bridge between the plan and the code: a criterion becomes one call to a small comparison helper. Two of these are already true; the third is the one still to build.
1function check(label, actual, expected) {2 const pass = JSON.stringify(actual) === JSON.stringify(expected);3 console.log((pass ? "PASS - " : "FAIL - ") + label);4}5 6check("a title arrives trimmed", " Dune ".trim(), "Dune");7check("no sessions means no pages", [].length, 0);8check("this criterion is not met yet", 1 + 1, 3);- line 2
Comparing the JSON text of both values is a blunt but reliable way to compare lists and records, not just single values. It is exactly what the graded exercises do.
- line 6
The label is the criterion, word for word. When this fails in six weeks, the output tells you which promise broke.
- line 8
A red check is not a problem, it is a to-do item you cannot forget. Start with them all red.
What it prints
PASS - a title arrives trimmedPASS - no sessions means no pagesFAIL - this criterion is not met yet
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
Which of these is an acceptance criterion you could check without asking the author what they meant?
The library is still a skeleton. What does this print - and why is it better than returning 0 for now?
function summarise(sessions) { throw new Error("summarise is not implemented yet");}try { console.log(summarise([]));} catch (error) { console.log("still to do: " + error.message);}
Why write down what version 1 will deliberately NOT do?
No exercise in this lesson
The deliverable is a written plan: acceptance criteria and an out-of-scope list for your own brief. A plan is judged by a person reading it beside the code that follows, and the sandbox can only compare values, so grading it would reward guessing our wording. Lessons 2 to 5 grade that code.
Worth remembering
- What makes a sentence an acceptance criterion rather than a wish?
You can imagine it failing. It names a function, an input and an observable result, so running the code settles it - no opinion required.
- What is the out-of-scope list for?
It fences the work so it can be finished, and it tells a later reader that a missing feature was a decision rather than an oversight.
- What is a walking skeleton, and why does each stub throw?
Every public function written with its real name and arguments, its body throwing 'not implemented yet'. Throwing means no half-built function can be mistaken for a working one.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: new Error(message) creates an error object whose message property holds that text, and error.name is 'Error' for the base constructor.
Check there: throw ends execution of the current function and passes control to the catch block.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown