diff --git a/.claude/skills/library/nbdev/SKILL.md b/.claude/skills/library/nbdev/SKILL.md new file mode 100644 index 0000000..3aeeb3a --- /dev/null +++ b/.claude/skills/library/nbdev/SKILL.md @@ -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 diff --git a/.claude/skills/remove-skills/SKILL.md b/.claude/skills/remove-skills/SKILL.md new file mode 100644 index 0000000..e23ea0f --- /dev/null +++ b/.claude/skills/remove-skills/SKILL.md @@ -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/`) + and compare it to the hash stored in the lockfile: + - If it matches (never modified since it was installed), delete + `.claude/skills//` 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. diff --git a/.claude/skills/setup-skills/LOCKFILE.md b/.claude/skills/setup-skills/LOCKFILE.md new file mode 100644 index 0000000..3ffa79d --- /dev/null +++ b/.claude/skills/setup-skills/LOCKFILE.md @@ -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/`) and its copied directory name in the project + (`.claude/skills/`). +- `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/`), and `hash-dir.sh` on the library's current +source (`~/.claude/skills/library/`). + +| 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. diff --git a/.claude/skills/setup-skills/SKILL.md b/.claude/skills/setup-skills/SKILL.md new file mode 100644 index 0000000..74d924e --- /dev/null +++ b/.claude/skills/setup-skills/SKILL.md @@ -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//` 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//` to + `.claude/skills//` in the project, run + `~/.claude/skills/setup-skills/hash-dir.sh .claude/skills/`, + and append `{name, hash: }` 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. diff --git a/.claude/skills/setup-skills/hash-dir.sh b/.claude/skills/setup-skills/hash-dir.sh new file mode 100755 index 0000000..267fee7 --- /dev/null +++ b/.claude/skills/setup-skills/hash-dir.sh @@ -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 +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "Usage: hash-dir.sh " >&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}' diff --git a/.claude/skills/update-skills/SKILL.md b/.claude/skills/update-skills/SKILL.md new file mode 100644 index 0000000..2d11a89 --- /dev/null +++ b/.claude/skills/update-skills/SKILL.md @@ -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/` + - `library_hash`: `~/.claude/skills/setup-skills/hash-dir.sh ~/.claude/skills/library/` + + 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//` entirely and copy + `~/.claude/skills/library//` 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/ ~/.claude/skills/library/`). + 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.