Compare commits

...

2 Commits

Author SHA1 Message Date
b233fe41e7 skills: Add project-specific skill library. 2026-07-05 15:16:53 -04:00
47241b8421 skills: Add /improve-codebase 2026-07-05 14:47:06 -04:00
8 changed files with 550 additions and 0 deletions

View File

@@ -0,0 +1,119 @@
# HTML Report Format
The architectural review is rendered as a single self-contained HTML file in the OS temp directory. Tailwind and Mermaid both come from CDNs. Mermaid handles graph-shaped diagrams reliably; hand-built divs and inline SVG handle the more editorial visuals (mass diagrams, cross-sections). Mix the two — don't lean on Mermaid for everything, it'll start to look generic.
## Scaffold
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Architecture review — {{repository name}}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
mermaid.initialize({ startOnLoad: true, theme: "neutral", securityLevel: "loose" });
</script>
<style>
/* small custom layer for things Tailwind doesn't cover cleanly:
dashed seam lines, hand-drawn-feeling arrow heads, etc. */
.seam { stroke-dasharray: 4 4; }
.leak { stroke: #dc2626; }
.deep { background: linear-gradient(135deg, #0f172a, #1e293b); }
</style>
</head>
<body class="bg-stone-50 text-slate-900 font-sans">
<main class="max-w-5xl mx-auto px-6 py-12 space-y-12">
<header>...</header>
<section id="candidates" class="space-y-10">...</section>
<section id="top-recommendation">...</section>
</main>
</body>
</html>
```
## Header
Repo name, date, and a compact legend: solid box = module, dashed line = seam, red arrow = leakage, thick dark box = deep module. No introduction paragraph — straight into the candidates.
## Candidate card
The diagrams carry the weight. Prose is sparse, plain, and uses the glossary terms (from the `/codebase-design` skill) without ceremony.
Each candidate is one `<article>`:
- **Title** — short, names the deepening (e.g. "Collapse the Order intake pipeline").
- **Badge row** — recommendation strength (`Strong` = emerald, `Worth exploring` = amber, `Speculative` = slate), plus a tag for the dependency category (`in-process`, `local-substitutable`, `ports & adapters`, `mock`).
- **Files** — monospaced list, `font-mono text-sm`.
- **Before / After diagram** — the centrepiece. Two columns, side by side. See patterns below.
- **Problem** — one sentence. What hurts.
- **Solution** — one sentence. What changes.
- **Wins** — bullets, ≤6 words each. e.g. "Tests hit one interface", "Pricing logic stops leaking", "Delete 4 shallow wrappers".
- **ADR callout** (if applicable) — one line in an amber-tinted box.
No paragraphs of explanation. If the diagram needs a paragraph to be understood, redraw the diagram.
## Diagram patterns
Pick the pattern that fits the candidate. Mix them. Don't make every diagram look the same — variety is part of the point.
### Mermaid graph (the workhorse for dependencies / call flow)
Use a Mermaid `flowchart` or `graph` when the point is "X calls Y calls Z, and look at the mess." Wrap it in a Tailwind-styled card so it doesn't feel parachuted in. Style with classDef to colour leakage edges red and the deep module dark. Sequence diagrams work well for "before: 6 round-trips; after: 1."
```html
<div class="rounded-lg border border-slate-200 bg-white p-4">
<pre class="mermaid">
flowchart LR
A[OrderHandler] --> B[OrderValidator]
B --> C[OrderRepo]
C -.leak.-> D[PricingClient]
classDef leak stroke:#dc2626,stroke-width:2px;
class C,D leak
</pre>
</div>
```
### Hand-built boxes-and-arrows (when Mermaid's layout fights you)
Modules as `<div>`s with borders and labels. Arrows as inline SVG `<line>` or `<path>` elements positioned absolutely over a relative container. Reach for this when you want the "after" diagram to feel like one thick-bordered deep module with greyed-out internals — Mermaid won't render that with the right weight.
### Cross-section (good for layered shallowness)
Stack horizontal bands (`h-12 border-l-4`) to show layers a call passes through. Before: 6 thin layers each doing nothing. After: 1 thick band labelled with the consolidated responsibility.
### Mass diagram (good for "interface as wide as implementation")
Two rectangles per module — one for interface surface area, one for implementation. Before: interface rectangle is nearly as tall as the implementation rectangle (shallow). After: interface rectangle is short, implementation rectangle is tall (deep).
### Call-graph collapse
Before: a tree of function calls rendered as nested boxes. After: the same tree collapsed into one box, with the now-internal calls shown faded inside it.
## Style guidance
- Lean editorial, not corporate-dashboard. Generous whitespace. Serif optional for headings (`font-serif` works well with stone/slate).
- Colour sparingly: one accent (emerald or indigo) plus red for leakage and amber for warnings.
- Keep diagrams ~320px tall so before/after sits comfortably side by side without scrolling.
- Use `text-xs uppercase tracking-wider` for module labels inside diagrams — they should read as schematic, not as UI.
- The only scripts are the Tailwind CDN and the Mermaid ESM import. The report is otherwise static — no app code, no interactivity beyond Mermaid's own rendering.
## Top recommendation section
One larger card. Candidate name, one sentence on why, anchor link to its card. That's it.
## Tone
Plain English, concise — but the architectural nouns and verbs come straight from the `/codebase-design` glossary, terms and exclusions alike. Concision is not an excuse to drift.
**Phrasings that fit the style:**
- "Order intake module is shallow — interface nearly matches the implementation."
- "Pricing leaks across the seam."
- "Deepen: one interface, one place to test."
- "Two adapters justify the seam: HTTP in prod, in-memory in tests."
**Wins bullets** name the gain in glossary terms: *"locality: bugs concentrate in one module"*, *"leverage: one interface, N call sites"*, *"interface shrinks; implementation absorbs the wrappers"*. Don't write *"easier to maintain"* or *"cleaner code"* — those terms aren't in the glossary and don't earn their place.
No hedging, no throat-clearing, no "it's worth noting that…". If a sentence could be a bullet, make it a bullet. If a bullet could be cut, cut it. If a term isn't in the `/codebase-design` glossary, reach for one that is before inventing a new one.

View File

@@ -0,0 +1,68 @@
---
name: improve-codebase
description: Scan a codebase for deepening opportunities, present them as a visual HTML report, then grill through whichever one you pick.
disable-model-invocation: true
---
# Improve Codebase
Surface architectural friction and propose **deepening opportunities** — refactors that turn shallow modules into deep ones. The aim is testability and AI-navigability.
This command is _informed_ by the project's domain model and built on a shared design vocabulary:
- Run the `/codebase-design` skill for the architecture vocabulary (**module**, **interface**, **depth**, **seam**, **adapter**, **leverage**, **locality**) and its principles (the deletion test, "the interface is the test surface", "one adapter = hypothetical seam, two = real"). Use its terms exactly in every suggestion, per its glossary.
- The domain language in `.claude/CONTEXT.md` gives names to good seams; ADRs in `.claude/adr/` record decisions this command should not re-litigate.
## Process
### 1. Explore
Read the project's domain glossary (`.claude/CONTEXT.md`) and any ADRs in the area you're touching first.
Then use the Agent tool with `subagent_type=Explore` to walk every top-level module or directory in scope (the whole repository, or the area the user pointed you to) — even if only briefly for the ones that turn out clean. Within each, judge friction organically rather than against a rigid checklist:
- Where does understanding one concept require bouncing between many small modules?
- Where are modules **shallow** — interface nearly as complex as the implementation?
- Where have pure functions been extracted just for testability, but the real bugs hide in how they're called (no **locality**)?
- Where do tightly-coupled modules leak across their seams?
- Which parts of the codebase are untested, or hard to test through their current interface?
Apply the **deletion test** to anything you suspect is shallow: would deleting it concentrate complexity, or just move it? A "yes, concentrates" is the signal you want.
Zero candidates is a legitimate outcome for a genuinely clean area — but it has to follow from having looked, not from stopping early.
### 2. Present candidates as an HTML report
Write a self-contained HTML file to the OS temp directory so nothing lands in the repository. Resolve the temp dir from `$TMPDIR`, falling back to `/tmp` (or `%TEMP%` on Windows), and write to `<tmpdir>/architecture-review-<timestamp>.html` so each run gets a fresh file. Open it for the user — `xdg-open <path>` on Linux, `open <path>` on macOS, `start <path>` on Windows. Treat the open as best-effort: it's a no-op in a headless/sandboxed environment with no display server, so report the absolute path regardless of whether the open succeeded.
The report uses **Tailwind via CDN** for layout and styling, and **Mermaid via CDN** for diagrams where a graph/flow/sequence reliably communicates the structure. Mix Mermaid with hand-crafted CSS/SVG visuals — use Mermaid when relationships are graph-shaped (call graphs, dependencies, sequences), and hand-built divs/SVG when you want something more editorial (mass diagrams, cross-sections, collapse animations). Each candidate gets a **before/after visualisation**. Be visual.
For each candidate, render a card with:
- **Files** — which files/modules are involved
- **Problem** — why the current architecture is causing friction
- **Solution** — plain English description of what would change
- **Benefits** — explained in terms of locality and leverage, and how tests would improve
- **Before / After diagram** — side-by-side, custom-drawn, illustrating the shallowness and the deepening
- **Recommendation strength** — one of `Strong`, `Worth exploring`, `Speculative`, rendered as a badge
End the report with a **Top recommendation** section: which candidate you'd tackle first and why.
**Use `.claude/CONTEXT.md` vocabulary for the domain, and the `/codebase-design` vocabulary for the architecture.** If `.claude/CONTEXT.md` defines "Order," talk about "the Order intake module" — not "the FooBarHandler," and not "the Order service."
**ADR conflicts**: if a candidate contradicts an existing ADR, only surface it when the friction is real enough to warrant revisiting the ADR. Mark it clearly in the card (e.g. a warning callout: _"contradicts ADR-0007 — but worth reopening because…"_). Don't list every theoretical refactor an ADR forbids.
See [HTML-REPORT.md](HTML-REPORT.md) for the full HTML scaffold, diagram patterns, and styling guidance.
Do NOT propose interfaces yet. After the file is written, ask the user: "Which of these would you like to explore?"
### 3. Grilling loop
Once the user picks a candidate, run `/grill` to walk the design tree with them — constraints, dependencies, the shape of the deepened module, what sits behind the seam, what tests survive.
Side effects happen inline as decisions crystallize — run `/domain-modeling` to keep the domain model current as you go, even if `.claude/CONTEXT.md` doesn't exist yet:
- **Naming a deepened module after a concept not in `.claude/CONTEXT.md`?** Add the term to `.claude/CONTEXT.md`. Create the file lazily if it doesn't exist.
- **Sharpening a fuzzy term during the conversation?** Update `.claude/CONTEXT.md` right there.
- **User rejects the candidate with a load-bearing reason?** Offer an ADR, framed as: _"Want me to record this as an ADR so future architecture reviews don't re-suggest it?"_ Only offer when the reason would actually be needed by a future explorer to avoid re-suggesting the same thing — skip ephemeral reasons ("not worth it right now") and self-evident ones.
- **Want to explore alternative interfaces for the deepened module?** Run the `/codebase-design` skill and use its design-it-twice parallel sub-agent pattern.

View File

@@ -0,0 +1,152 @@
---
name: nbdev
description: nbdev conventions for notebooks — directives, cell structure, docments, tests, execution. Use for any .ipynb operation — including reads — in an nbdev project.
---
# nbdev
## Tool Preference
- Use the **Jupyter MCP** for all `.ipynb` operations — read, edit, insert, delete, execute
- Do **not** use the built-in `NotebookEdit` tool; it writes cell source as a single JSON string which breaks standard Jupyter formatting and produces noisy diffs
- Re-read the notebook before editing if it may have changed since your last read — cell indices/IDs can shift under concurrent edits (e.g. via JupyterLab's real-time collaboration), and editing by a stale index can hit the wrong cell
## nbdev Directives
Directives are comments at the top of a cell that control how nbdev processes it:
- `#| export` — include this cell in the exported Python module and in the docs
- `#| hide` — exclude this cell from both the module and the docs
- `#| hide_input` — show cell output in docs but hide the source code
- `#| default_exp module_name` — set which module this notebook exports to (second cell)
- `#| exporti` — export to module but do not show in docs (for internal helpers)
- `#| eval: false` — include in docs but do not execute during `nbdev-test`
Imports needed only for tests or examples should **not** be exported.
Never hand-edit the exported `.py` module files — they're build artifacts regenerated from the notebook by `nbdev_export`. All edits go through the source notebook in `nbs/`.
## Notebook Structure
Every notebook must follow this structure:
**Cell 1 — Markdown frontmatter:**
```markdown
# Module Title
> A one-line description of what this module does
```
The H1 becomes the page title in docs. The blockquote becomes the subtitle.
**Cell 2 — Default export:**
```python
#| default_exp module_name
```
**Body cells** — alternating between exported code, demonstrations, and markdown explanations (see Cell Structure below).
**Last cell:**
```python
#| hide
import nbdev; nbdev.nbdev_export()
```
Before declaring any notebook task complete, restart the kernel and run all cells top-to-bottom to verify it is fully reproducible.
## Cell Structure
Keep cells short. Each exported function gets its own cell, immediately followed by a demonstration. Do not write long functions with comments interspersed — split them into small separate cells with explanations and working examples after each.
The pattern per concept:
1. *(Optional)* A markdown cell explaining what comes next
2. A `#| export` code cell with the function
3. One or more plain code cells demonstrating usage
4. Assertions that double as tests
Example:
```python
#| export
def slugify(text: str) -> str:
"Convert text to a URL-safe slug"
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
```
```python
slug = slugify("Hello, World!")
assert slug == "hello-world"
slug
```
## Docstrings and Parameter Documentation
Keep docstrings short — a single-line summary is sufficient for most functions. Elaborate in separate markdown or code cells below, where you can use real examples.
Use **docments** (inline parameter comments) instead of verbose docstring parameter sections:
```python
#| export
def greet(
name: str, # Person to greet
greeting: str="Hi", # Greeting word to use
) -> str: # The composed greeting
"Compose a greeting for name"
return f"{greeting}, {name}!"
```
This renders as a clean parameter table in the docs automatically — no need to repeat type information in the docstring body.
Use backticks around symbol names in docstrings and markdown — nbdev automatically converts these to hyperlinks to the relevant reference page.
## Code Style
- **Prefer composition**: write small functions that do one thing well
- Each exported function should be focused enough to fit naturally in a single notebook cell — one cell, one idea
- Use type hints on all exported functions
- Avoid classes unless state is genuinely needed — prefer functions that take and return data
- If you do write a class, use `fastcore`'s `@patch` decorator to define each method in its own cell, immediately followed by a demonstration. This avoids long class definitions and keeps examples close to the code
When a class is needed, document its methods with `show_doc`:
```python
from nbdev.showdoc import show_doc
show_doc(MyClass.my_method)
```
## Tests
Every code cell is run as a test by nbdev unless explicitly marked otherwise — any exception fails the test.
- Turn demonstrations into tests by adding `assert` statements
- Use `fastcore.test` helpers for better error messages:
```python
from fastcore.test import test_eq, test_fail
test_eq(slugify("Hello World"), "hello-world")
```
- Document expected error cases with `test_fail`:
```python
test_fail(lambda: slugify(""), contains="empty")
```
- Each test/demo cell should import what it needs directly — don't rely on a name imported in a later cell just because it happened to be in scope during a prior run
## Execution
- Always execute cells after writing them to verify they work
- If a cell errors, read the full traceback before attempting a fix — do not guess
- When installing packages, use `%pip install` inside the notebook (not `!pip install`) so they install into the running kernel
- Use autoreload at the top of notebooks that import from other modules in the project:
```python
%load_ext autoreload
%autoreload 2
```
## Documentation
- Use H2 (`##`) markdown cells to group related symbols within a notebook
- Use H4 (`####`) markdown cells to split long explanations within a symbol's section (notes, examples, edge cases, etc.)
- Add rich representations to classes via `_repr_markdown_` where it aids understanding
- Include real code examples, plots, and diagrams — notebooks support rich output, use it
## Outputs
- Never print secrets, tokens, passwords, or API keys into cell output — notebook outputs get committed to git and published in docs, unlike transient script output
- Prefer summaries over dumping large data structures (`.head()`, `len()`, `[:5]`, etc.)
- Large outputs consume context window — keep them concise

View File

@@ -0,0 +1,37 @@
---
name: remove-skills
description: Remove one or more previously added library skills from the current project.
disable-model-invocation: true
---
Removes a skill that [`setup-skills`](../setup-skills/SKILL.md) previously
copied into the current project, deleting both its files and its entry in
`.claude/skills-lock.yaml` (see [LOCKFILE.md](../setup-skills/LOCKFILE.md)
for its schema).
## Steps
1. Read `.claude/skills-lock.yaml`. If it doesn't exist or is empty, tell
the user there's nothing installed to remove and stop.
2. Determine which skill(s) to remove:
- If the user's invocation already named a specific skill, use that —
if it isn't in the lockfile, say so and stop.
- Otherwise, list every skill currently in the lockfile and ask the
user to pick one (or more).
3. For each skill to remove, compute its current hash
(`~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`)
and compare it to the hash stored in the lockfile:
- If it matches (never modified since it was installed), delete
`.claude/skills/<name>/` and remove its lockfile entry immediately —
no confirmation needed, since nothing of the user's is being lost.
- If it differs (locally customized), tell the user it has local
changes that will be permanently lost and ask for confirmation
before deleting. If they decline, leave that skill installed and
move on to the next.
4. Finish with a summary of what was removed and what was left in place.
Done when every skill to remove has been either deleted (with its lockfile
entry removed) or explicitly left in place with a stated reason.

View File

@@ -0,0 +1,53 @@
# Skills Lockfile
`.claude/skills-lock.yaml`, at the root of a project, tracks which library
skills (from `~/.claude/skills/library/`) have been copied into that
project's `.claude/skills/`, so [`setup-skills`](SKILL.md),
[`update-skills`](../update-skills/SKILL.md), and
[`remove-skills`](../remove-skills/SKILL.md) all agree on what's installed
without re-deriving it from the filesystem.
## Schema
A YAML list of entries, one per installed skill:
```yaml
- name: nbdev
hash: 3f2a9b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a
- name: terraform-conventions
hash: 9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a3f2a9b
```
- `name` — matches both the skill's directory name in the library
(`skills/library/<name>`) and its copied directory name in the project
(`.claude/skills/<name>`).
- `hash` — the output of `hash-dir.sh` run against that one skill's
directory contents, recorded at the moment it was last copied or
confirmed up to date. Never a hash of anything else — not the whole
project, not the whole library, just that one skill's own directory
tree.
## What a mismatch means
To classify a skill's state, compare three values: the lockfile's stored
`hash`, `hash-dir.sh` on the project's current copy
(`.claude/skills/<name>`), and `hash-dir.sh` on the library's current
source (`~/.claude/skills/library/<name>`).
| stored vs. project copy | stored vs. library source | meaning |
|--------------------------|----------------------------|--------------------------------------|
| match | match | nothing to do |
| match | differs | library moved on — safe to update |
| differs | match | project customized on purpose — leave it |
| differs | differs | conflict — report, don't touch |
## Writing to the lockfile
- Adding a skill: append a new `{name, hash}` entry.
- Applying a safe update: overwrite that entry's `hash` in place with the
library's current hash.
- Removing a skill: delete its entry entirely.
Never reorder or restructure existing entries beyond what an add, update,
or remove requires — this file is meant to diff cleanly in a project's
git history.

View File

@@ -0,0 +1,46 @@
---
name: setup-skills
description: Add relevant skills from the shared skills library to the current project.
disable-model-invocation: true
---
Adds opt-in, project-specific skills from `~/.claude/skills/library/` into
the current project's `.claude/skills/`, tracked in
`.claude/skills-lock.yaml` (see [LOCKFILE.md](LOCKFILE.md) for its schema).
Only ever adds — checking already-installed skills for updates is
[`update-skills`](../update-skills/SKILL.md)'s job, not this one's.
## Steps
1. Read `.claude/skills-lock.yaml` in the current project, if it exists.
Note every skill name already listed — these are already installed and
must not be re-proposed.
2. List every skill under `~/.claude/skills/library/*/SKILL.md` and read
each one's `name` and `description`.
3. Inspect the current project (file tree, manifests like
`pyproject.toml`/`package.json`, file extensions present, etc.) and
judge which library skills — excluding ones already installed — seem
relevant, the same way you'd reason about any unfamiliar codebase.
Propose that shortlist to the user with your reasoning, one line per
skill. If the user asks to see the full catalog instead, list every
library skill (minus already-installed ones) with its description.
4. Let the user confirm, adjust, or pick freely from the full list.
5. For each confirmed skill:
- If `.claude/skills/<name>/` already exists in the project and is
*not* in the lockfile, skip it and tell the user why (a same-named
skill already lives there and isn't tracked — remove or rename it
first if they want the library version).
- Otherwise, copy `~/.claude/skills/library/<name>/` to
`.claude/skills/<name>/` in the project, run
`~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`,
and append `{name, hash: <output>}` to `.claude/skills-lock.yaml`
(create the file, an empty YAML list, if it doesn't exist yet).
6. Report what was added and what was skipped, and why.
Done when every confirmed skill is either copied and recorded in the
lockfile, or explicitly skipped with a stated reason.

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Deterministic recursive hash of a directory's file contents.
#
# Hashes relative paths, not absolute ones, so two directories with
# identical contents hash identically regardless of where they live on
# disk (needed to compare a project's copied skill against the library
# source it was copied from).
#
# Usage: hash-dir.sh <directory>
set -euo pipefail
if [ $# -ne 1 ]; then
echo "Usage: hash-dir.sh <directory>" >&2
exit 1
fi
dir="$1"
if [ ! -d "$dir" ]; then
echo "Not a directory: $dir" >&2
exit 1
fi
(cd "$dir" && find . -type f -print0 | sort -z | xargs -0 -r sha256sum) | sha256sum | awk '{print $1}'

View File

@@ -0,0 +1,52 @@
---
name: update-skills
description: Check the current project's installed library skills for upstream changes and apply the safe ones.
disable-model-invocation: true
---
Compares every skill listed in the current project's
`.claude/skills-lock.yaml` (see [LOCKFILE.md](../setup-skills/LOCKFILE.md)
for its schema) against both the project's own copy and the current
library source, and decides what to do about each one. Never installs a
skill that isn't already there — that's
[`setup-skills`](../setup-skills/SKILL.md)'s job.
## Steps
1. Read `.claude/skills-lock.yaml`. If it doesn't exist or is empty, tell
the user there's nothing to check and stop.
2. For each `{name, hash}` entry, compute:
- `project_hash`: `~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/<name>`
- `library_hash`: `~/.claude/skills/setup-skills/hash-dir.sh ~/.claude/skills/library/<name>`
If either path is missing entirely, report that anomaly for this skill
(don't try to classify it) and move on to the next entry.
3. Classify each entry against the table in
[LOCKFILE.md](../setup-skills/LOCKFILE.md#what-a-mismatch-means),
using `project_hash` in place of "project copy" and `library_hash` in
place of "library source". The two outcomes that need action below are
**safe update** (stored matches project, differs from library) and
**conflict** (stored differs from both). "Locally customized" needs no
message beyond the summary.
4. If there are any safe updates, list them by name and ask for one
confirmation to apply all of them — unless the user's invocation
already included an explicit go-ahead argument (e.g. `-y`, `yes`), in
which case apply them without asking. Applying means: delete
`.claude/skills/<name>/` entirely and copy
`~/.claude/skills/library/<name>/` in its place, so no file the project
copy had but the library no longer has can survive — then recompute its
hash and overwrite that entry's `hash` in `.claude/skills-lock.yaml` in
place.
5. For every conflict, report it and show a recursive diff between the
project's copy and the library's current version
(`diff -ru .claude/skills/<name> ~/.claude/skills/library/<name>`).
Do not modify the project's copy or the lockfile entry for a
conflicted skill under any circumstances — surfacing it is the whole
job here.
6. Finish with a summary: updated, left alone (customized), conflicted,
already current, and any anomalies from step 2.