style(no-slop): remove every em-dash + banned words across all modules + capstone

Apply the no-ai-slop standard (now binding in AGENTS.md): the em-dash character is
banned outright (restructured, not blind-replaced), plus the banned word/phrase
list (delve, leverage, robust, seamless, truly, unlock, etc.). 0 em-dashes remain
in modules + capstone; the only "robust" left is the planted M10 ai-change.patch
trap. Module H1 titles use a colon separator.

All deliberate teaching devices preserved; labs compile/parse (py/sh/yaml/json);
no junk. AGENTS.md updated with the hard no-slop rules.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfzV5QvtPDz8LJS3Pu5VLT
This commit is contained in:
2026-06-22 23:21:09 -04:00
parent 513d7e7ac8
commit 389ac2e460
99 changed files with 1324 additions and 1315 deletions
+51 -51
View File
@@ -1,4 +1,4 @@
# Module 12 When It Goes Wrong: Revert, Reset, and Recovery
# Module 12: When It Goes Wrong: Revert, Reset, and Recovery
> **A bad change already shipped. Now what?** Recovery is its own skill. Knowing the *right* undo for
> the situation is the difference between a clean five-second fix and force-pushing over your
@@ -8,15 +8,15 @@
## Prerequisites
- **Module 2 Version Control as a Safety Net.** You can commit, read a `diff`, and `git restore`
- **Module 2: Version Control as a Safety Net.** You can commit, read a `diff`, and `git restore`
uncommitted changes. This module is the rest of the undo toolkit: undoing things that are *already
committed*, including things already shared.
- **Module 6 Branches: Sandboxes for Experiments.** You merge branches. The headline example here
- **Module 6: Branches: Sandboxes for Experiments.** You merge branches. The headline example here
is undoing a bad *merge*, which only makes sense once you've made one.
- **Module 8 Remotes and Hosting.** You've pushed history somewhere others can pull it. That's what
makes "shared history" real and it's the dividing line between the safe undo and the dangerous
- **Module 8: Remotes and Hosting.** You've pushed history somewhere others can pull it. That's what
makes "shared history" real, and it's the dividing line between the safe undo and the dangerous
one. Module 8 was the *backup* half of the backup-and-recovery thread; this is the *recovery* half.
- **Modules 1011 Reviewing Code You Didn't Write / Collaboration.** A bad change usually arrives
- **Modules 1011: Reviewing Code You Didn't Write / Collaboration.** A bad change usually arrives
as a merged PR, and other people (and agents) are pulling from the same branch. Recovery has to be
safe for *them*, not just you.
@@ -29,13 +29,13 @@ If you've parachuted in: you minimally need to be comfortable with commits, bran
By the end of this module you can:
1. Choose the correct undo for a situation `restore`, `revert`, or `reset` and explain why the
1. Choose the correct undo for a situation (`restore`, `revert`, or `reset`) and explain why the
other two would be wrong.
2. Cleanly undo a change that's already on shared history with `git revert`, including the hard case:
reverting a merge commit.
3. Recover commits you thought you'd destroyed using `git reflog`, even after a `reset --hard`.
4. Drop named recovery points with tags (and host releases) before risky work.
5. State precisely where Git's recovery powers end what it is *not* a backup for, and why that
5. State precisely where Git's recovery powers end: what it is *not* a backup for, and why that
matters before you trust it.
---
@@ -45,23 +45,23 @@ By the end of this module you can:
### Three undos, three blast radii
Git has more than one "undo," and the failure mode is using the wrong one. They differ by *what they
touch* and *whether they're safe once history is shared*. Hold this table in your head the rest of
touch* and *whether they're safe once history is shared*. Hold this table in your head; the rest of
the module is just filling it in:
| Command | Undoes | Touches history? | Safe on shared history? |
|---------|--------|------------------|--------------------------|
| `git restore <file>` | **Uncommitted** edits in your working tree | No | Yes there's nothing shared to break |
| `git revert <commit>` | An **already-committed** change, by writing a *new* inverse commit | No it *adds* | **Yes** this is the team-safe undo |
| `git reset <commit>` | Moves your branch pointer **backward**, un-committing | **Yes it rewrites** | **No** dangerous once others have pulled |
| `git restore <file>` | **Uncommitted** edits in your working tree | No | Yes; there's nothing shared to break |
| `git revert <commit>` | An **already-committed** change, by writing a *new* inverse commit | No; it *adds* | **Yes**; this is the team-safe undo |
| `git reset <commit>` | Moves your branch pointer **backward**, un-committing | **Yes; it rewrites** | **No**; dangerous once others have pulled |
`restore` you already met in Module 2 it's for the mess that hasn't been committed yet. This module
`restore` you already met in Module 2; it's for the mess that hasn't been committed yet. This module
is the other two rows, because the AI's worst messes are the ones that already made it into a commit,
a merge, or a PR.
### `git revert` undo by adding, not erasing
### `git revert`: undo by adding, not erasing
The mental model: a commit is a diff (a set of line changes). `git revert <commit>` computes the
*opposite* diff and commits it. The bad change is still in the history but a new commit immediately
*opposite* diff and commits it. The bad change is still in the history, but a new commit immediately
after it cancels it out. The net effect on your files is "as if it never happened"; the net effect on
your *history* is "we tried it, then we deliberately undid it," which is honest and readable.
@@ -84,7 +84,7 @@ This also maps straight back to the Module 2 reframe: the repo is durable memory
is *more* informative than a silent erase. Six months later, `git log` tells you the feature was
tried and pulled, and the message says why. You're writing the project's memory, not editing it.
### Reverting a bad **merge** the headline case
### Reverting a bad **merge**: the headline case
This is the one that bites people, because it's exactly what happens when a bad PR gets merged
(Modules 1011): you don't have one bad commit, you have a *merge commit* that pulled in a whole
@@ -95,14 +95,14 @@ error: commit abc123 is a merge but no -m option was given.
fatal: revert failed
```
A merge commit has **two parents** the branch you were on, and the branch you merged in. Git can't
A merge commit has **two parents**: the branch you were on, and the branch you merged in. Git can't
guess which side is "the mainline you want to keep." You tell it with `-m`:
```bash
git revert -m 1 <merge-sha>
```
`-m 1` means "treat parent #1 the branch I was sitting on when I merged, i.e. `main` as the line
`-m 1` means "treat parent #1 (the branch I was sitting on when I merged, i.e. `main`) as the line
to keep, and undo everything the *other* side brought in." `-m 2` would mean the opposite. For "a bad
feature got merged into main," it's almost always `-m 1`. You can confirm the parents before you act:
@@ -118,11 +118,11 @@ re-merge a branch whose merge you reverted, **revert the revert** first (`git re
then add your new work on top, then merge. This is a real, recurring source of "why didn't my merge
do anything," and now you know the cause.
### `git reset` moving the branch pointer (and why it's sharp)
### `git reset`: moving the branch pointer (and why it's sharp)
`git reset <commit>` doesn't write an inverse commit. It **moves your current branch to point at an
older commit**, effectively un-committing everything after it. Because it changes *which commits the
branch contains*, it rewrites history and that's both its power and its danger.
branch contains*, it rewrites history, and that's both its power and its danger.
It comes in three flavors that differ only in what they do to your files:
@@ -138,7 +138,7 @@ git reset --hard HEAD~1 # un-commit AND throw the changes away entirely
- `--hard` deletes the changes from your working tree too. This is the one that ruins days.
**When `reset` is correct:** *only on history you have not shared.* Cleaning up your own local
commits before you push squashing three "wip" commits into one, fixing a botched last commit is
commits before you push (squashing three "wip" commits into one, fixing a botched last commit) is
exactly what it's for. The moment a commit has been pushed and someone else has pulled it, `reset`
becomes a way to *rewrite history out from under them*: your branch and theirs now disagree about
what happened, and the only way to push your rewritten version is `--force`, which overwrites the
@@ -148,11 +148,11 @@ The rule, stated plainly:
> **Already shared? Use `revert`. Only ever local? `reset` is fine.** When unsure, assume shared.
### `git reflog` recovering commits you thought you destroyed
### `git reflog`: recovering commits you thought you destroyed
Here's the reassuring part. `reset --hard` *feels* like it nukes commits permanently. It almost
never does. Git keeps a private, local log of **everywhere `HEAD` has ever pointed** every commit,
reset, checkout, merge, rebase in the *reflog*. A commit you "lost" with `reset --hard` is no
never does. Git keeps a private, local log of **everywhere `HEAD` has ever pointed**: every commit,
reset, checkout, merge, and rebase lands in the *reflog*. A commit you "lost" with `reset --hard` is no
longer reachable from your branch, but it's still in the object database, and the reflog still knows
its SHA.
@@ -161,7 +161,7 @@ git reflog
# 9f8e7d6 HEAD@{0}: reset: moving to HEAD~1
# a1b2c3d HEAD@{1}: commit: Add the feature I just "lost" <- there it is
# ...
git reset --hard a1b2c3d # branch pointer back to the lost commit fully recovered
git reset --hard a1b2c3d # branch pointer back to the lost commit, fully recovered
# or, more cautiously, inspect it first on a throwaway branch:
git branch recovered a1b2c3d
```
@@ -173,13 +173,13 @@ don't know it exists until the day they need it.
Two limits, because they matter: the reflog is **local only** (it's not pushed; a fresh clone
has an empty reflog), and entries **expire**. Unreachable ones are garbage-collected after roughly
30 days by default, reachable ones after about 90. The reflog is a recovery net for *recent* mistakes
on *your* machine, not an archive. (And it can only recover what was *committed* see "Where it
on *your* machine, not an archive. (And it can only recover what was *committed*; see "Where it
breaks.")
### Tags and releases named recovery points
### Tags and releases: named recovery points
Commits have SHAs; SHAs are unmemorable. A **tag** is a human-readable, permanent name pinned to a
specific commit a recovery point you can actually find later.
specific commit, a recovery point you can actually find later.
```bash
git tag -a v1.0 -m "Last known-good before the big AI refactor" # annotated tag on HEAD
@@ -192,7 +192,7 @@ git checkout v1.0 # inspect the exact known-good state
Use them as deliberate checkpoints: **before you turn an agent loose on a large, sweeping change, tag
the known-good state.** If the refactor goes wrong, `v1.0` is a named anchor you can diff against or
return to without spelunking through `log` for the right SHA. On your git host, a **release** is a tag
plus notes and downloadable artifacts the same idea, dressed up as a thing the rest of the team can
plus notes and downloadable artifacts, the same idea dressed up as a thing the rest of the team can
point at. Tags are the durable, *shareable* recovery points the reflog is not.
---
@@ -201,16 +201,16 @@ point at. Tags are the durable, *shareable* recovery points the reflog is not.
Recovery was always a real skill. AI raises its value on every axis:
- **AI makes bigger, bolder changes faster and lands them through the same PR door.** A sweeping
- **AI makes bigger, bolder changes faster, and lands them through the same PR door.** A sweeping
"refactor the whole module" that *looks* right, passes a human skim (Module 10), gets merged
(Module 11), and only then reveals it broke something. That's a bad *merge* on shared history the
(Module 11), and only then reveals it broke something. That's a bad *merge* on shared history, the
exact case `git revert -m 1` exists for. The faster code merges, the more you need the clean,
team-safe undo.
- **Agents run destructive git commands.** An agent told to "clean up the branch history" can reach
for `reset --hard` or a force-push and vaporize work. `reflog` is your net for precisely this
for `reset --hard` or a force-push and vaporize work. `reflog` is your net for precisely this,
which is why an IT pro supervising agents needs it *cold*, not as trivia.
- **Recovery is durable memory, done right.** A `revert` commit records that something was tried and
pulled, and why readable by the next session (Module 2's reframe) and by the next teammate. A
pulled, and why, readable by the next session (Module 2's reframe) and by the next teammate. A
silent `reset` erases that memory. On a project where agents reconstruct state from `git log`,
preferring `revert` over `reset` keeps the history honest for the next agent that reads it.
- **The "tag before the risky thing" habit is an AI habit.** The riskiest changes in your week are
@@ -236,7 +236,7 @@ do them once on purpose now.
command, so everyone produces the *same* bad merge instead of relying on the AI to misbehave on cue.
> **A note on realism.** By now (postModule 4) your AI edits files directly. We hand you the exact
> broken snippet anyway so the lab is deterministic the point is practicing the *recovery*, not
> broken snippet anyway so the lab is deterministic; the point is practicing the *recovery*, not
> waiting for a model to break something on demand.
You direct the agent to do the git work and you verify the result. The whole point of this lab is
@@ -244,7 +244,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
1. Get the repo onto a clean `main`. Tell your agent:
> Make sure `~/ai-workflow-course/tasks-app` is on a clean `main` switch to it and confirm
> Make sure `~/ai-workflow-course/tasks-app` is on a clean `main`; switch to it and confirm
> there's nothing uncommitted.
Verify before you go further:
@@ -284,7 +284,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash
python cli.py add "ship it"
python cli.py clear # prints "cleared all tasks" looks fine!
python cli.py clear # prints "cleared all tasks", looks fine!
python cli.py list # CRASHES: it corrupted tasks.json, load() blows up
```
@@ -312,7 +312,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
git revert -m 1 <merge-sha> # writes a NEW commit that undoes the whole merge
```
6. **Verify and decide this is the part you own.** Don't take "I reverted it" on faith. Confirm the
6. **Verify and decide; this is the part you own.** Don't take "I reverted it" on faith. Confirm the
agent kept the *right* parent: parent 1 is the old `main` tip, parent 2 is `bad-clear`, and `-m 1`
keeps parent 1. If it had used `-m 2` it would have kept the broken side.
@@ -326,7 +326,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
```bash
rm -f tasks.json # drop the corrupted state file the bug wrote
python cli.py add "back to normal"
python cli.py list # works again the clear command is gone
python cli.py list # works again, the clear command is gone
git log --oneline # the bad merge is STILL there, with a revert after it
```
@@ -337,7 +337,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
That last point is the whole lesson: you undid the effect **without rewriting history**. Anyone who
pulled the bad merge just pulls your revert on top and they're fine.
### Part B "Lose" a commit, recover it with the reflog
### Part B: "Lose" a commit, recover it with the reflog
1. Make a small real commit you'd be sad to lose. Tell your agent:
@@ -380,7 +380,7 @@ that *you* hold the judgment: which undo, which parent, whether it actually work
**not** have saved those, because they were never committed. Recovery covers committed history, not
unsaved scratch work.
### Part C (optional) Drop a named recovery point
### Part C (optional): Drop a named recovery point
Before you hand the agent something sweeping, have it tag the current known-good state:
@@ -405,27 +405,27 @@ important thing it teaches is **where the analogy stops.** Git gives you excelle
logical recovery for versioned text*. It is emphatically **not** a general backup system. Treating it
like one is how people lose data they thought was safe.
- **It is not backup for your database or any runtime state.** Your app's data lives in a database,
- **It is not backup for your database, or any runtime state.** Your app's data lives in a database,
in object storage, on a running server. None of that is in the repo (and shouldn't be). `git revert`
rolls back *code*; it does nothing for the rows your buggy migration already mangled. Restoring data
is a different discipline with different tools Git has no opinion on it.
- **It is not backup for secrets which shouldn't be in there anyway.** API keys, tokens, and
is a different discipline with different tools; Git has no opinion on it.
- **It is not backup for secrets, which shouldn't be in there anyway.** API keys, tokens, and
credentials don't belong in the repo in the first place (Module 17 is the whole story). If they *did*
leak in, note the trap: `revert` does **not** remove them from history the secret is still sitting
leak in, note the trap: `revert` does **not** remove them from history; the secret is still sitting
in the old commit for anyone with the repo. A committed secret is a *leaked* secret; rotate it, don't
just revert it.
- **It only recovers what was committed.** This is Module 2's limit, sharpened. `reset --hard` and
`git restore` both destroy *uncommitted* working-tree changes, and **the reflog cannot bring those
back** there's no object to recover because nothing was ever committed. The defense is the same one
back**; there's no object to recover because nothing was ever committed. The defense is the same one
the whole course keeps repeating: commit often, so "uncommitted" is always a small window.
- **It is poor backup for large binaries.** Git versions text beautifully and binaries terribly
(Module 3): every change to a big binary stores a whole new copy, bloating the repo, and the "diff"
is useless noise you can't review or merge. Datasets, video, compiled artifacts, model weights
is useless noise you can't review or merge. Datasets, video, compiled artifacts, model weights:
these need real artifact/object storage, not your Git history.
- **The reflog is local and temporary.** It's your machine only not pushed, empty in a fresh clone
- **The reflog is local and temporary.** It's your machine only (not pushed, empty in a fresh clone),
and it's garbage-collected (roughly 30 days for unreachable entries). It's a recovery net for recent
local mistakes, not an offsite archive. The *offsite, distributed* durability comes from pushing to
remotes which is exactly Module 8's half of this thread. Recovery (this module) and backup
remotes, which is exactly Module 8's half of this thread. Recovery (this module) and backup
(Module 8) are two different powers; you need both.
- **Reverting a merge has a sting in the tail.** As covered above: once you `revert -m 1` a merge,
re-merging that branch later quietly does nothing useful until you *revert the revert*. Forget this
@@ -442,13 +442,13 @@ more. Know that boundary and you'll trust it exactly as far as it deserves.
- You can state, without looking, which undo to use for (a) an uncommitted mess, (b) a bad change
already pushed to a shared branch, and (c) three local "wip" commits you want to squash before
pushing and why the wrong choice is wrong in each case.
pushing, and why the wrong choice is wrong in each case.
- You have reverted a real merge commit with `git revert -m 1` on your `tasks-app`, and your `git log`
shows both the bad merge and the revert sitting on top of it (history preserved, effect undone).
- You have "lost" a commit with `reset --hard` and recovered it from `git reflog`.
- You can explain, in one breath, four things Git is *not* a backup for: your database, your secrets,
your uncommitted changes, and your large binaries and why the reflog wouldn't have saved the third.
your uncommitted changes, and your large binaries, and why the reflog wouldn't have saved the third.
When `revert` vs. `reset` is automatic, the reflog feels like a safety net instead of a rumor, and you
can name where Git's recovery stops, you've got the recovery half of the thread. That completes the
team layer (Unit 2) next, Unit 3 starts automating the checking and shipping, beginning with tests.
team layer (Unit 2); next, Unit 3 starts automating the checking and shipping, beginning with tests.
@@ -1,9 +1,9 @@
# Module 12 lab the deliberately BROKEN `clear` command.
# Module 12 lab: the deliberately BROKEN `clear` command.
#
# Paste the elif block below into cli.py's main(), alongside the other
# `elif command == "..."` branches (e.g. right after the "done" branch).
# Do NOT paste this header or the import line into cli.py if json is already
# imported there (it is) just the elif block.
# imported there (it is); just the elif block.
#
# Why it's broken: it "works" once (prints a friendly message), but it writes
# the state file in the WRONG SHAPE. The next time the app loads tasks.json,